Files
novalon-website/docs/acceptance/2026-09-21-gates/final-tree-results.md
T
zhangxiang a0328a623f chore(qa): 验收台账与证据入库 + 构建/部署配置同步
- docs/acceptance/qa-tracker.md:跨周期缺陷单一真源台账(§7=第五轮)。
- 周期 1/2 + iPhone SE/axe 验收证据目录、ACCEPTANCE_REVIEW 快照入库。
- 同步 README/CONTEXT/CLAUDE/DESIGN/testing/deployment/lessons-learned 口径;
  next.config/Dockerfile/nginx/Jenkinsfile/docker-compose/sentry/prisma 对齐
  standalone 产物装配与部署形态。
2026-09-28 10:48:09 +08:00

113 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 验收 §8 放行条件复跑结果(最终代码树)
生成时间:2026-09-22(UTC)。命令在**同一棵最终代码树**上串行执行,每步前后打印 :3000/:3100 端口占用数(0 才可用),
证据链编号:chain1(静态 + Lighthouse + GA4 生产复跑)→ chain2(作废,孤儿服务复用毒化)→ chain3(进程回收修复后复跑)
→ chain4/chain5/chain6(404 水印、`global-error` 兜底页、header 函数式 setState、移动端性能计时修复后,`EXIT_TEST=0` 归零轮)
→ chain7(Lighthouse 死断言处理 + axe 规则级通道)→ chain8(覆盖率棘轮 + iPhone SE 口径复跑)→ chain9(静态门禁与 Lighthouse 的第二次连续全绿)。
复跑脚本随证据存放于本目录:`chain7-dead-audit-and-rule-coverage.sh`、`chain8-coverage-and-iphone-se.sh`、`chain9-lighthouse-9urls.sh`。
## ② E2E 归零(`npm run test` = `test:functional && test:visual:all`)
| 轮次 | 结果 | 说明 |
|---|---|---|
| chain3 | 功能 `1048 passed / 4 failed / 28 skipped`(19.0m),端口预检 0 | 4 例均为**满负载竞态**(隔离复跑 `/tmp/repro-4.log` 33 例全过):p2 hover 下拉链接点击、p3 `isVisible→boundingBox` 两段式解析、uj-11 `scrollIntoViewIfNeeded` 撞 detach、p4 把 `waitForTimeout(1500)` 计入加载耗时 |
| chain4 | 功能 `1049 passed / 3 failed / 28 skipped`(15.7m) | 上述 4 例全过;新暴露同类 3 例:移动端 LCP 2736ms(dev 现场编译计入计时)、`website-acceptance.spec.ts:121` 与 `mobile.spec.ts:101` 找不到 `[data-testid="mobile-navigation"]` |
| chain6 | 功能 `1052 passed / 0 failed / 28 skipped`(17.4m)→ 视觉 `115 passed / 0 failed`(3.7m);`EXIT_TEST=0 2026-09-22T04:20:43Z` | **已归零**。chain4 的 3 例(移动端 LCP、两例 `mobile-navigation`)全部通过;视觉阶段真正执行(`/tmp/e2e-final9.log` 末段逐条列出 `visual-webkit-desktop` 的 115 例,含 L1 13 路由 + L2 组件态 + L3 排版色彩 + 浅色/深色主题各一)。全程未使用 `--update-snapshots`,基线未被改写。`POST_TEST_LISTENERS=0` |
chain4 的移动端两例经**根因定位后按产品缺陷修复**:`src/components/layout/header.tsx:184` 原为 `setIsOpen(!isOpen)`,
读取渲染闭包里的 `isOpen`,与抽屉的 `AnimatePresence` 退出动画(0.2s + 0.25s)叠加时高负载下丢点击 ⇒ 用户连点汉堡按钮可能打不开抽屉。
改为函数式 `setIsOpen((prev) => !prev)`。同文件同类的另外两处(`admin/login/page.tsx:80`、`admin/notifications/page.tsx:167`)
未在失败用例链路上,记为残留项交范围裁定。视觉基线未被改写(全程未使用 `--update-snapshots`)。
## ④ 静态门禁(chain6 最终树)
| 门禁 | 命令 | 结果 |
|---|---|---|
| 类型 | `npm run type-check` | chain6 `EXIT_TYPECHECK=0`(2026-09-22T03:51:14Z,2s)。**2s 触发怀疑,已用正/负对照自证不是空跑**:临时写入 `src/__typecheck-probe.ts`(`const probe: number = "not a number"`)⇒ `tsc` 报 `error TS2322`(11.1s,冷缓存);删除后复跑干净(2.9s,`tsconfig.compilerOptions.incremental=true` 命中 `.tsbuildinfo`)。探针文件已删除,未进入代码树 |
| 单元 | `npm run test:unit` | chain6 `EXIT_UNIT=0`:**137 suites / 1659 tests 全通过**(`Time: 6.936 s`,含新增 `src/app/global-error.test.tsx` 3 例) |
| Lint | `npm run lint` | chain6 `EXIT_LINT=0`:**0 error / 106 warning**(chain4 同为 106)。⚠️ [AGENTS.md](../../../AGENTS.md) §5 记的「实测 105 条」已过期,需同步;warning 是否清零由范围裁定,见 CONTEXT.md 决策表。其中 1 条是 `Unused eslint-disable directive`(死抑制,非真问题但可清) |
| 品牌文本通道 | `npm run check:brand-token` | chain6 `EXIT=0`,双通道契约未违反 |
| 对比度(令牌配对) | `npm run check:contrast` | chain6 **合计 44 组:✅ 44 / ❌ 0 / ⚠️ 0** → `contrast-and-headings.txt`(chain3/4/6 各一段) |
| 标题层级 | `npm run check:headings` | chain6 逐页 **0 问题**,`EXIT=0` |
| 构建 | `npm run build` | chain6 `EXIT_BUILD=0`(8s 增量,路由表与 24 个 html 产物 mtime 新于 03:50 已核对,非空跑) |
| Lighthouse | `npm run lighthouse` | chain6 `EXIT=0`(21 运行,但含 3 条死断言)→ **chain7 `EXIT_LIGHTHOUSE=0`(04:50:08Z)与 chain9 `EXIT_LIGHTHOUSE=0`(05:10:58Z)连续两次:9 URL × 3 次 = 27 运行、0 error、0 条 `not a known audit`**。URL 表已补 `/products/erp-upgrade`、`/about/brand`(§7 缺口的 Lighthouse 半边),两条新路由四类目均满分(perf 100 / a11y 1.0 / bp 1.0 / seo 1.0,LCP 786ms 与 794ms) |
| axe 节点计数(⑤) | 见下节 | `passed=true` |
**chain9 二次确认(05:03–05:11Z)**:处理完死断言、改过 `config/test/lighthouserc.json` 与文档/探针之后,把 `type-check`、`lint`、`test:unit`、`check:contrast`、`check:headings`、`check:brand-token` 全量重跑,**6 项 `EXIT=0`**(lint 仍 0 error / 106 warning),前后 `LISTENERS=0`。chain9 同时是 Lighthouse 在 9 URL 配置下的第二次连续全绿。
### Lighthouse 的 3 条死断言(chain6 新发现,已在最终树处理)
`config/test/lighthouserc.json:95-97` 的 `autocomplete-valid` / `presentation-role-conflict` / `svg-img-alt` 三条 `warn` 断言,
每轮都稳定打印 `"…" is not a known audit. expected: >=1 found: 0`(chain4+chain6 合计 42 条 = 3 条 × 7 URL × 2 轮)。
两个独立信源确认它们是**恒不成立的死断言**,而非「页面恰好没问题」:
1. **运行时报告**:最新 JSON 报告(`lighthouse-reports/contact-2026_09_22_04_25_34.report.json`,`lighthouseVersion 12.6.1`)的 `audits` 键集合里没有这三个 id,而同批断言的 `color-contrast`/`heading-order`/`target-size`/`aria-conditional-attr`/`skip-link` 均在列。
2. **审计注册表**:`@lhci/cli@0.15.1` 自带的是 `lighthouse@12.6.1`(不是顶层的 13.4.1 —— 这层版本裂差本身就是「为什么本地包里没有该审计」的答案),其 `core/audits/accessibility/` 共 64 个审计文件,无此三者。
覆盖率影响并非对等,因此不能简单删掉了事:用与门禁同款的 `runOnly: {type:'tag', values:['wcag2a','wcag2aa','wcag21a','wcag21aa']}`
做正/负对照探针(`docs/acceptance/2026-09-21-axe/probe-axe-rule-coverage.mjs`,每条规则各造一个必然违规的最小节点):
| 规则 | axe 4.11.4 是否在门禁 tag 内 | 探针是否抓到违规 |
|---|---|---|
| `autocomplete-valid` | 在 | ✅ 1 node |
| `svg-img-alt` | 在 | ✅ 1 node |
| `presentation-role-conflict` | **不在**(属 `best-practice` tag) | 规则未参与运行 |
⇒ 前两条的真实兜底是 ⑤ 的 axe 通道(已 34 路由 × 4 组合 = 0 违规);第三条在 axe 通道里也测不到,必须以规则级 `runOnly` 显式补上,
否则「删死断言」会变成「悄悄拆掉一道门」。
**chain7 复验(最终处理)**:`lighthouserc.json` 删掉这 3 条;`axe-contrast-evidence.mjs` 在 tag 通道之外增加第二次
`axe.run(document, {runOnly: {type:'rules', values: EXTRA_RULES}})`,并按页记录 `extraRulesChecked` 与 `extraRuleNodes`,
把 `extraRuleNodes === 0` 与 `extraRulePagesUnderCovered === 0`(即每页三条规则确实都跑了)一并写入 `thresholds` 与 `passed`。
结果:`EXIT_AXE_FULL=0`,`Σ extraRulesChecked = 408 = 34 路由 × 4 组合 × 3 规则`(分母闭合,不是"没测到所以 0")、
`extraRuleNodes = 0`;`EXIT_LIGHTHOUSE=0` 且 chain7 段 `not a known audit` 计数 **0**(chain4+chain6 历史段共 42 条,保留作对照)。
## ⑤ 全站双引擎双主题 axe 节点计数 = 0
**chain7 最终树(`docs/acceptance/2026-09-21-axe/axe-evidence.json`,`generatedAt 2026-09-22T04:43:02Z`,axe-core 4.11.4)**
——同一棵 src 代码树,`EXIT_AXE_FULL=0`;chain6(tag 通道单跑)快照存为 `axe-evidence-chain6-tagonly.json`,chain5 前一刻快照存为 `axe-evidence-chain5-preheader-fix.json`。
| 组合 | pages | contrastNodes | violationNodes | themeMismatch | bgMismatch | extraRuleNodes | extraRulePagesUnderCovered |
|---|---|---|---|---|---|---|---|
| chromium / light | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
| chromium / dark | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
| firefox / light | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
| firefox / dark | 34 | 0 | 0 | 0 | 0 | 0 | 0 |
`routeCount=34 rows=136(= 34 × 4 组合,分母断言)passed=true`;`Σ extraRulesChecked = 408 = 34 × 4 × 3`,
规则级通道覆盖 `autocomplete-valid` / `presentation-role-conflict` / `svg-img-alt`(后一条不在 wcag tag 集合内,只能靠它)。
**口径升级(这是 ⑤ 的关键,不是换实现)**:清单来源从「`/sitemap.xml` 的 29 条」扩为
**sitemap ∪ 预渲染产物(`dist/standalone/dist/server/app/**/*.html`)∪ 站内链接 BFS**(`crawl-sitemap.mjs`,打印 `skippedInternalDocs=1`)。
扩目录首轮(chain3)就**不是 0**:`/_not-found` 的 h1 用 `text-brand-ink` + `opacity-20` ⇒ 双引擎双主题各 1 个 `color-contrast` 节点;
`/_global-error` 是 Next 内置 500 文档(`<html id="__next_error__">` 无 `lang`、深色丢本站令牌)。
两处修复见 `docs/lessons-learned.md §5.24` 与 `CONTEXT.md` 决策行;框架内置文档按**内容**(非名字)排除并留痕,
一旦框架把本站 `global-error.tsx` 预渲染到该路径,条件即不再命中、路由自动回到清单。
历史对照:`axe-evidence-sitemap29.json`(29 页版,`passed=true`,116 行)—— 当时的 0 是真 0,但分母不足以覆盖 404 与兜底页。
## ⑥ 与 A-7 的补充复跑(chain8,最终树)
| 项 | 结果 |
|---|---|
| 覆盖率棘轮(A-7) | `npm run test:coverage` → **`EXIT_COVERAGE=0`**(2026-09-22T04:50:44Z):137 套件 / 1659 例全通过,`All files` = **Stmts 77.92 / Branch 84.6 / Funcs 77.64 / Lines 77.92**,对照 `config/test/jest.config.js` 的 `coverageThreshold`(82 / 75 / 75 / 75)四项均达标。数值已回写 `AGENTS.md` §5(原记 84.71 / 77.55 / 77.89 / 77.89 是 2026-09-21 树,`global-error.tsx` 加入后微移)→ `coverage-final-tree.txt` |
| iPhone SE 口径复跑(⑥ 的仿真部分) | `EXIT_SE_AUDIT=0`(2026-09-22T04:52:07Z),证据写入**新目录** `docs/acceptance/2026-09-22-iphone-se/`(40 图 + `measurements.json` + `audit.log`),不覆盖 09-21 那份 21:07Z 的旧证据。本轮把探针路由从 4 条扩到 5 条(新增 `/no-such-route-404-check`),得 **20 行 = 5 页 × 深/浅 × 同意条 pending/dismissed**:`overflow=0`(`docScrollW == clientW == 375` 全部成立)、`themeNull=0`、`dismissed` 态 `footerBottomGap=64` 一致(R-1 的双安全区空带已消除)。`pendingBannerOverlapFooter=8` 是 `fixed bottom-*` 浮层的既定行为,可关闭后消失,与旧证据同口径。**限制不变**:这是 Playwright 仿真,条件⑥ 要的「真机 iPhone SE + 系统深色」仍只能由交付决策人实机走查 |
**新探针当场挖到的一条观察(不是回归,待裁定)**:404 路由的 `footerBottomGap` 全为 `null` ⇒ 预渲染产物 `dist/standalone/dist/server/app/_not-found.html` 里**没有 `<footer>`**(同一探针下首页有 `<footer data-testid="footer" role="contentinfo">`)。根因不是 bug:`src/app/layout.tsx` 本身不渲染 Header/Footer,页面级外壳由各页自行组合,而 `src/app/not-found.tsx` 只返回 `NotFoundContent`(自带「返回首页 / 搜索」出口)。因此**404 与根错误页无站内导航、无备案页脚**——是否给边界页补齐页头页脚属设计判断,未擅自改动,列入决策清单。
## ① 四项 P0 的回归测试归属(非快照,指向可点开的具体用例)
- A-1/A-2 权限提升与改密鉴权:`src/app/api/admin/users/route.test.ts`(11 例,含「不得借先建号再自我提权绕过」「资料字段不得先落库再被角色校验拒绝」「不得停用/降级超管」)、`src/app/api/admin/roles/route.test.ts`(9 例)、`src/lib/permissions.test.ts`(10 例)。
- A-3 存储型 XSS:`src/lib/sanitize.test.ts`(15 例)。
- A-4 上传白名单 + 魔数:`src/lib/media/upload-policy.test.ts`(扩展名/MIME 申报不符、双扩展名、`.html`/`.svg`/可执行、路径穿越、空与超限)、`src/lib/media/media-service.test.ts:60,69`(**`upload-policy` 未被 mock**,断言「HTML 载荷在落盘前即被拒绝」,因此同时是路由下游的接线回归)、`src/app/api/admin/media/route.test.ts`(12 例:401/403/400/体积/多图)。
## 已知口径限制(不掩饰)
1. **chain2 那次 `805 passed / 263 failed` 作废**:`check:headings`/`check:contrast` 的脚本只 kill 包装进程 ⇒ `next-server` 孤儿占住 :3000,被 Playwright `reuseExistingServer` 静默复用,205 个失败是 `page.goto` 超时而非断言失败。已修(`detached` + 负 PID 杀进程组 + `process.exit` 前显式收服),chain3/4/6 的 `PRE/POST_*_LISTENERS` 均为 0 即为正/负对照。
2. **满负载竞态与"隔离必过"的差别**:本地 `retries: 0`、CI `retries: 2`(`e2e/playwright.config.ts`),且 4 个 project × 多 worker 同机跑 3 个引擎。本轮不做"加 retries 掩盖",而是逐项消除竞态成因(预热、auto-retry 断言、限定 `main` 作用域、函数式 setState)。残留风险:同机并发仍可能出新的竞态例。
3. ③「Jenkins 无 `|| echo` 后连续两次全绿」只能由交付决策人触发;本地已按 Jenkinsfile 原样跑通同一命令序列(`npm run build` + `npm run test:e2e:prod`)。
4. ⑥ 已在最终树上以仿真复跑(chain8,`docs/acceptance/2026-09-22-iphone-se/`,含新增的 404 路由);`measurements.json` 的 `caveat` 仍写明「Playwright 仿真,非真机」,**真机走查只能由交付决策人完成**,本条件不得由本地证据替代。
5. ② 的「提交信息留痕」需一次经授权的提交才能落地;**本轮全部改动(含 `src/app/global-error.tsx`、header setState、4 个 spec 修订、`CONTEXT.md`/`lessons-learned.md`)均未提交**。
6. 28 skipped 的拆解见 §5.23 与下表;其中 16 条 GA4 `@critical` 已在**生产目标**下单独复跑并全部真断言(`ga4-production-run.txt`:`Running 4 tests using 4 workers` → `4 passed (7.2s)`,含 `next start` 与 `output: standalone` 的告警原文)。
7. **覆盖缺口的处理进度**:`/products/erp-upgrade` 与 `/about/brand` 此前既不在 Lighthouse URL 表也不在视觉回归的 13 条路由表里(视觉表有 `/products/erp`,与 `erp-upgrade` 是**不同路由**)——这正是 §5.22 已记过的同型缺口。**Lighthouse 半边已闭合**(chain7 起为 9 URL × 3 = 27 运行,`EXIT=0`);**视觉半边仍未闭合**,把两条路由加进 `e2e/visual-regression.spec.ts` 需要 `--update-snapshots` 重写已审批基线,属破坏性动作,须单独授权。