主题
发版前冒烟 Runbook(rb-debug MCP 驱动)
创建:2026-07-22 · roadmap 5-5(
docs/plans/product-iteration-roadmap-2026h2.md§五) 性质:发版前对运行中的 dev app 跑一遍的半自动冒烟清单。不是 CI 门(CI 只 --lib 单测 + build); 也不是全自动 E2E(tauri-driver不支持 macOS WKWebView,见 roadmap 5-5)。执行环境:一个「Claude + rb-debug MCP」会话 + 运行中的 dev app(
pnpm tauri dev)。 断言纪律:用客观只读探针(rb_db_query/rb_state/rb_supabase_query/rb_logs)验证状态, 不靠截图肉眼(rb_snapshot最贵,只在纯视觉观感存疑时用;见 memoryfeedback_mcp_token_economy)。 驱动步骤分两类:🤖=MCP 可驱动(rb_click/rb_type/rb_scroll);🙋=需人工动作(如双击查词、提供账号)。
前置
- 启动 dev app:终端
pnpm tauri dev(首次编译数分钟)。 - 准备一个测试 Supabase 账号(sync 流程需要;勿用真实用户数据)。
- 准备一个本地 .epub 测试文件路径。
- 确认 rb-debug MCP 连通:
rb_state store=tabs返回而非fetch failed。
本 runbook 尚未在 dev app 上实跑验证(2026-07-22 写就时 app 未运行)。首次执行时若某条 断言/选择器与实际不符,就地修正本文件(它是活文档)。
Flow 0 — 启动 + 首页渲染 + 登录门
| 🤖 驱动 | rb_state store=tabs |
| ✅ 断言 | 返回成功;tabs 含 id:'home'(首位,type='web' url='')+ id:'review'(type='review-source',webviewLabel='content-review');activeTabId 存在 |
| 🤖 驱动 | rb_state store=auth |
| ✅ 断言 | 未登录时 SPA 门在登录页(session 为空);已登录则 session 非空 |
| 🤖 兜底 | 首页结构存疑时 rb_dom selector="[data-testid=home], main"(不要上来就 rb_snapshot) |
Flow 1 — 登录 → 自动同步启动
| 🙋/🤖 驱动 | rb_type 填邮箱/密码输入框 + rb_click 登录按钮(选择器以 AuthPanel 实际为准) |
| ✅ 断言 | rb_state store=auth → session/access_token 非空、auth_user_id 有值 |
| ✅ 断言 | rb_state store=auth → 同步生命周期启动(轮询状态字段,见 useAuthStore) |
| ⚠️ 隐私 | 用测试账号;截图会带账号信息,本流程避免 rb_snapshot |
Flow 2 — 开网页 → 双击查词 → 保存生词 → 库可见(核心闭环)
| 🤖 驱动 | rb_type 地址栏输入一个英文网页 URL + 回车/rb_click 导航 |
| ✅ 断言 | rb_state store=tabs → 新增 web tab、activeTab 指向它;rb_logs tail=20 无红错 |
| 🙋 驱动 | 在 content webview 里双击一个生词(content-script 的 lookup 挂 dblclick;rb_click 只合成单击,双击需人工) |
| ✅ 断言 | 查词弹窗出现:rb_dom selector=".rb-word-popup, [data-rb-popup]"(选择器以 WordPopup 实际为准) |
| 🙋 驱动 | 点弹窗「加入生词本」(含语境来源的入口;见 memory project_notebook_requires_context) |
| ✅ 断言 | rb_db_query:SELECT word, added_at FROM learning_entries WHERE deleted_at IS NULL ORDER BY added_at DESC LIMIT 3 → 目标词出现 |
| ✅ 断言 | 语境落库:SELECT word, sentence FROM word_cloze_contexts WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 3 → 该词的真实句入池 |
| ✅ 断言 | 来源关联:SELECT wpl.word FROM word_page_links wpl WHERE wpl.deleted_at IS NULL ORDER BY rowid DESC LIMIT 3 + reading_pages 有对应源 |
| 🤖 驱动 | 打开 Library/Vocab,rb_dom 确认该词行渲染(WordRow) |
Flow 3 — 复习一张卡 → SM-2 状态推进
| 🤖 前置 | 先取基线:rb_db_query:SELECT word, repetitions, next_review_date, mastery_level, last_review_quality FROM learning_entries WHERE deleted_at IS NULL AND next_review_date <= datetime('now') ORDER BY next_review_date ASC LIMIT 1(实跑确认此查询有效,未复习词 next_review_date=epoch 即到期) |
| 🤖 驱动 · 进复习模块 | ✅ 2026-08-07 实测点通:rb_click selector="nav[aria-label] button:nth-of-type(3)" webview=main → 断言 rb_state store=workspace 得 activeModule: "review"。位置选择器,不吃 locale(rail 顺序:1 发现 / 2 阅读 / 3 回顾 / 4 词汇与笔记 / 5 学习统计)。原「title/aria 中英混用不可靠」的障碍由此绕开——不要再用文案选。 |
| 🙋/🤖 驱动 · 开始 session + 揭晓 | picker 选组 → 点卡面揭晓。未固化(本轮无到期卡,没能实跑)。 |
| 🤖 驱动 · 评分 | 从源码推导、待首跑验证:Hard/Easy 的文案是硬编码英文(ReviewSession.tsx:296/302 的 <div class="text-sm">Hard/Easy</div>),不吃 locale;两个按钮是评分条里仅有的 flex-1 + font-semibold 按钮 → button[class*="flex-1"][class*="font-semibold"],第 1 个 = Hard,第 2 个 = Easy。⚠️ 揭晓前它们 pointerEvents:none + aria-hidden,必须先揭晓再点。⚠️ 会推进真实卡片排期(低危、正常使用) |
| ✅ 断言 | 同一 word 复查:repetitions 变化符合 SM-2(Easy 首次 reps→1;已推进则 +1);next_review_date 前移;last_review_quality 更新 |
| ✅ 断言 | 与黄金向量一致性:结果应落在 docs/cross-end/sm2-golden-vectors.json 覆盖的行为域内(单测已锁纯函数,此处验端到端落库) |
| ✅ 断言 | rb_state store=review → 卡片状态机推进(currentCard/reviewedCount) |
Flow 4 — 本地 EPUB 打开 → 章节导航 → 划线持久化
⚠️ 2026-08-07 重写:原 Flow 4 叫「位置恢复」,断言「恢复到上次阅读位置(TOC/滚动位置)」。 roadmap 3-5 验收证明产品根本没有这个行为(书/章/章内偏移三层全不恢复,
addEpubTab永远落chapters[0];见epub-longform-acceptance-handoff.mdE7 + backlog B4)。 一条永远不可能通过的断言会让 smoke test 要么长期挂红、要么被习惯性忽略——两种都比没有更糟。 故本 flow 改为验「打开 → 章节导航 → 划线跨会话持久」这条真实存在的闭环; 位置恢复在 backlog B4 修复后再把断言加回来。
| 🙋 驱动 | 地址栏 📂 → 选本地 .epub。只能人工:这是 macOS 原生文件面板,rb_click 只触发页面 JS 事件、碰不到原生窗口;地址栏也不接受本地路径(useNavigation.ts:78 一律按 URL 处理) |
| ✅ 断言 | rb_state store=tabs → epub tab(type='epub',epubInfo.toc 非空,url 形如 rb-cache://localhost/epub/<hash>/…) |
| ✅ 断言 | rb_db_query:SELECT source_ref, name, source_type, last_opened_at FROM reading_pages WHERE deleted_at IS NULL ORDER BY last_opened_at DESC LIMIT 3 → 该书 source_type='book'。注:路径列名是 source_ref 不是 uri;标题是 name(2026-07-22 校正);source_ref 对 EPUB 带 #锚点(2026-08-07 补) |
| ⚠️ 已知 | 打开后落封面且目录不自动开(backlog B1);reading_pages 的行只在存词/划线时才产生,光打开不建行(MainApp.tsx:278 让 epub 跳过 logResourceOpen) |
| 🤖 驱动 · 开目录 + 跳章 | ✅ 2026-08-07 实测点通:TOC 条目是 [class*="space-y-0"] > *:nth-child(N)(主 webview,div[role=button]),N 从 1 起。先用 rb_dom 同选择器读 span 文本确认落点,再 rb_click |
| ✅ 断言 | 内容 webview 到位:rb_dom selector="#<该章锚点 id>" webview=content-<tabId> → found: true |
| 🙋 驱动 · 划线 | 选中一句 → 划高亮。未固化(选区 + 浮动工具栏,rb_click 不产生真实 Selection) |
| ✅ 断言 | rb_db_query:SELECT annotation_type, user_id, substr(source_text,1,40), created_at FROM page_annotations ORDER BY created_at DESC LIMIT 3 → 有 highlight 行、user_id = 当前登录用户 |
| 🙋 驱动 · 跨会话 | 关掉该 tab → 从「最近打开」重开(不要点「继续阅读」——那条对 EPUB 是断的,backlog B3) |
| ✅ 断言 | 重新跳到同一章后 rb_dom selector=".rb-annotation" webview=content-<新 tabId> → total_matches 与划线数一致(✅ 2026-08-07 实测得 2)。rb_logs 无恢复报错 |
| 💡 多标签对照法 | 需要 A/B 时可开两个 epub tab 各自导航,再分别数 .rb-annotation——切标签用 rb_click selector="[title*=\"<书名片段>\"]" webview=main(✅ 实测可用;同名多标签时用 :first-of-type 取靠前那个) |
Flow 5 — sync 双向(本地 ↔ Supabase 真相对账)
| 🤖 驱动 | 触发一次同步(登录后自动轮询,或手动入口 rb_click) |
| ✅ 断言 push(首选) | rb_state store=auth → syncStatus.pending_push == 0 且 syncStatus.last_sync_at 晚于 Flow 2 存词时刻 → push 已完成 |
| ✅ 断言 | 本地 synced_at 卫生:rb_db_query:SELECT word, (synced_at IS NOT NULL) synced, (synced_at >= updated_at) clean FROM learning_entries WHERE deleted_at IS NULL ORDER BY added_at DESC LIMIT 5 → 全部 synced=1 clean=1 |
| ✅ 断言 | rb_logs tail=30 无 sync 错误(无 23503 FK / 无 watermark 异常) |
| ⚠️ 环境限制 | rb_supabase_query(Supabase 真相直查)在 CN 网络稳定失败("error sending request")——MCP server 直连 supabase.co 不走系统代理(2026-07-22 实跑确认)。故 push 成功改用 app 自身 sync 状态断言(上面首选项);app 走自己的网络栈能同步。跨设备 pull 验证需第二端 |
通过标准
- Flow 0/2/3/4 必过(核心闭环 + 阅读器 + 复习算法端到端)。
- Flow 1/5 有测试账号时必过;无账号时记为 skipped 并在发版说明标注「sync 未冒烟」。
- 任一
rb_logs出现未预期红错 = 不通过,定位后再发版。
实跑校准记录
2026-07-22(首次实跑,账号 <测试账号 A>):只读断言层全部对真实数据校准通过;发现并修正:
- Flow 4:
reading_pages列名uri→source_ref(URL/路径)、标题列name(原 runbook 写错,已修)。真实数据含一本本地 EPUB(source_type='book')last_opened_at 有值 → EPUB 追踪闭环验证 ✓。 - Flow 5:
rb_supabase_query在 CN 网络稳定失败(MCP server 不走代理)→ 改用 app sync 状态断言(pending_push=0 + synced_at≥updated_at),已验证 ✓。 - Flow 0:review tab webviewLabel =
content-review(已补)。 - Flow 2:learning_entries / word_cloze_contexts 列名与查询全部正确,真实存词(unlock/spread/fungus)+ 语境池 + gloss 均落库 ✓。
- Flow 3:due 卡基线查询有效 ✓;复习 UI 驱动选择器未固化(图标按钮,title 中英混用),下次实跑定位回填。
- 未实跑的写驱动步骤(存词双击、复习评分):因改动真实数据 + 选择器待定,本次只校准断言层;写路径已由单测(roadmap 5-1)+ 黄金向量三端(5-2)锁死。
2026-08-07(roadmap 3-5 EPUB 验收同轮顺手固化,账号 <测试账号 B> / <test-user-id-A>):
已实测点通的选择器(全部位置/结构型,不吃 locale——原「title 中英混用不可靠」的障碍由此绕开):
- 进复习模块:
nav[aria-label] button:nth-of-type(3)→rb_state store=workspace得activeModule:"review"✓ (rail 顺序 1 发现 / 2 阅读 / 3 回顾 / 4 词汇与笔记 / 5 学习统计) - TOC 条目:
[class*="space-y-0"] > *:nth-child(N)(主 webview 的div[role=button])✓ - 切标签页:
[title*="<书名片段>"](同名多标签用:first-of-type)✓ - 划线计数:内容 webview 里的
.rb-annotation(annotations.js:14 MARK_CLASS;持久化与临时标记同类名, 临时的多一个rb-annotation-transient)✓ /debug/click(=rb_click)对主 webview 的 React onClick 有效(合成 click 事件会被 React 委托监听接住)✓
仍需人工的写驱动(不是选择器问题,是能力边界,别再试):
- 打开本地 .epub → macOS 原生文件面板,合成事件碰不到;地址栏不接受本地路径
- 双击存词 / 选区划线 → 需要真实 Selection 与 dblclick,
/debug/click只发单次 click
Flow 4 已按 roadmap 3-5 结论重写:原「位置恢复」断言断言了一个产品不存在的行为 (三层全不恢复,见 epub-longform-acceptance-handoff.md E7),改为验「打开 → 章节导航 → 划线跨会话持久」; 位置恢复待 backlog B4 修复后再加回。
Flow 3 评分按钮:源码推导出 button[class*="flex-1"][class*="font-semibold"](文案硬编码英文 Hard/Easy, 不吃 locale),本轮无到期卡未能实跑,首跑时验证并把此行改为 ✓。
与其他质量门的关系
| 层 | 覆盖 | 何时 |
|---|---|---|
| CI(ci.yml) | 编译 + 纯函数/算法单测 + i18n 门 | 每次 push/PR 自动 |
| 本 runbook | 真机端到端闭环(UI+webview+DB+sync) | 发版前手动一遍 |
/release skill | 打包 + cfg-gate + tag 双推 | 发版编排 |
建议:
/release流程 Step 1 前先跑本 runbook。未来 flows 稳定后可把纯 DB 断言部分抽成脚本进一步自动化。