# TASK · 验收修复单:两条既有契约测试被本轮改动打红且未更新 - 日期:2026-09-15 - 基线 commit:`origin/staging` @ `6c748d86` - 执行分支:`codex/chat-reading-load-fix-20260915` - 来源:Claude 对 `8144fca2`(chat-reading-load)与 `6c748d86`(mobile-touch-and-breakpoints)的验收 - 规模:两条断言 + 两段三栏说明。**不改产品代码。** --- ## 1. 验收结论摘要 两单绝大部分通过。门禁在 Linux 上实测: | 项 | 结果 | | --- | --- | | `tsc --noEmit` | 0 错 | | `npm run lint` | **0 error** / 120 warning(全部既有) | | 测试总数 | 基线 `ebd5ba68` 3214 → 改后 3232(**+18**) | | `npm run build`(Turbopack) | exit 0;`/` 为 `○ Static` | | 首屏 JS gzip-9 | 585,246 B → 583,660 B,**−1,586 B / −0.27%**(口径同 09-02 / 09-06:预渲染 `index.html` 引用脚本去重求和) | | 全量测试失败 | 基线 27 → 改后 **29** | **两条新增失败就是本单要修的东西。** 基线 27 条失败与改后逐条一致(Docker / DB / 部署 / rsync 环境缺口),没有一条消失也没有一条变形。 执行方两份 PROGRESS 都把全量测试与 `next build` 记成了 Windows 环境缺口(`SKILL.md` symlink `EPERM`、CRLF),这个判断对他们的机器成立,但也因此**没有机会看到这两条**。本单把它们补上。 顺带澄清:`PROGRESS-mobile-touch-and-breakpoints` 里记的 `membership-page`「login redirects…」CRLF 失败,**在 Linux 上通过**,不是回归,无需处理。 --- ## 2. 事故实证 ### 2.1 `the pre-markdown fallback renders prose but never raw markdown` - 文件:`frontend/tests/chat-bundle-splitting-contract.test.ts` - 断点:`TestContext. (…:47:10)` - 报错:`Expected values to be strictly equal: 2 !== 1` 失败的那一句: ```js // And: one stable wrapper serves both paths, so no styling or caret is lost on upgrade. assert.equal(contentSource.match(/className="message-markdown"/g)?.length, 1); ``` `8144fca2` 把回答拆成口语层与折叠层,两层各有一个 `
`(`chat-message-content.tsx` 里现在实测 2 处)。 **同一个 test 里还有第二条会接着红**(node:test 首个断言失败即中止,所以 TAP 只报了上面那条): ```js assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/); ``` 口语层改走 `renderProse(spoken, renderMarkdown)`,`plainParagraphs` 现在只出现在 `renderProse` 内部的 `(plainParagraphs(text) ?? []).map(…)`,字面量 `plainParagraphs(spoken) ?? []` 已不存在。 同一个 test 里的 `assert.match(contentSource, /

\{paragraph\}<\/p>/)` **仍然成立**(`renderProse` 里还是这一行),不要动。 ### 2.2 `auth redirects stay hard document loads so stale session state cannot survive` - 文件:`frontend/tests/chat-navigation-a11y-contract.test.ts` - 断点:`TestContext. (…:68:10)` - 报错:`Expected values to be strictly equal: 3 !== 4` 失败的那一句: ```js assert.equal(pageSource.match(/window\.location\.assign\("\/login"\)/g)?.length, 4); ``` `8144fca2` 删掉了拉建议问题的那个 `useEffect`,其中包含: ```js if (caught instanceof OnboardingAuthenticationError) { window.location.assign("/login"); return; } ``` 调用点从 4 处变成 3 处。这条断言数的是"硬跳转的次数",入口删了所以少一次——**是正确的减少,不是把跳转换成了 `router.push`**。同一个 test 里的 `doesNotMatch(/router\.push\("\/login"\)/)` 与 `doesNotMatch(/router\.replace\("\/login"\)/)` 仍然成立,红线本身没破。 ### 2.3 性质 两条都是**契约测试跟着代码漂了但没更新**,不是产品回归。但 `frontend/AGENTS.md` §7 两条红线各破一次: - §7.1「相关测试套件 fail=0」——两套在基线是绿的。 - §7.3「改任何既有断言必须写『原值 / 新值 / 原因』三栏」——这两条既有断言被改动打红,既没更新也没说明。 --- ## 3. 决策记录 1. **改测试,不改产品代码。** 两处的新行为都是任务书明确要求的结果:两层可见性必然有两个 `message-markdown` 包裹;删除建议问题入口必然少一处硬跳转。回退产品代码去迁合测试是反的(`CLAUDE.md`:不要为迎合工具改业务代码)。 2. 两条断言都**保持"数数"的形式**,只改数字并补三栏说明。不要改成模糊匹配(`>= 1`、`assert.ok`)——它们数的就是"有几个包裹 / 有几次硬跳转",放宽等于把防线拆了。 --- ## 4. 硬红线 1. **不得修改 `frontend/src/` 下任何文件。** 本单只动 `frontend/tests/` 与文档。 2. `tsc --noEmit` 0 错;`npm run lint` 0 error。 3. 改完全量 `npm test` 的失败数必须回到 **27**,且失败清单与 `ebd5ba68` 基线逐条一致。 4. 不得放宽断言形式(见 §3.2)。 5. 不得顺手修 120 条既有 warning。 --- ## 5. 任务分解 ### 任务 1 · 修 `chat-bundle-splitting-contract.test.ts` 把那两条断言更新为当前架构,并在**紧邻的注释里**写三栏: ```js // 原值: contentSource.match(/className="message-markdown"/g)?.length === 1 // 新值: 2 // 原因: 回答改为两层可见性(口语层 + 折叠的完整分析),两层各有一个包裹。 // 这条仍然数包裹个数,不得放宽成 >= 1——它防的是"升级到 markdown 时换了壳"。 assert.equal(contentSource.match(/className="message-markdown"/g)?.length, 2); // 原值: assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/) // 新值: 断言 renderProse 内部的回退路径 // 原因: 口语层与报告层共用 renderProse,plainParagraphs 只在它内部调用一次。 assert.match(contentSource, /\(plainParagraphs\(text\) \?\? \[\]\)\.map/); ``` **补一条新断言**,把这条 test 原本守的东西守回来——"两个包裹用的是同一套渲染路径": ```js // 两层必须都走 renderProse,不得一层 markdown 一层裸文本。 assert.equal(contentSource.match(/renderProse\(/g)?.length >= 2, true); ``` 具体写法执行方可调,但**必须让这条 test 重新具备"检测到两层渲染路径分叉"的能力**,不能只是把 1 改成 2 就完事。 **验收标准**:`npx tsx --test tests/chat-bundle-splitting-contract.test.ts` fail=0;三栏注释齐全;断言仍是精确计数。 ### 任务 2 · 修 `chat-navigation-a11y-contract.test.ts` ```js // 原值: pageSource.match(/window\.location\.assign\("\/login"\)/g)?.length === 4 // 新值: 3 // 原因: 8144fca2 删除建议问题请求,连同它的 OnboardingAuthenticationError 分支里 // 那一次 window.location.assign("/login")。少的是调用点,不是把硬跳转换成了 // router.push——同 test 里两条 doesNotMatch 仍然成立。 assert.equal(pageSource.match(/window\.location\.assign\("\/login"\)/g)?.length, 3); ``` 顺手核对同一个 test 里其余断言在当前代码下仍然成立(`redirectToLogin`、`persistLoginSessionReturn`、`signOut` 三段),**成立就别动**。 **验收标准**:`npx tsx --test tests/chat-navigation-a11y-contract.test.ts` fail=0。 ### 任务 3 · 全量复核与文档 **3.1** 跑全量并逐条比对: ```bash npm test > /tmp/head.tap 2>&1 grep -oE "^ *not ok [0-9]+ - .*" /tmp/head.tap | sed 's/^ *not ok [0-9]* - //' | sort ``` 必须得到 **27** 条,且与 `ebd5ba68` 的 27 条一字不差。数字与清单写进 PROGRESS。 **3.2** 文档: - 两份既有 PROGRESS(`PROGRESS-chat-reading-load-20260915.md`、`PROGRESS-mobile-touch-and-breakpoints-20260915.md`)各补一段「验收补正」,写明本单修了哪两条、为什么原来的 Windows 环境缺口判断让它们漏网,并把 Linux 实测的门禁数字抄进去(tsc 0 / lint 0 error / 3232 tests / build exit 0 / `/` Static / gzip −0.27%)。 - `docs/tasks/PROGRESS-chat-reading-load-fix-20260915.md`:本单自己的进度。 - **不新增 BUG 编号**:两条都是测试契约漂移,没有用户可感知现象,不进 `docs/BUG_HISTORY.md`。若执行中发现真实回归,从 **BUG-699** 起(BUG-698 已由 `TASK-settings-dialog-size-and-nav-20260915.md` 占用)。 - 不动 `CHANGELOG.md`(无用户可见行为变化)。 --- ## 6. 让步顺序 1. 任务 1 的"新断言"若想不出既精确又稳的写法,**先把两条数字断言改对并写满三栏**,把新断言的缺口写进 PROGRESS 交回。数字改对是底线,加强是加分。 2. **绝不让步**:不得改 `frontend/src/`;不得把精确计数放宽成模糊匹配;全量失败数必须回到 27。 --- ## 7. 开工前置命令 ```bash cd /workspace/Jyotisha git status -sb | head -1 git fetch origin --prune git worktree add -b codex/chat-reading-load-fix-20260915 \ .worktrees/chat-reading-load-fix-20260915 origin/staging cd .worktrees/chat-reading-load-fix-20260915/frontend npm ci npx tsx --test tests/chat-bundle-splitting-contract.test.ts tests/chat-navigation-a11y-contract.test.ts # 应先看到两条红 ``` 交付:`git push origin HEAD:staging`,推完核对远端 SHA。 --- ## 8. 未纳入本单的欠账 以下在验收中确认仍然欠着,**不属于本单范围**,列在这里免得被当成已完成: | 欠账 | 归属 | | --- | --- | | 浏览器 / iPhone 真机走查(折叠块、宽表横滑、输入框 16px、44px 触控、断点目视) | `docs/testing/chat-reading-load-20260915.md`、`docs/testing/mobile-touch-and-breakpoints-20260915.md` | | iOS 键盘遮挡输入框 | `docs/testing/ios-keyboard-composer-20260915.md`,结论出来前不立单 | | staging 部署核对 | 线上仍是 `8d56d0ab`,两单尚未部署;部署后 `/api/health` 的 `deployment.gitCommit` 应等于含门禁路径改动的最新 staging 提交 | | 设置弹窗尺寸与分区菜单强调条 | `TASK-settings-dialog-size-and-nav-20260915.md`(BUG-698),待领取 | | BUG-630 的另一半(本命第一步未强制计算工具) | 与主题卡无关,仍未处理 |