Files
EchoChat/.playwright-mcp/task16/scenarios/05-reopen-5times-resource-cleanup.md
bujinyuan 5ed14c2aac docs(meeting): Task 16 E2E 剧本落盘 + .gitignore 精细化
Playwright MCP 4 场景剧本(.playwright-mcp/task16/):
- README.md:总览 + 账号/环境/资源断言指南
- scenarios/01-create-and-join-by-code.md:创建→会议号加入→媒体打通→结束,含 Redis/MySQL/media-server 三维资源断言
- scenarios/02-invite-link-with-password.md:邀请链接带密码 + P0-4 "密码不走 URL query" 回归
- scenarios/03-notify-quickjoin.md:通知中心「立即加入」跳转 + 过期卡片降级
- scenarios/04-host-toolkit-and-transfer.md:主持人四件套 + 宽限期重连/到期自动转让(P1-1 原子性 + P1-7 处理锁验证)
- scenarios/05-reopen-5times-resource-cleanup.md:反复开关 5 次 + 前端 heap 快照/ Redis 累积对比/ media-server 观测端点对比,验证 Step A(f5ae095)资源清理修复

手工回归点(manual-regression.md):17 项 MCP 难以稳定覆盖的交互场景
(HTTPS 自签 / 跨网段 iOS / 网络抖动 / 物理返回 / 跨 tab 并发 / 摄像头抢占 等)

.gitignore 调整:
- 放行 .playwright-mcp/task16/ 下的 Markdown 剧本文档进入版本控制
- 仍忽略运行时产物(*.log/*.png/*.yml/截图)
- task15/ 保持未跟踪状态,避免历史截图噪声入库

Made-with: Cursor
2026-04-23 17:21:42 +08:00

131 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 场景 05反复开关 5 次会议 —— 资源清理专项回归
## 目标
- **端到端验证 Step A 资源清理修复(提交 `f5ae095`**
- 前端 `_engine` / `AudioContext` / `setInterval` / `_pendingBroadcastTimers` 在每次结束会议后完全释放
- 后端 Redis `resource:*` / `member_state:*` / `host_grace:*` / `empty_ttl:*` 零累积
- mediasoup Router / Transport / Producer / Consumer 不残留
- 回归 B1 / B2 / B3 三条资源清理路径
- 资源断言曲线5 轮循环后 Redis `dbsize` 与 mediasoup `/internal/info` 恢复到 baseline
## 前置条件
依 [`../README.md`](../README.md) 通用前置;额外确认:
- 单浏览器 context 双 tabAlice + Bob
- 开启 Chromium DevTools → Memory → **勾选「Record allocation stacks」**,在脚本执行前 + 第 5 轮结束后各拍一次 heap snapshot
## MCP 执行脚本
### 步骤 0baseline 采集
```bash
# baseline_redis.txt
redis-cli DBSIZE | tee /tmp/baseline_redis.txt
redis-cli KEYS 'echo:meeting:*' | wc -l >> /tmp/baseline_redis.txt
# baseline_media.json
curl -s -H "X-Internal-Token: $INTERNAL_TOKEN" http://localhost:3300/internal/info \
| jq .stats > /tmp/baseline_media.json
```
在 Alice tab 的 DevTools Performance 面板录制整段脚本;脚本起止时刻各打一个 `performance.mark`
### 步骤 1-5循环 5 次创建→打通→结束
`i in {1..5}`
1. **创建**Alice tab 会议首页 → 创建即时会议 → 设备预览 → 立即进入(记录 `$ROOM_CODE_i`
2. **加入**Bob tab 通过会议号加入 `$ROOM_CODE_i`
3. **打通断言**(轻量):两端 `browser_evaluate` 检查 `localAudioEnabled && localVideoEnabled`
4. **结束**Alice 点「结束会议」Bob tab 等待 `room.ended` toast
5. **结束后清理断言**(每轮都要做):
```bash
echo "===== Round $i ====="
redis-cli KEYS "echo:meeting:resource:$ROOM_CODE_i:*" # 必须为空
redis-cli KEYS "echo:meeting:member_state:$ROOM_CODE_i:*" # 必须为空
redis-cli KEYS "echo:meeting:host_grace:$ROOM_CODE_i" # 必须 nil
redis-cli KEYS "echo:meeting:empty_ttl:$ROOM_CODE_i" # 必须 nil
```
6. **前端资源断言**(每轮):
```js
// 在 Alice tab 的结束页执行
const store = window.__pinia?.state?.value?.meeting
return {
round: $i,
localState: store.localState, // 'ended'
pendingBroadcastTimers: window.__broadcastTimersSize ?? 'N/A',
}
```
7. **回首页**Alice tab 物理返回(`browser_press_key` Back / 点击结束页「返回」按钮)触发 `onUnload` → `exitEndedRoom`
8. `browser_evaluate` 再次确认 pinia state 全 reset`localState='idle' && participantCount=0 && !engineExists`
9. Bob tab 同样回首页
10. **延迟 2 秒**模拟用户操作间隙,进入下一轮
### 步骤 65 轮结束后的累积断言(核心)
```bash
# 对比 baseline必须零增量
echo "===== Redis 累积对比 ====="
redis-cli DBSIZE
redis-cli KEYS 'echo:meeting:*' | wc -l
diff /tmp/baseline_redis.txt <(redis-cli DBSIZE && redis-cli KEYS 'echo:meeting:*' | wc -l)
echo "===== media-server 累积对比 ====="
curl -s -H "X-Internal-Token: $INTERNAL_TOKEN" http://localhost:3300/internal/info | jq .stats > /tmp/after5_media.json
diff /tmp/baseline_media.json /tmp/after5_media.json
echo "===== MySQL 快照 ====="
mysql -e "SELECT COUNT(*) AS active FROM meeting_rooms WHERE status='active'" echochat
# 期望active=0
mysql -e "SELECT COUNT(*) AS alive FROM meeting_participants WHERE left_at IS NULL" echochat
# 期望alive=0
```
### 步骤 7前端内存快照对比
1. 在 Alice tab 打开 DevTools → Memory → 拍 heap snapshot #25 轮结束后)
2. 对比 #1baseline与 #2
- 关键类:`MediaStream` / `RTCPeerConnection` / `AudioContext` / `Consumer` / `Producer` 的对象数量
- 允许少量增长mediasoup-client `Device` 缓存),但必须远小于 5×
3. 记录 Performance tab 的「JS Heap」曲线应呈锯齿下降每次 GC 回收),**不得持续单调上升**
## 预期结果汇总
| 断言维度 | 期望 |
|---|---|
| 每轮 Redis 清理 | `resource/member_state/host_grace/empty_ttl` 全空 |
| 5 轮累积 Redis `DBSIZE` | 与 baseline 一致(允许 ±2 容错,针对 session/heartbeat key |
| 5 轮累积 media-server | Router / Transport / Producer / Consumer 回到 baseline |
| MySQL | `active` 会议数 = 0`left_at IS NULL` 参与者 = 0 |
| 前端 heap snapshot | 关键对象类数量增长 ≤ 2近似常数非线性 |
| 前端 Performance | JS Heap 曲线呈锯齿,无持续上升趋势 |
| 控制台 | 无 `unhandled promise rejection` / `NotReadableError` / `leaked listener` |
## 资源清理验证矩阵(对应 Step A 修复)
| 代码路径 | 验证手段 | 通过条件 |
|---|---|---|
| F1 `_onRoomEnded` 结束页数据保留 | `localState='ended'` 时 `currentRoom` 非 null | 结束页正确渲染 |
| F1 `exitEndedRoom` 动作 | onUnload 后 `currentRoom=null` | pinia state reset |
| F2 `endMeeting` 本地兜底 | 断开 WS 再结束(额外场景) | 本地仍能触发 `_onRoomEnded` |
| F3 `_pendingBroadcastTimers` | 结束后 `clearTimeout` | `window.__broadcastTimersSize=0` |
| B1 `cleanupRoomRedisResidual` | 每轮 Redis 查 | `resource:*` / `member_state:*` = 0 |
| B3 `OnRoomEnded` timer 取消 | pprof / sync.Map 大小 | `graceTimers` / `emptyTTLTimers` 稳定无增长 |
## 失败排查
| 现象 | 定位 |
|---|---|
| 某轮 `resource:*` 非空 | `EndRoom` 未走 cleanup → 检查 commit `f5ae095` 是否正确落地 |
| Redis `DBSIZE` 持续增长 | 检查 session/heartbeat key 是否泄漏;排除 meeting 模块 |
| 前端 heap 持续上升 | 抓 heap diff找保留路径常见`Producer._events`、`Vue reactive` 持有 Consumer |
| `localState` 卡 `ended` 不回 `idle` | `onUnload` 未调 `exitEndedRoom` 或 `room.vue` 组件未卸载(检查 H5 `uni.reLaunch` 行为) |
## 延伸测试(可选)
- **10 轮**:循环次数改 10观察是否线性无增长
- **跨 tab 并发**:第 3 轮时 Bob 主动 leave而非等 Alice end验证双路径清理一致
- **主动断网**:第 4 轮打通后 Alice 关网卡 2 分钟 → 触发 host grace → 进入场景 04 路径