test(chat): restore two-layer markdown contract counts
Independent Staging Quality Gate / validate (push) Successful in 9m19s
Independent Staging Quality Gate / publish (push) Successful in 11m32s

Spoken and report layers each keep a message-markdown shell and both must call renderProse; login hard-jumps drop from 4 to 3 after the onboarding suggestion effect went away. No product code.
This commit is contained in:
jesse-ux
2026-09-15 13:27:17 +08:00
parent ca5ecafc1a
commit ff5023a282
6 changed files with 86 additions and 4 deletions
@@ -76,3 +76,9 @@
## 未做
`TASK-mobile-touch-and-breakpoints-20260915.md`(BUG-695~697)按任务书必须排在本轮合入 staging 之后,本提交不含。
## 验收补正(2026-09-15
Linux 门禁实测(相对本机 Windows 环境缺口):`tsc --noEmit` 0 错;`npm run lint` 0 error / 120 warning;测试声明 基线 `ebd5ba68` 3214 → 改后 3232`npm run build` exit 0`/``○ Static`;首屏 JS gzip-9 585,246 B → 583,660 B(−0.27%)。全量失败 27 → **29**
多出来的两条是既有契约没跟着改:`chat-bundle-splitting-contract``message-markdown` 包裹数仍锁 1,以及 `plainParagraphs(spoken)` 字面量;`chat-navigation-a11y-contract``window.location.assign("/login")` 仍锁 4。本机当时把全量 TAP 记成 Windows 缺口,所以没看到。修复见 `TASK-chat-reading-load-fix-20260915.md`
@@ -0,0 +1,46 @@
# PROGRESS · 验收修复单:两条契约测试(2026-09-15)
- 执行分支:`codex/chat-reading-load-fix-20260915`
- 工作树:`.worktrees/chat-reading-load-fix-20260915`
- 基线:`origin/staging` @ `ca5ecafc`(任务书写 `6c748d86`;其后两笔是文档)
## 做了什么
只改 `frontend/tests/` 与文档。未碰 `frontend/src/`。未进 BUG_HISTORY。未改 CHANGELOG。
任务 1 · `chat-bundle-splitting-contract.test.ts`
- `message-markdown` 包裹计数 1 → 2,三栏注释写在断言上方。
- `plainParagraphs(spoken) ?? []` 改为断言 `renderProse` 内部的 `(plainParagraphs(text) ?? []).map`
- **没有只改数字。** 补了「两层必须走同一条渲染路」:口语层与报告层都要出现 `renderProse(spoken|report, renderMarkdown)`,精确计数为 2;禁止 `{renderMarkdown(spoken|report)}``>{spoken|report}</div>`。一层 markdown、一层裸文本会把计数从 2 打到 1。
任务 2 · `chat-navigation-a11y-contract.test.ts`
- `window.location.assign("/login")``homeSurface` 上 4 → 3,三栏注释写在断言上方。
- 同 test 其余断言未改:`redirectToLogin``persistLoginSessionReturn` + `replace("/login")``signOut``assign("/login")`、两条 `doesNotMatch` router.push/replace、billing hook 仍是 4。
## 测试数字
| | 说明 |
| --- | --- |
| `test(` 声明 | 未新增 test 函数,Linux 门禁口径仍是 3232 |
| `chat-bundle-splitting-contract` | 5 pass / 0 fail(全量 TAP 里 `pre-markdown fallback` 已绿) |
| `assign("/login")` 计数 | homeSurface 实测 3consultation-run / profile-onboarding / home-cloud-sync 各 1);`router.push/replace("/login")` 仍不存在 |
| `tsc --noEmit` | 0 错 |
| `npm run lint` | **0 error** / 120 warning(既有,未动) |
| 本机 Windows 全量 TAP | 3041 tests / 2874 pass / **153 fail** / 14 skip。与本机既有环境缺口同型(CRLF、`SKILL.md` EPERM、Docker/DB、`G:\\G:\\`)。`auth redirects…` 仍红是因为同 test 里未改的 `persistLoginSessionReturn();\n window.location.replace` 碰上工作区 CRLF,不是 4→3 没改对。 |
Linux 全量失败必须回到 **27**(基线 `ebd5ba68` 的 Docker / DB / 部署 / rsync 缺口,两条契约漂移去掉后)。本机 153 不能当作 27 的证据;以 Gitea Linux 门禁为准。
## 既有断言三栏
| 位置 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `message-markdown` 包裹数 | 1 | 2 | 口语层 + 完整分析各一个壳;仍精确计数 |
| `plainParagraphs(spoken) ?? []` | 口语层字面量 | `renderProse``(plainParagraphs(text) ?? []).map` | 两层共用 renderProse |
| 两层渲染路径 | (原 1 个包裹隐含「markdown 与裸段落同一条路」) | `renderProse(spoken\|report, renderMarkdown)` 恰好 2 次 | 防一层 markdown 一层裸文本 |
| `window.location.assign("/login")` | 4 | 3 | 建议问题入口删掉,少一次硬跳转,不是改成 router.push |
## 偏离
无。未走让步 1(两层路径的新断言已落地,不是只改数字)。
@@ -46,3 +46,9 @@ BUG-697:报告中心 720、阅读区 760 并入 767。目录 860 保留,就
- 任务 3.3430 并入 480、620 并入 640,未看到必须保留原值的理由。
- 任务 1 未走让步 3(热区高度仍是 44,不只加宽)。
- 浏览器 / 真机项见 `docs/testing/mobile-touch-and-breakpoints-20260915.md`,不得写成通过。
## 验收补正(2026-09-15
Linux 门禁实测:`tsc --noEmit` 0 错;`npm run lint` 0 error / 120 warning;测试声明 3232`npm run build` exit 0`/``○ Static`;首屏 gzip −0.27%。全量失败 29 条里,多出来的 2 条属于上一轮 `8144fca2` 的契约漂移,不是本单回归;本单的断点 / 触控合同在 Linux 上绿。
`membership-page`「login redirects…」在 Linux 上通过。本机 Windows 记的那条 CRLF 失败不是回归,无需处理。
+1 -1
View File
@@ -119,7 +119,7 @@
| — | — | 初始化后点首页「家庭」报运行合同未完成:本命第一步未强制计算工具、主题卡无入口钉死 | 入口已下线,机制保留供历史会话 | `codex/consultation-family-contract-incomplete-20260909`BUG-630);主题卡发出点已删,`guided_topic` 枚举仍可读回;走查 `docs/testing/consultation-family-contract-incomplete-20260909.md` 不再可执行 |
| `TASK-chat-reading-load-20260915.md` | `PROGRESS-chat-reading-load-20260915.md` | 首页主题卡下线 + 回答两层可见性 + 折叠层宽表可横滑 + 输入框 16px。内容一字不删。⚠️ BUG-630 走查路径失效 | 已验收(2 条契约测试未通过,见修复单) | `8144fca2` |
| `TASK-mobile-touch-and-breakpoints-20260915.md` | `PROGRESS-mobile-touch-and-breakpoints-20260915.md` | 消息操作按钮触屏命中区 27×34、相邻仅隔 1px(BUG-695);CSS 平板上限 900px 与 `sidebarViewportForWidth` 的 1024 不一致,9011023 是混合态(BUG-696);报告域 720/760/860 三个断点互不对齐,761–860 目录已塌、正文还是桌面(BUG-697)。含断点白名单契约测试。**串行在 chat-reading-load 之后** | 已验收 | `6c748d86`BUG-695697 |
| `TASK-chat-reading-load-fix-20260915.md` | | **验收修复单**`chat-bundle-splitting-contract``message-markdown` 包裹数 1→2、`plainParagraphs(spoken)` 字面量已不存在)与 `chat-navigation-a11y-contract``window.location.assign("/login")` 4→3,建议问题入口删除带走一处)两条既有断言被本轮打红且未更新;Linux 全量 27→29。只改测试不改产品代码 | 待领取 | `codex/chat-reading-load-fix-20260915` |
| `TASK-chat-reading-load-fix-20260915.md` | `PROGRESS-chat-reading-load-fix-20260915.md` | **验收修复单**`chat-bundle-splitting-contract``message-markdown` 包裹数 1→2、`plainParagraphs(spoken)` 字面量已不存在)与 `chat-navigation-a11y-contract``window.location.assign("/login")` 4→3,建议问题入口删除带走一处)两条既有断言被本轮打红且未更新;Linux 全量 27→29。只改测试不改产品代码 | 待验收 | `codex/chat-reading-load-fix-20260915` |
| `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal``dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` |
### 个人报告
@@ -44,11 +44,30 @@ test("the pre-markdown fallback renders prose but never raw markdown", () => {
assert.deepEqual(plainParagraphs("只有一段。"), ["只有一段。"]);
// 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);
// 原值: contentSource.match(/className="message-markdown"/g)?.length === 1
// 新值: 2
// 原因: 回答改为两层可见性(口语层 + 折叠的完整分析),两层各有一个包裹。
// 这条仍然数包裹个数,不得放宽成 >= 1——它防的是"升级到 markdown 时换了壳"。
assert.equal(contentSource.match(/className="message-markdown"/g)?.length, 2);
assert.match(contentSource, /className="message-answer"/);
assert.match(contentSource, /TechniqueAuditDisclosure/);
assert.match(contentSource, /<p key=\{index\}>\{paragraph\}<\/p>/);
assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/);
// 原值: assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/)
// 新值: 断言 renderProse 内部的回退路径
// 原因: 口语层与报告层共用 renderProseplainParagraphs 只在它内部调用一次。
assert.match(contentSource, /\(plainParagraphs\(text\) \?\? \[\]\)\.map/);
// 两层必须都走 renderProse,不得一层 markdown 一层裸文本。
// 口语层若改成 {spoken} / renderMarkdown(spoken)、报告层仍走 renderProse(或反过来),
// 下面的精确计数会从 2 掉到 1。不得改成 >= 1。
assert.match(contentSource, /renderProse\(spoken,\s*renderMarkdown\)/);
assert.match(contentSource, /renderProse\(report,\s*renderMarkdown\)/);
assert.equal(
contentSource.match(/renderProse\((?:spoken|report),\s*renderMarkdown\)/g)?.length,
2,
);
assert.doesNotMatch(contentSource, /\{renderMarkdown\((?:spoken|report)\)\}/);
assert.doesNotMatch(contentSource, />\{(?:spoken|report)\}<\/div>/);
});
test("gsap loads on demand while keeping the reduced-motion gate", () => {
@@ -65,7 +65,12 @@ test("auth redirects stay hard document loads so stale session state cannot surv
// without adding a `next` query to the login page.
assert.match(pageSource, /function redirectToLogin\(\): never \{/);
assert.match(pageSource, /persistLoginSessionReturn\(\);\n window\.location\.replace\("\/login"\);/);
assert.equal(pageSource.match(/window\.location\.assign\("\/login"\)/g)?.length, 4);
// 原值: 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);
assert.equal(pageSource.match(/window\.location\.replace\("\/login"\)/g)?.length, 1);
assert.doesNotMatch(pageSource, /router\.push\("\/login"\)/);
assert.doesNotMatch(pageSource, /router\.replace\("\/login"\)/);