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
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
# 场景 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 双 tab(Alice + Bob)
|
||||
- 开启 Chromium DevTools → Memory → **勾选「Record allocation stacks」**,在脚本执行前 + 第 5 轮结束后各拍一次 heap snapshot
|
||||
|
||||
## MCP 执行脚本
|
||||
|
||||
### 步骤 0:baseline 采集
|
||||
|
||||
```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 秒**模拟用户操作间隙,进入下一轮
|
||||
|
||||
### 步骤 6:5 轮结束后的累积断言(核心)
|
||||
|
||||
```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 #2(5 轮结束后)
|
||||
2. 对比 #1(baseline)与 #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 路径
|
||||
Reference in New Issue
Block a user