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
5.4 KiB
5.4 KiB
场景 04:主持人四件套 + 宽限期自动转让
目标
- 验证 Task 15 主持人四件套:请他静音、请他开麦、转让主持人、踢出会议
- 验证 Task 8 host 宽限期状态机(P1-1 原子转让 + P1-7 处理锁):
- host 主动断开 WS → 宽限期内重连 → 撤销转让
- host 宽限期到期 → 自动转让给最早加入的参会人
- 覆盖设计 §6.5(生命周期状态机)、§6.6(权限矩阵)
前置条件
依 ../README.md 通用前置;需 3 个账号 Alice/Bob/Carol 同时入会。
MCP 执行脚本 A:主持人四件套
步骤 1:三人入会
- Alice 创建即时会议 →
$ROOM_CODE - Bob 用会议号加入
- Carol 用会议号加入
- 截图 →
01-three-joined.png
步骤 2:Alice 请他静音 Bob
- Alice tab 打开成员面板 → Bob 右侧 「⋯」 菜单 → 「请他静音」
- Bob tab 期望:
- Toast:「主持人已将你的麦克风关闭」
localAudioEnabled=false- 成员面板自己的 AV 图标变灰
- Alice tab 期望:Bob 行 AV 图标变灰
- 截图 →
02-bob-muted-by-host.png
步骤 3:Alice 请他开麦 Bob
- Alice tab → Bob 「⋯」菜单 → 「请他开麦」
- Bob tab 期望:
- 弹窗请求确认「主持人请你开启麦克风,是否同意?」
- 点「同意」后
localAudioEnabled=true
- 截图 →
03-bob-ask-unmute.png
步骤 4:Alice 踢出 Carol
- Alice tab → Carol 「⋯」菜单 → 「踢出会议」
- 弹窗确认「踢出 Carol?」→ 确认
- Carol tab 期望:
- 收到
meeting.member.kicked广播 - Toast:「主持人已将你移出会议」
- 跳回会议首页
- 收到
- Alice / Bob tab 期望:Carol 行从成员列表消失
- 截图 →
04-carol-kicked.png
步骤 5:Alice 转让主持人给 Bob
- Alice tab → Bob 「⋯」菜单 → 「转让主持人」
- 弹窗确认 → 确认
- 原子性验证(P1-1):
期望同时满足:
mysql -e "SELECT host_id, (SELECT role FROM meeting_participants WHERE room_id=r.id AND user_id=55) AS alice_role, \ (SELECT role FROM meeting_participants WHERE room_id=r.id AND user_id=56) AS bob_role \ FROM meeting_rooms r WHERE room_code='$ROOM_CODE'" echochathost_id=56+alice_role=participant+bob_role=host(同一事务原子更新) - Alice tab / Bob tab 工具栏切换(Alice 看到"结束会议"按钮消失,Bob 看到它出现)
- 截图 →
05-host-transferred.png
MCP 执行脚本 B:宽限期自动转让
前置:接续场景 A 的会议;此时 Bob 是主持人,Alice 是参会人
步骤 6:Bob 断开 WS(模拟网络断开)
- Bob tab DevTools → Network 面板 → Throttling 切换到 Offline
- 等待 WS 心跳超时(~15s),Bob 客户端
_onWsDisconnected触发 - 期望 Redis 立即写入
echo:meeting:host_grace:$ROOM_CODE:redis-cli GET "echo:meeting:host_grace:$ROOM_CODE" # 期望:JSON {host_id:56, started_at:..., grace_until:...} redis-cli TTL "echo:meeting:host_grace:$ROOM_CODE" # 期望:约 120 秒(配置 `meeting.host_grace_seconds`) - Alice tab 期望:工具栏显示「主持人掉线中(宽限期 2:00)」倒计时(若 UI 已实现)
步骤 7a:宽限期内重连(撤销转让)
- 15 秒后 Bob tab Network 恢复 Online
- 期望
_onWsReconnected触发,执行OnHostReconnect:redis-cli EXISTS "echo:meeting:host_grace:$ROOM_CODE" # 期望:0(key 已 DEL) - Alice tab 工具栏倒计时消失
- 查日志:
tail -50 backend/go-service/logs/*.log | grep "host 宽限期内重连" # 期望:`host 宽限期内重连,撤销转让计划`
步骤 7b:宽限期到期(自动转让)
若步骤 7a 已执行,请先让 Bob 重新 Offline 并等待整个 120s 周期完成。
- Bob tab Offline,保持 120+s
- 期望本地 timer 到期触发
HandleHostGraceExpired:- Alice tab 收到
meeting.host.changed广播,auto_reason='host_grace_expired' - 工具栏/成员面板 Alice 头像变为主持人标识
meeting_rooms.host_id=55+meeting_participants.role原子变更
- Alice tab 收到
- 处理锁验证(P1-7):
# 宽限期到期瞬间抓一下处理锁 redis-cli GET "echo:meeting:host_grace_handling:$ROOM_CODE" # 期望:`1`(SETNX 成功 60s TTL),60s 后自然过期 - 截图 →
06-host-auto-transferred.png
步骤 8:清理
Alice(现任主持人)结束会议,走场景 01 的资源清理断言。
预期结果汇总
| 断言 | 预期 |
|---|---|
| 请他静音 | Bob localAudioEnabled=false + toast |
| 请他开麦 | Bob 弹窗确认后 localAudioEnabled=true |
| 踢出 | Carol 跳首页 + 成员列表移除 |
| 主动转让原子性 | host_id + 两行 role 同事务更新 |
| host 宽限期 Redis | key 写入 + TTL≈120s |
| 宽限内重连 | key DEL + 本地 timer cancel |
| 宽限到期自动转让 | 最早加入者成为新 host + 广播 host.changed |
| 处理锁 SETNX | host_grace_handling:* 存在 60s |
失败排查
| 现象 | 定位 |
|---|---|
转让后 host_id 和 role 不一致 |
P1-1 回归:检查 TransferHost 是否包在单事务 |
| 宽限到期但 host 不变 | 看 handling 锁是否被"自然过期 + DEL=0"误拦(P1-7 应已修) |
| 多节点部署下双重转让 | SETNX 处理锁应保证单节点唯一处理 |