- 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 产物装配与部署形态。
15 KiB
验收 §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 轮)。
两个独立信源确认它们是恒不成立的死断言,而非「页面恰好没问题」:
- 运行时报告:最新 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均在列。 - 审计注册表:
@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/体积/多图)。
已知口径限制(不掩饰)
- chain2 那次
805 passed / 263 failed作废:check:headings/check:contrast的脚本只 kill 包装进程 ⇒next-server孤儿占住 :3000,被 PlaywrightreuseExistingServer静默复用,205 个失败是page.goto超时而非断言失败。已修(detached+ 负 PID 杀进程组 +process.exit前显式收服),chain3/4/6 的PRE/POST_*_LISTENERS均为 0 即为正/负对照。 - 满负载竞态与"隔离必过"的差别:本地
retries: 0、CIretries: 2(e2e/playwright.config.ts),且 4 个 project × 多 worker 同机跑 3 个引擎。本轮不做"加 retries 掩盖",而是逐项消除竞态成因(预热、auto-retry 断言、限定main作用域、函数式 setState)。残留风险:同机并发仍可能出新的竞态例。 - ③「Jenkins 无
|| echo后连续两次全绿」只能由交付决策人触发;本地已按 Jenkinsfile 原样跑通同一命令序列(npm run build+npm run test:e2e:prod)。 - ⑥ 已在最终树上以仿真复跑(chain8,
docs/acceptance/2026-09-22-iphone-se/,含新增的 404 路由);measurements.json的caveat仍写明「Playwright 仿真,非真机」,真机走查只能由交付决策人完成,本条件不得由本地证据替代。 - ② 的「提交信息留痕」需一次经授权的提交才能落地;本轮全部改动(含
src/app/global-error.tsx、header setState、4 个 spec 修订、CONTEXT.md/lessons-learned.md)均未提交。 - 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的告警原文)。 - 覆盖缺口的处理进度:
/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重写已审批基线,属破坏性动作,须单独授权。