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

15 KiB
Raw Blame History

验收 §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 §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 重写已审批基线,属破坏性动作,须单独授权。