- 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 产物装配与部署形态。
233 lines
45 KiB
Markdown
233 lines
45 KiB
Markdown
# 系统性 Code Review / 全面测试 / 验收报告 — 2026-09-21
|
||
|
||
- 分支:`refactor/optimize-ui`(领先 `origin/main` 30 个提交;`src/` 改动 120 文件,+3248 / −3959)
|
||
- 环境:Next.js 16.3.0 (Turbopack) / React 18.3 / TypeScript 5 strict / Prisma 6.19 + SQLite / Tailwind 3.4
|
||
- 方法:4 路并行模块审查(API+鉴权 / lib+hooks / 组件+页面 / 数据+配置+测试基建),所有结论由主代理逐条复核证据后定级;无法证实的假设已撤回(见 §7)
|
||
- 实测门禁:type-check / lint / 单测+覆盖率 / build / **E2E 三浏览器全量** / check:contrast / check:headings / 安全响应头 / **Lighthouse 7 URL** / **变异测试(quick 作用域)** —— 全部由本次亲跑取回原始输出,未引用任何历史报告(详见 §2)
|
||
|
||
## 1. 验收结论
|
||
|
||
**不通过验收(REJECTED)。**
|
||
|
||
阻断项 5 类:①生产级安全漏洞(权限提升 ×2、存储型 XSS ×2);②E2E 92 例失败(3 浏览器一致复现,含 6 例 `@critical`);③CI 用 `|| echo` 吞掉全部功能性测试,绿色徽章不证明任何行为正确性;④本分支引入 2 处用户可见 UI 回归;⑤**可访问性门禁双重失效**(A-11)导致一个**全站 62 页**的 WCAG AA 对比度失败(A-12 Cookie 条隐私政策链接 3.31:1)在 `check:contrast` 与 Lighthouse 两道门禁下同时报绿。
|
||
|
||
> 关键判据不是「失败数」而是「门禁是否可信」。当前 `test:all` 与 Jenkins 绿灯所覆盖的范围,与 AGENTS.md §5 声称的质量门禁之间存在实质落差;本次实测中**两道 a11y 门禁双双通过,却被两个独立引擎各测出 critical 级失败**,即为该落差的最强证据。
|
||
|
||
## 2. 门禁执行结果(本次实测,非引用)
|
||
|
||
| 门禁 | 命令 | 结果 | 判定 |
|
||
|---|---|---|---|
|
||
| 类型检查 | `npm run type-check` | 0 error,EXIT=0 | 通过 |
|
||
| Lint | `npm run lint` | 0 error / **128 warning** | **不达 AGENTS.md「无警告」** |
|
||
| 单元测试 | `npm run test:unit` | **132 套件 / 1594 例全通过**,23.9s | 通过 |
|
||
| 覆盖率 | `npm run test:coverage` | stmts 75.9 / branch 84.28 / func 75.46 / line 75.9 | 通过(但门禁形同虚设,见 §3.3) |
|
||
| 生产构建 | `npm run build` | EXIT=0,62 静态页生成 | 通过 |
|
||
| E2E 三浏览器 | `playwright test --project=chromium,firefox,webkit` | **710 通过 / 92 失败 / 8 跳过**,17.2min,EXIT=1 | **失败** |
|
||
| 色彩对比度 | `npm run check:contrast` | 7/7 通过 | 通过(**但仅 7 组硬编码浅色对,盲区见 §3.4**) |
|
||
| 标题层级 | `npm run check:headings` | 10/10 页 0 问题 | 通过 |
|
||
| 安全响应头 | `npm run test:security:headers` | 6 通过 / 2 警告 | 通过(**但打的是线上 novalon.cn,不验证本分支**) |
|
||
| Lighthouse | `npx lighthouse@13`(对 `lighthouserc.json` 全部 7 个 URL,desktop preset) | **性能全绿**(perf 99-100 / FCP 246-250ms / LCP 790-896ms / CLS **0.000** / TBT 0ms / SI 248-414ms);**a11y 92-97、7 页全部含 contrast 失败节点,共 10 节点** | **断言 0 违规 → 门禁"通过",但门禁太松看不见真实 a11y 缺陷,见 §3.4** |
|
||
| 变异测试 | `npx stryker run --inPlace --mutate 'src/lib/utils.ts'`(`test:mutation:quick`) | **91.18%**(31 killed / 3 survived / 0 no-cov / 0 error),≥ `break:50`;**全量作用域未跑**(外推需数小时) | 通过(阈值达标),但暴露 2 处真实断言缺陷 + 作用域排除「零编造」核心,见 §3.5 |
|
||
| 压测 | `npm run test:performance` | **不可运行**:`k6` 是 v0.0.0 占位包(`node_modules/k6/package.json`:"Dummy package for autocompleting k6 scripts"),无 `node_modules/.bin/k6` | **失效** |
|
||
|
||
Lint 128 warning 构成:`no-explicit-any` 49 · `react-hooks/set-state-in-effect` 12 · `next/no-img-element` 10 · 其余 57。
|
||
|
||
## 3. 阻断级问题(P0)
|
||
|
||
### 3.1 权限提升与账号接管(已逐行复核)
|
||
|
||
**A-1 `content_admin` → `super_admin` 自主提权** — `src/app/api/admin/users/route.ts:90`(POST)/ `:166`(PUT)放行 `content_admin`;`:135` `const rolesToAssign = body.roleCodes?.length ? body.roleCodes : ['readonly']` 直接取请求体,`:139`/`:214` 仅校验「角色是否存在于 DB」,不校验「调用者是否有权授予」。`:201-208` 只阻止移除**自己**的 super_admin,授予方向无任何 allowlist。
|
||
攻击:`POST /api/admin/users {username,password,roleCodes:["super_admin"]}`。
|
||
修复:两个 handler 均将 `body.roleCodes` 与调用者角色集求交,非 `super_admin` 不得授予 `super_admin`。
|
||
|
||
**A-2 `content_admin` 可重置任意用户密码 → 接管超管** — `users/route.ts:189-191` `if (body.password) updateData.password = await hashPassword(body.password)` → `:193` `prisma.user.update({ where: { id: userId } })`,`userId` 来自 `:172` 攻击者可控的 query 参数,未校验目标是否持有 `super_admin`。同理 `:188` 可将超管 `status` 置 0 致其失联。
|
||
攻击:`PUT /api/admin/users?id=<superadmin_cuid> {password:"x"}` 后登录。
|
||
修复:改密/停用他人须 `super_admin`;目标持有 `super_admin` 时同样要求调用者为 `super_admin`。
|
||
|
||
架构成因:`src/proxy.ts:104` `matcher: ['/admin/:path*']` 且 `:81` 显式 `!pathname.startsWith('/api/')` —— **proxy 完全不覆盖 `/api/*`**,每个 route 必须自守卫。(Next 16 已将 `middleware.ts` 更名 `proxy.ts`,此处用法正确。)
|
||
|
||
### 3.2 存储型 XSS ×2(已验证数据通路)
|
||
|
||
**A-3 CMS 富文本未净化直出公开页** — `src/app/terms/page.tsx:212` 与 `src/app/privacy/page.tsx:267`:`dangerouslySetInnerHTML={{ __html: cmsContent }}`,`cmsContent` 来自 `getPublishedItems('legal-page')` → `item.data.content`(`terms/page.tsx:181`)。字段类型 `richtext`,描述即「支持 HTML 标签」(`content-types.ts:1563`)。**全仓无净化器**(`DOMPurify` / `sanitize-html` 检索 0 命中);`RichTextEditor.tsx:38-46` 的 TipTap `Link` 未配置 `protocols`。
|
||
影响:任何可编辑 legal-page 的账号(或经 A-1 提权者)即可在 `/terms`、`/privacy` 对**全部访客**注入脚本。
|
||
修复:渲染端统一 `sanitize-html`(白名单标签/属性/协议),并在 `uploadMedia`/items 写入侧再校验一次。
|
||
|
||
**A-4 上传文件同源分发且安全头丢失** — `src/app/api/admin/media/route.ts:79-84` 将客户端可控的 `file.type`/`file.name` 原样传入;`src/lib/media/media-service.ts:36-87` `uploadMedia` **无扩展名/MIME 白名单**(`:44` `isImage()` 只门控缩略图,不门控落盘),`generateUniqueFileName` 保留原扩展名 → `evil.html` 存入 `public/uploads/`。`nginx-static-production.conf:163-168` 的 `location /uploads/` 自带 `add_header`,按 nginx 语义**不再继承** server 级 `X-Content-Type-Options: nosniff`(`:42`)与 CSP(`:128`)→ 以 `text/html` 同源渲染。叠加 CSP 含 `script-src 'unsafe-inline' 'unsafe-eval'`(`next.config.mjs:40`),失去兜底。
|
||
修复:`uploadMedia` 增白名单 + magic-byte 校验;`/uploads/` 内重新 `add_header nosniff/CSP` 并对非图片强制 `Content-Disposition: attachment`;移除 `unsafe-eval`。
|
||
|
||
### 3.3 测试基建不可信(最高价值发现)
|
||
|
||
**A-5 CI 功能性测试全部被 `|| echo` 吞掉** — `Jenkinsfile:179` `playwright test --grep "@smoke|@critical" || echo "⚠️ …继续执行"`;`:181` journey、`:214` 视觉回归、`:236` `npm audit`、`:238` 安全头 同法吞没。真门禁仅 lint(:129)/type-check(:136)/coverage(:148)/build(:174,254)。**AGENTS.md §5 列为必须的 lighthouse、`check:contrast`、`check:headings`、mutation、k6、integration 在流水线中完全缺席。**
|
||
结论:「CI 绿」当前只证明 lint + tsc + 「覆盖率≥30%」+ build 成功,不证明任何用户行为。
|
||
修复:去掉功能性 stage 的 `|| echo`,把缺失门禁纳入流水线。
|
||
|
||
**A-6 `@critical` GA4 测试是自证式空测** — `e2e/ga4-event-tracking.spec.ts:47,75,165,235`(4 例,标签 `@critical`):`beforeEach:24-31` 注入 mock `window.gtag`,测试体 `:56-62` **自行调用** `gtag('config','G-TEST123',…)`,再于 `:65-70` 断言 `__gtagCalls` 中存在该调用。TC-GA4-003 `:184-205` 甚至不点击按钮,只判可见性后自调用;且 `if (isCtaVisible) {} else {}` 两分支均自调用 → **CTA 不存在也通过**。断言的 `G-TEST123` 由测试自己提供,与应用无关。
|
||
影响:`npm run test:critical` 对分析埋点的绿灯为 0 信息量。
|
||
修复:改为拦截真实网络请求(`gtag/js` 的 `page_path`/事件参数),或让 `trackEvent` 走可注入 sink 并断言应用调用。
|
||
|
||
**A-7 覆盖率门禁形同虚设 + 三处文档口径互斥** — 实际生效门禁用 `jest.config.js` global `branches 30 / functions 25 / lines 32 / statements 30`,而实测为 `84.28 / 75.46 / 75.9 / 75.9`,**低于实测约 45 个百分点**;目录级阈值多数不可约束:`seo` branches 门限 0(实测 100)、`content` branches 门限 4(实测 100)、`ui` functions 门限 10(实测 81)。文档互斥:`CLAUDE.md:39,196` 称阈值 80% 且路径写作 `config/test/jest.config.js`(**路径错误**,真实配置在仓库根;`config/test/jest.config.js` 仅 Stryker 使用);`docs/development/quality-gates.md:82-85` 称四项均 ≥70%。
|
||
修复:阈值上调至「实测 −5pp」的棘轮值;统一三处文档并修正配置路径。
|
||
|
||
**A-8 单测不接触真实数据层** — `jest.setup.js:12-27` 全局 mock `PrismaClient`(`findMany → []`),`:29-50` 整体 mock `@/lib/cms/data-server`。因此 §4 所有数据层缺陷(无事务、TOCTOU、JSON.parse 崩溃)**没有任何测试能发现**。`npm run test:integration`(`--testPathPatterns='src/app/api/'`)跑的仍是 mock 版 `route.test.ts`(如 `items/route.test.ts:14` mock `@/lib/db`),命名误导。
|
||
另有空洞断言:`src/lib/db.test.ts:6-9` `expect(prisma).toBeDefined()` 对全局 mock 永真;`colors.test.ts`(≈18)、`constants.test.ts`(≈24)、`design-system.test.ts`(21) 大量 `toBeDefined()` 静态常量。
|
||
|
||
**A-9 视觉基线已「追认现状」,且 9 张近空白** — 提交 `f543e47` 自述「87 例失败…均为尺寸级不匹配」后重生成 86/115 基线 ⇒ 重生时点已存在的回归**被固化为参照**,该套件此后无法再发现它。基线总数 107(5 project × 21,齐全),但 `visual-{chromium,firefox,webkit}-desktop/…/button-{default,hover,focus}-*.png` 为 **912 / 1111 / 1961–2030 字节**,日期 Jul 26 与 Jul 6(**在 9 月重生成之外**)⇒ 近空白区域仍算「比对通过」。容差偏松:`playwright.config.ts:38-41` `maxDiffPixels:200`、`maxDiffPixelRatio:0.005`、`threshold:0.3`。部分断言条件化(`visual-regression.spec.ts:73,91,101,111,130,182` 元素缺失即 0 断言通过)或为永真(`:199` `color||fontFamily`、`:217` `brand||ink||bg` `toBeTruthy()`)。
|
||
|
||
**A-10 E2E 从不验证交付物** — `playwright.config.ts:130` `command: 'npm run dev'` + `:132 reuseExistingServer: true`,全部规格跑在 **dev server**(甚至可能是上一轮残留进程)而非 `output:'standalone'` 构建产物。构建期才暴露的问题(预渲染、ISR、production header)永不被测。
|
||
修复:新增 `--project=production` 指向 `npm run start` 的 webServer。
|
||
|
||
### 3.4 可访问性门禁的双重失效与已证实的全站缺陷
|
||
|
||
**A-11 两个对比度门禁同时看不见同一个真实违规** — 两条独立通路各自漏检:
|
||
|
||
1. **`check:contrast` 静态漏检** — `scripts/utils/check-color-contrast.ts:10-18` 的 `criticalColorPairs` 是 **7 组硬编码十六进制**(全部 `#FFFFFF` 底),既不读 `tailwind.config.js`/`globals.css`,也**不含任何暗色模式配对**,更不覆盖 alpha 修饰类。而本分支主题提交(`4b8500a`、`846585a`)恰好把暗色模式与 `--color-brand` 双通道拆分作为主战场 —— 门禁与其要保护的对象完全脱节,令牌一旦改动它仍对着陈旧色值报绿。
|
||
2. **Lighthouse 阈值漏检** — `lighthouserc.json` 对 accessibility 断言 **≥0.9**,而含 axe critical 失败的实际得分是 **92-97**:`products` 92 分(7 个 `aria-required-parent` critical 节点)、其余 6 页 96-97 分(每页 1-4 个 `color-contrast` 节点)。**critical 级 WCAG 失败被折算成分数后落在门禁线之上**,故 `npm run lighthouse` 会显示全绿。
|
||
|
||
**A-12 Cookie 同意条的「隐私政策」链接在全部 62 页对比度不达标(已双引擎证实)** — `src/components/analytics/CookieConsent.tsx:152` `text-[var(--color-brand)]`(#C41E3A)落在同文件 `:141` 的 `bg-[var(--color-bg-primary)]`(暗色 #0A0E14)上:
|
||
|
||
- Lighthouse/axe 实测 **3.3:1**(15.75px normal,要求 4.5:1);我按 `globals.css:23/416` 令牌值独立算得 **3.31:1**,两法吻合。
|
||
- 该组件挂在**根布局** `src/app/layout.tsx:223` ⇒ 7/7 被测页各命中 1 次,即**全站每一页**都失败(10 个失败节点中的 7 个来自此处,余 3 个见 §4.3)。
|
||
- 根因是**违反已写明的设计契约**:DESIGN.md:154「**The Two-Channel Red Rule.** 底色用 `--color-brand`(暗黑不翻),文字用 `--color-brand-ink`(暗黑翻至 #F87171)。合并成一条是 bug 的源头」、DESIGN.md:232「用 `text-brand-ink` 写红色文字…**永不混用**」。此处的合规写法应为 `text-brand-ink`。
|
||
- 讽刺点:这是**隐私/Cookie 同意 UI**里指向隐私政策的链接,属合规可见路径。
|
||
|
||
**同类面**:全仓 **67 处** `text-[var(--color-brand)]`(23 个文件,含 `CookieConsent`、`error.tsx`、`not-found-content.tsx`、`cta-section`、`hero-section-v2`、`product-card`、`service-card` 等)对比 233 处合规的 `text-brand-ink`。这些站点在浅色面(#FFFFFF 上 **5.84:1**)偶然达标,一旦位于暗色面即跌到 **3.31:1**(`--color-brand-bg` #2A1418 上更仅 **2.97:1**)—— 而本分支暗色为默认。**修复应整族收敛而非逐点打补丁**:以 `text-brand-ink` 替换全部 67 处,并加一条 grep 门禁禁止 `text-[var(--color-brand)]`。
|
||
|
||
> 本项由 Playwright+axe(移动)与 Lighthouse+axe(桌面)**两个独立引擎**分别复现,非源码推断 —— 这是本次验收中证据强度最高的一类结论。
|
||
|
||
### 3.5 变异测试(本次实跑,唯一真正量化「测试有没有断言」的门禁)
|
||
|
||
`npm run test:mutation:quick`(作用域 `src/lib/utils.ts`):**91.18%**(31 killed / 3 survived / 0 no-coverage / 0 error,均值 14.97 tests/mutant,74s)≥ `stryker.config.json` 的 `break: 50` ⇒ 阈值达标。但 3 个存活变异体经逐个复核后,**2 个是本项目的真实测试缺陷**:
|
||
|
||
**A-13 `lerp` 的全部测试用例都以 `start = 0` 输入 ⇒ `start` 偏移量从未被检验** — `src/lib/utils.ts:49` `start + (end - start) * t` 被改为 `start + (end + start) * t` 后仍全绿。原因(`src/lib/utils.test.ts:120-128` 四例逐一验算):`lerp(0,10,0.5)`、`lerp(0,100,0.25)`、`lerp(0,10,0)`、`lerp(0,10,1)` —— **`start` 恒为 0**,而 `end - 0` 与 `end + 0` 数值相同,故该变异在数学上不可观测。这是**测试数据选择缺陷**:函数唯一独有的参数(`start`)恰好是唯一没被非零值覆盖的那个。
|
||
|
||
**A-14 `randomBetween` 只断言边界,检不出算子错误** — `utils.ts:45` `Math.random() * (max - min) + min` 改为 `/` 后仍全绿。`test.ts:106-115` 只做 `toBeGreaterThanOrEqual(1)` / `toBeLessThanOrEqual(10)`;变异实现给出 `rand/9 + 1 ∈ [1, 1.89]`,负数例给出 `∈ [-1, -0.89]` —— 均落在断言区间内。分布被压到区间一端 11% 的长度而测试无法察觉,因为**没有任何一例检验取值是否覆盖全区或分布是否均匀**。
|
||
|
||
**(反向校准)第 3 个存活体不是缺陷** — `utils.ts:25` 的 `if (timeout)` → `if (true)`:`clearTimeout(null)` 在 Node/浏览器均为合法 no-op,故该变异体与原实现**语义等价**,存活属正常,不计入测试质量问题。列出以示本次定级未把噪声当发现。
|
||
|
||
**A-15 变异门禁的作用域把「零编造」核心排除在外** — `stryker.config.json:20` 的 `mutate` 含排除项 `"!src/lib/constants/**"`,而 `src/lib/constants/metrics-basis.ts`(`resolveMetricBasis` / `weakestBasis` / `FORBIDDEN_PROOF_PHRASES`,即项目 AGENTS.md §3「零编造」原则的唯一机械载体)**正在该目录内**。后果:那个「未声明口径必须保守回落到 `target`」的回落逻辑,若被改坏(例如回落到 `verified`)**不会有任何变异测试发现** —— 与 B-3「`basis` 仅类型约定、写入侧无校验」构成同一处治理缺口的两半。
|
||
|
||
**门禁自身的两点风险(实测记录)**:
|
||
1. `npm run test:mutation` 与 `:quick` 均带 `--inPlace`,Stryker 会**直接改写工作树**(其日志自述 "In place mode is enabled, Stryker will be overriding YOUR files")。本次运行期间 `git status` 一度显示 10+ 个文件为 ` M`(Stryker 为注入覆盖率而临时修补 jest/babel 配置),结束后由 `.stryker-tmp/backup-*` 复原,**我已核实工作树恢复到运行前状态**(仅本报告与 `deliverables/` 两个未跟踪项)。但这意味着 CI 中一旦该 job 中途崩溃,仓库将留下**被变异过的源码**且无 `git checkout` 提示 —— 建议 CI 改用沙箱模式(去掉 `--inPlace`)。
|
||
2. 顺带证伪了一个可疑点:我曾怀疑 Stryker 用的 `config/test/jest.config.js` 与根 `jest.config.js` 存在测试发现差异(其 dry-run 报 "Ran 668 tests" 而 `test:unit` 为 1594)。实测 `npx jest --config config/test/jest.config.js` ⇒ **132 套件 / 1594 例全通过**,两配置的 `testMatch`/`roots`/`setupFilesAfterEnv` 一致,无结果分歧,故**不列为缺陷**(668 与 ✘ 标记是 Stryker perTest 覆盖分析的呈现方式,✘ = 该测试未覆盖当前变异体,**不是失败**)。唯一真实差异仍是 A-7 已记的**阈值不同**(此配置 global 为 70/55/55/55,根配置为 30/25/32/30)—— 即两份 jest 配置近重复却配着互不相同的门禁值。
|
||
|
||
|
||
|
||
## 4. 高危问题(P1)
|
||
|
||
### 4.1 本分支引入的 UI 回归(验收主要风险)
|
||
|
||
**R-1 双重移动端安全区留白 ≈190px** — `globals.css:1321-1323`(本分支审计修复 `152eef9` 新增)`footer { padding-bottom: calc(64px + env(safe-area-inset-bottom,0px)) }` 叠加 `src/components/layout/footer.tsx:171` 的 `pb-[calc(8rem+env(safe-area-inset-bottom,0px))]` ⇒ iPhone SE/15 上备案行下方 128+64+2×inset ≈ **192–226px** 空深色带,**全部 31 页**。
|
||
修复:二选一(建议只保留 CSS 侧,并删除组件 `pb-[…]`)。
|
||
|
||
**R-2 动效时长收敛到错误基准** — `CONTEXT.md:76` 与 `DESIGN.md:233` 规定入场 **180–280ms**,`--transition-normal: 280ms`(`globals.css:252`);分支却收敛到 300ms:`src/components/ui/scroll-reveal.tsx:73` `duration = 0.3` 且注释**错误引用契约为「200-300ms」**;另有 `duration-300` ×108、`duration: 0.3` ×179 未令牌化。
|
||
修复:以 `duration-normal/fast` 令牌替换字面量,修正注释;或正式修订 CONTEXT.md。同类:`--stagger-*` 令牌(`globals.css:272-276`)**零采用**(`var(--stagger` 检索 0 命中),实散为 `detail-trust-section.tsx:108` `index*0.06`、`header.tsx:230` `index*0.05`、`contact-content-v3.tsx:262` `delay: 1.2`(**1200ms**,远超 150ms 段间上限)。
|
||
|
||
**R-3 暗色模式新闻页对比度不达标** — `src/app/(marketing)/news/[slug]/NewsDetailClient.tsx:47` `bg-[var(--color-brand-bg)] text-[var(--color-brand)]`(另 `:34`、`:124`)误用**设计上不翻转**的 `--color-brand` #C41E3A 作文字色,违反 DESIGN.md:154 双通道红规则。暗色实测 **3.31:1**(`--color-bg-primary` #0A0E14 上)与 **2.97:1**(`--color-brand-bg` #2A1418 上)。`news-detail-content-v3.tsx:29,105`、`news-content-v3.tsx:37` 同病。同处 `hover:bg-[var(--color-brand)]/20`、`text-[var(--color-brand)]/30` 在构建产物中**不生成任何 CSS**(对照 `border-brand/30` 可正常编译)。
|
||
**本项是 §3.4 A-12 全站缺陷的一个局部实例** —— 同一契约违反在全仓共 67 处,故修复须按 A-12 整族收敛,只改新闻页会留下 Cookie 条等仍在失败。
|
||
修复:文字改用 `text-brand-ink`。同族缺陷另见 `content-unavailable.tsx:38`(本分支新增,暗色 3.31:1)。
|
||
|
||
**R-4 审计 §10.1 修复 #4 只落一半** — `src/components/detail/solution-service-card.tsx:128` `shadow-blue-500/20 → shadow-brand` 已做,同行保留 `bg-gradient-to-br from-[var(--color-accent-blue)] to-[#1d4ed8]`(硬编码 hex + 双色渐变),`:127` `hover:border-blue-300`、`:134` `group-hover:text-blue-600` 绕过 `accent-blue` 令牌且暗色不翻转 ⇒ **蓝色块配品牌红光晕**,违反 DESIGN.md:150/153「One Voice」与 CONTEXT.md:45「红与强调色不同卡」。
|
||
|
||
**R-5 `prefers-reduced-motion` 未被 framer-motion 尊重** — 全仓 **0 处 `MotionConfig`**(`grep MotionConfig src` 无命中),`globals.css:755-764` 的 CSS 守卫(`animation/transition-duration: 0.01ms !important`)**管不到 framer-motion 的 JS-rAF 内联样式**。仅部分文件单独调用 `useReducedMotion()`,未接线者(如 `detail-trust-section.tsx:80-83` 的 `y:24→0`、`detail-hero.tsx`、`product-detail-content-v3.tsx`)在暗色+动效默认开启的本分支上,对前庭敏感用户仍产生大位移。与 DESIGN.md:122「reduced-motion 全链路守卫」不符。
|
||
修复:根布局包 `<MotionConfig reducedMotion="user">`,一处收口。
|
||
|
||
### 4.2 逻辑与安全(非本分支引入,但在验收范围内)
|
||
|
||
**B-1 创建接口绕过发布工作流** — `src/app/api/admin/items/route.ts:130` `status: status || 'draft'` + `:135` `publishedAt: status==='published' ? new Date() : null`,仅 `:107` `requirePermission(…,'create')` 守卫;PUT 侧 `:184-186` 明确拒改 status 并要求走 workflow 接口 —— 即 `POST {status:'published'}` 可跳过 submit/approve 与 `publish` 权限直接上线。且任意 status 字符串可入库,之后 `workflow.ts:42` 对所有动作返回 `false`,条目永久卡死且无反馈。
|
||
|
||
**B-2 登录接口用户枚举 + 零限流** — `src/app/api/auth/login/route.ts:20-22` 在 `:24` `verifyPassword` **之前**返回 `'账号已被禁用,请联系管理员'`,未知用户则为 `:17` `'用户名或密码错误'`;即便消息相同,`user` 不存在时不跑 bcrypt 也留下可靠的时间侧信道。全仓唯一限流在 `src/app/api/contact/route.ts:14`,登录裸奔(`nginx-static-production.conf:177` `rate=100r/s` 仍允许约 860 万次/日)。修复:禁用检查移到验密之后;对 `/api/auth/login` 加 per-IP+per-username 计数或上游独立 `limit_req`。
|
||
|
||
**B-3 `basis` 仅类型约定,非结构强制**(直接对应「零编造」原则)— 类型侧全部可选:`products.ts:15`、`services.ts:40`、`solutions.ts:27`、`sections.tsx:323`、`about-content-v4.tsx:39` 皆 `basis?: MetricBasis`;运行侧 `items/route.ts:131` `JSON.stringify(data || {})` **完全不按 `FieldDefinition` 校验**(`cms/types.ts:50-55` 的 `validation {min,max,pattern}` 全仓从未执行,zod 只在 `api/contact/route.ts` 使用)。故 `POST {data:{metrics:[{value:'99.9%',basis:'verified'}]}}` 会渲染「已有可核验的实测出处」。机械门禁 `metrics-basis.test.ts:53-64,170-182` 读的是**导入的 seed 字面量**、只扫 `src/app`+`src/components`,`prisma/` 与 `/admin` 编辑后的库内容从不复检。
|
||
缓解事实:`resolveMetricBasis`/`weakestBasis`(`metrics-basis.ts:34-47`)保守回落 + `content-types.ts:8-18` 的 `metricBasisField` 明示「留空按目标口径处理」—— 兜底方向正确,`FORBIDDEN_PROOF_PHRASES` 扫描也确属严格(含 `length>40` 防空跑)。真正的漏洞在**写入侧无校验**,以及 `home-content-v15.tsx:510,552,555,562` 用 `Record<string, any>` / `as any` 把 CMS 载荷从类型系统里放行。
|
||
|
||
**B-4 `data:null` 写库即打挂整页** — `items/route.ts:197` `if (data !== undefined) updateData.data = JSON.stringify(data)`(PUT 缺 POST 那样的 `|| {}` 兜底)→ `data-server.ts:20` `JSON.parse(item.data)` 得 `null` → `terms/page.tsx:180` `items.find(i => i.data.pageType === 'terms')` 抛 `TypeError`。同类:`data-server.ts:20,75-77,89-91,106` 的 `JSON.parse` **无 try/catch**(对照 `media-service.ts:133-139 parseDerivatives` 已正确守卫),单个脏列即可中断页面/SSG。
|
||
|
||
**B-5 角色权限重写无事务** — `src/app/api/admin/roles/route.ts:81` 先 `deleteMany` 全部权限再 `:85-97` 循环 `create`,**全仓应用代码零 `$transaction`**。中途失败即留下**权限为空/半权限**的角色(提权/降权双向风险)。同因:`cms/workflow.ts:59→66→80` `version: item.version+1` 读-改-写在事务外(`items/route.ts:192` 却用了正确的 `{ increment: 1 }`,同规则两实现)→ 并发审批丢更新 + status TOCTOU;`workflow.ts:80→90→102` update/audit/notify 三连 await 无原子性,通知失败会把成功审批变成 500。SQLite 侧 `src/lib/db.ts:7` 未配 `journal_mode=WAL`/`busy_timeout`(`DATABASE_URL` 无查询参数),默认 rollback journal + `busy_timeout=0` 下并发写直接 `SQLITE_BUSY`。
|
||
|
||
**B-6 `/api/cms/draft/enable` 密钥缺失即放行 + 开放重定向** — `route.ts:15` `if (expectedSecret && secret !== expectedSecret)` 为 **fail-open**;`CMS_PREVIEW_SECRET` 经全仓检索**不在 `.env` / `.env.local` / `.env.production` / `.env.example` 任何一处**,故线上恒为未配置,任意请求可 `draft.enable()`,并直达 `:25-27` `NextResponse.redirect(new URL(redirect, request.url))`,`redirect` 取请求体、协议相对形式 `//evil.com` 即跳出站外。(内容面影响当前为零:`draftMode()` 除这两个路由外无人读取,`data-server.ts:38,49,195,219` 硬过滤 `status:'published'`。对照 `/api/cms/revalidate/route.ts:40-45` 未配置即 500,写法正确 —— 应统一为 fail-closed。)
|
||
|
||
**B-7 刷新令牌无轮换/吊销** — `auth/refresh/route.ts:16` 仅验签名,`:21-25` 直接用 `payload` 重签,不加载用户 ⇒ 被禁用/删除的用户 7 天内持续换取访问令牌,绕过 `login/route.ts:20` 的禁用拦截;`:24` `role: payload.role` 沿用旧 claim,降权用户保留高权标识(`permissions.ts:38-56` 按 `UserRole` 实查故服务端不破,但 `admin-layout.tsx:241`、`auth/me/route.ts:15` 会显示陈旧角色)。`logout/route.ts:5-10` 只清 cookie,`prisma/schema.prisma` 无 jti/tokenVersion 表 ⇒ 失窃 refresh token 登出后仍有效。
|
||
|
||
**B-8 Bento 网格 ARIA 角色无父(3 页 × 3 浏览器 = 9 例失败的真实根因)** — `src/components/sections/bento-grid.tsx:23` 把 `role="list"` 放在 `BentoGrid`、`:47` 子项 `role="listitem"`;但 `src/app/(marketing)/products/products-content-v3.tsx:9` **只 import 了 `BentoItem`**,卡片直接落在无 `role="list"` 的 `div.grid lg:grid-cols-2 gap-px`(`:~193`)上 ⇒ axe `aria-required-parent`(critical)。
|
||
**已由 Lighthouse 桌面端独立复现并逐节点确认**:`/products` 命中 **7 个** critical 节点,`data-testid` 分别为 `bento-product-card-{erp,crm,cms,bi,sds,oa,novavis}` —— 与我按源码推断的「7 张 BentoItem 卡」精确一致,故根因不是假设。该页 a11y 得分因此降至 **92**(仍高于 0.9 门禁,见 A-11)。
|
||
修复:改用 `BentoGrid` 包裹,或去掉 `BentoItem` 的 `role="listitem"`。
|
||
|
||
**B-9 `tracking-tightest` 是空类名,10 个 H1 丢失展示级字距** — `tailwind.config.js:157-165` 的 `letterSpacing` 仅 `tighter/tight/normal/wide/wider/widest/eyebrow`,**无 `tightest`**;构建产物 `dist/static/chunks/0o3c09ni2y1ao.css` 内只有 `tracking-tighter`/`tracking-tight` 两条规则,**不存在 `.tracking-tightest`** ⇒ 该类在 10 处标题(`contact-content-v3.tsx:249`、`about-content-v4.tsx:70`、`cases-content-v3.tsx:146`、`team-content-v3.tsx:168` 等)**静默失效**,全部大字标题以 0em 字距呈现,与 DESIGN.md:28 的 −0.03em 相悖。与 config 自述(`:130-133`)已发生过的 `font-calligraphy` 同族缺陷。
|
||
|
||
**B-10 mega 下拉键盘不可关闭(WCAG 1.4.13 / 2.1.2)** — `src/components/layout/mega-dropdown.tsx:37` `onMouseEnter` 开、`:28-30` 仅 `onMouseLeave` 关,无 Escape 路径;`header.tsx:28-32` 的全局 Escape 只看 `isOpen`(移动抽屉),从不看 `openDropdown`。键盘用户在「产品」上回车后必须动鼠标才能关闭。
|
||
|
||
### 4.3 E2E 真实失败分类(92 = 约 30 例 × 3 浏览器,非抖动)
|
||
|
||
| 失败簇 | 例数 | 定性 |
|
||
|---|---|---|
|
||
| `ga4-event-tracking` TC-GA4-001..004 `@critical` | 12 | **测试自身缺陷**(A-6 空测)+ 断言环境不成立 |
|
||
| `mobile-accessibility` axe `aria-required-parent` `/products` | 3 | **真实缺陷** B-8(Lighthouse 桌面端独立复现同样 7 节点,见 B-8) |
|
||
| `mobile-accessibility` axe `color-contrast` `/about`、`/team` | 6 | **需产品裁决**:节点为 `about-content-v4.tsx:217`、`team-content-v3.tsx:85,118` 的 `text-text-muted/30|/10` **`aria-hidden="true"` 装饰性序号**(Lighthouse 桌面端在 `/about` 复现同样 3 个 `aria-hidden` 节点,类名一致);axe 按视觉可见性仍会报。建议按 WCAG「incidental/decorative」显式豁免,或提高 alpha。真正该修的是**正文** `text-text-secondary/80`(`brand-content.tsx:129`、`team-content-v3.tsx:205`)。**注意:Lighthouse 另在 7/7 页各报 1 例装饰节点之外的真缺陷,即 A-12 Cookie 条链接 —— 移动套件反而没抓到它** |
|
||
| 触摸目标 ≥44px(首页,`@accessibility`+`@performance`) | 6 | 真实移动可用性风险,需按元素定位 |
|
||
| `p1-brand-visual-audit`「不应出现可见 novalon」× 首页/全站 | 6 | **测试过期**(见 §7),`CONTEXT.md:194` 已批准该文案 |
|
||
| `uj-11b` 共创旅程 `@critical` | 6 | **真实可测性缺陷**:`uj-11-home-conversion.spec.ts:117` 依赖 `data-testid="early-access-banner"`,而首页 HTML 中该 testid **0 命中**;`:131` 期望文案「成为首批共创客户」HTML 亦 0 命中(「首批客户共创中」「共创进行时」均存在)。实现未落测试钩子 |
|
||
|
||
## 5. 中危(P2)摘要
|
||
|
||
- `header.tsx:187` `aria-controls="mobile-menu"` 指向 `{isOpen && …}` 内才存在的 `id`(`:219`)——关闭时 AT 解析落空。
|
||
- `contact-content-v3.tsx:111` `setErrors(prev => ({…prev,[field]:undefined}))` 不删键 ⇒ `:376` `Object.keys(errors).length` 与 `:379`「请修正以下 N 项」长期错误,字段全修完后仍残留带空 `<li>` 的 `role="alert"` 红框。
|
||
- `header.tsx:167` `<div className="hidden md:flex">` 使 `ThemeToggle` **桌面专属**,移动抽屉(`:224-261`)从不渲染 ⇒ 系统深色下的手机用户无法切浅色。本分支暗色默认开启,影响放大。
|
||
- `header.tsx:87` 高度 80/64 动画 vs `layout.tsx:18` 固定 `pt-16`(64px) ⇒ `scrollY===0` 时首屏顶部 16px 内容压在固定头下方。
|
||
- `api-crypto.ts:49-50` 以 `content-length` 判定有无请求体,chunked/H2 下**静默跳过解密**并把 `{data:"<base64>"}` 交给 handler(`:93`),管理端保存即写入密文;`admin-api.ts:44-49` `catch {}` 在非安全上下文(`crypto.subtle` 不可用)**降级明文**;`:85` 将 `e.message` 回显客户端。密钥为 `NEXT_PUBLIC_*` 且盐硬编码 ⇒ 属混淆而非加密,无任何逻辑把它当鉴权用(此点正确)。
|
||
- `analytics.ts:178-188` `trackOutboundLink` 同时发 `outbound_click` 与 `click`,外跳统计**双计**;`:172` `value || 1` 吞掉 0;`:18-23` 默认 `analytics: true` 与 `:29-42` 无形状合并的 `JSON.parse` 可能违背用户实际同意。
|
||
- `use-focus-trap.ts:43-46` Escape 恒 `preventDefault` 并归还焦点却不通知持有者(焦点可逃出仍开启的 trap);`:13` 的 `[tabindex]:not([tabindex="-1"])` 只约束末项,`<button tabIndex={-1}>`/`<input type=hidden>` 被当可聚焦;`:64` 无条件 `overflow='unset'`,层叠弹窗互相解锁滚动。`use-keyboard-shortcuts.ts:34-36` `preventDefault()` 后调可能未接线的 `onSkipToContent` ⇒ **Tab 键对键盘用户死路**。
|
||
- `media-service.ts:113-117` 先删文件后删 DB 记录,DB 失败即留下指向已删文件的资产;`:109-111` 空 `catch {}` 使畸形 derivatives 的文件永不被删。`media/storage.ts:29` `fs.unlink` 未做 `resolve`+前缀校验(当前 `deleteMedia` 只接受库内路径)。
|
||
- 死代码(grep 复核零引用):`ui/loading-skeleton.tsx`、`cms/RichTextEditor.tsx`、`examples/ContactFormAnalyticsExample.tsx`、`content/testimonials.tsx`、`lib/gradients.ts`(+仅被它引用的 `lib/colors.ts`;且 `getGlowStyle` 把 `primary` 映射为调色板中不存在的蓝 `'0, 94, 184'`)、`ui/metric-card.tsx`、`ui/stats-showcase.tsx`、`sections/stats-bar.tsx`。汇总桶 `ui/index.ts`(201 行)、`sections/index.ts`、`layout/index.ts` **无任何 importer** ⇒ 经其可达的 `metric-card/stats-showcase/milestone-timeline/flip-clock/page-nav/list-page-hero/accordion/tabs/dialog/select` 全为死接线。这解释了 §4 中若干「渲染缺陷」实为潜伏缺陷。
|
||
- `products/[id]/page.tsx:9-14`(及 `solutions/[id]`、`services/[id]`、`news/[slug]`)同时声明 `generateStaticParams` 与 `export const dynamic='force-dynamic'` —— 后者胜出 ⇒ 前者是死码,且这 4 类详情页退出全站 `revalidate=3600` ISR 策略。`next.config.mjs:4` 实为 `output:'standalone'`,而 `:10` 注释仍以「静态导出限制」为 `unoptimized:true` 辩护,AGENTS.md/CLAUDE.md 亦仍称静态导出。
|
||
- 配置/文档矛盾:`Dockerfile` 与 `docker-compose.yml` 把 `dist` 当静态 HTML 根拷贝、`Dockerfile.static` 拷贝**不存在的 `html/`** ⇒ 用基础 Dockerfile 部署即白屏,仅 `Dockerfile.prod`(`COPY dist/standalone` + `node server.js`)与配置模式相符。`next.config.mjs:36` `X-Frame-Options: DENY` 与 nginx `SAMEORIGIN`(`:40,124,140,182,194`)**双重且互斥**,并产生重复 CSP —— 这正是 `test:security:headers` 报 2 警告、且 `X-Frame-Options` 实际值为 `"DENY, SAMEORIGIN"` 的来由。Sentry 未用 `withSentryConfig` 包裹 ⇒ **不上传 source map**,线上堆栈为压缩态。`tsconfig.json` 仍排除已不存在的 `src/app/(marketing)/_archive`(AGENTS.md/CLAUDE.md 亦仍描述 `_archive/` 约定)。
|
||
- `data-server.ts:182-210` 只解析 `zi.itemId`、忽略 `ContentZoneItem.item`,且条目全部未发布的 zone 不写 key(返回 `undefined` 而非 `[]`)。`workflow.ts:94` 把 `submit`/`reject` 一律记为 `action:'update'`(驳回与编辑在审计日志中不可分),且 `'approve'|'archive'` 越出 `cms/types.ts:196` 联合类型,仅靠 `:50` 的 `as unknown as` 通过编译。
|
||
|
||
## 6. 已核实为正确(避免无谓返工)
|
||
|
||
`$queryRaw/$executeRaw` 应用层零使用(仅 Prisma 生成类型含);Prisma 写入无 `...body` 质量赋值;`media-service.ts:9` `path.basename` 先于 `storage.ts:19` `path.join`,遍历已消;通知路由按会话 `userId` 收口(`notifications.ts:80,90`),无 IDOR;`roles/route.ts:18,59` 正确限 `super_admin` 且 `:73` 保护该角色自身;`revalidate` fail-closed;cookie `HttpOnly; SameSite=Lax` + 条件 `Secure`;`auth.ts:10-15` 缺密钥即抛(无硬编码兜底);三个 `.env*` 均已 gitignore(`git ls-files` 仅 `.env.example` 占位)。
|
||
迁移无漂移(5 个 migration 与 `schema.prisma` 对齐,RBAC 表已在 `prisma/dev.db` 落地);seed 中 55 处 `basis` 一律取最弱 `'target'`、无一处声称 `'verified'`。
|
||
`verifyAccessTokenEdge`(`proxy.ts:34-67`)以 HMAC 重算比对,`alg:none` 无法伪造;`color-contrast.ts:21-28` WCAG sRGB 亮度与 0.03928 阈值正确;`use-reduced-motion.ts`、`animated-counter.tsx:59`、`stats-showcase.tsx:48` 等 observer/listener 清理齐备;`theme-toggle.tsx` 是 SSR 安全实现范本;`static-link.tsx` 外链 `rel="noopener noreferrer"` 正确;`footer.tsx:195` 暗色对比度实测 **7.54:1**(审计 P1-2 确已修复);`webpackBuildWorker` 未出现在 `experimental`(符合项目铁律)。
|
||
Jest 侧无 `.only` / `.skip` / `.todo` / `xit` / 盲写快照;`playwright.config.ts` `forbidOnly:!!CI`、`retries: CI?2:0` 配置正确,仅 2 处合理的按浏览器条件跳过。
|
||
**性能预算实测达标且余量充足(7/7 页,desktop preset,`lighthouserc.json` 全部 URL)**:performance **99-100**、FCP **246-250ms**(预算 ≤2000)、LCP **790-896ms**(预算 ≤3000)、CLS **0.000**(预算 ≤0.1)、TBT **0ms**(预算 ≤300)、SI **248-414ms**(预算 ≤3000)、best-practices **100**、SEO **100**。`lighthouserc.json` 的 6 项性能断言 + 4 项分类断言**零违规**,且本分支未引入性能退化 —— 这是少数几个「AGENTS.md §5 声称的门禁经实测确实成立」的项。故上表把 Lighthouse 判为「门禁太松」仅指 **accessibility 子项的 0.9 阈值容下了 critical 失败**(A-11-2),性能维度本身可信。
|
||
|
||
## 7. 复核中主动撤回的判断(保持报告可信)
|
||
|
||
1. ~~「首页服务区忽略 CMS,恒渲染兜底数据」~~ —— 我据 `content-types.ts:88` 的 `serviceFields` 未声明 `subtitle/highlights/href` 推断该假设,随后以三重证据否证:`prisma/dev.db` 的 service 行实测**含** `subtitle/href/highlights(4)/metrics`,且首页 HTML 中 CMS 文案(「战略咨询」「数字化成熟度评估」)命中 3/2/2 次、兜底专属串(「Strategy Consulting」「数字化转型战略咨询」)命中 **0** 次。CMS 通路正常。
|
||
2. ~~「seed 指标缺 `basis`,违反结构强制」~~ —— `content-types.ts:8-18` 的 `metricBasisField.description` 明示「留空按目标口径处理并自动标注」,`metrics-basis.ts:33-38` 亦为刻意保守回落。这是设计而非缺陷。
|
||
3. ~~「`prisma/dev.db` 与 schema 漂移,缺 RBAC 表」~~ —— 我误查了仓库根的**陈旧残留** `./dev.db`(Jul 6) 与 0 字节 `./data.db`;真实库 `.env:8` 指向 `prisma/dev.db`,RBAC 四表与 5 个 migration 俱在。
|
||
4. ~~「`Novalon 创始团队` 是品牌一致性回归」~~(6 例 E2E 失败的定性)—— `CONTEXT.md:194`(✅ 2026-08-31 确认)在「零编造」条款下**明确批准** FounderQuote 兜底署名「Novalon 创始团队」,`prisma/seed.ts:961`、`home-content-v15.tsx:115` 与单测 `home-content-v15.test.tsx:181` 一致。故 `p1-brand-visual-audit.spec.ts:30` 的 `FORBIDDEN_TEXT=/novalon/i` 前提已过期,应改测试而非改文案。
|
||
5. `stats-bar.tsx:88` `parseInt("99.9")→99`(99.9% 永久显示 99%)与 `:71` 运行期插值类 `lg:grid-cols-${items.length}`(Tailwind 不生成,布局静默停在 `md:grid-cols-3`)、`animated-counter.tsx:62` 先显终值再跳 0 起算 —— 缺陷成立但**当前不可达**(组件经无 importer 的死汇总桶暴露,§5 死代码清单);列为修复时必须一并删除的死码,而非线上问题。`animated-counter.test.tsx:5-12` mock `useCountUp→500` 且 `value={500}`,两分支同值故永不能观测此问题。
|
||
6. ~~「两份 jest 配置测试发现不一致,变异门禁只看到 42% 的用例」~~ —— 我据 Stryker dry-run 报 "Ran 668 tests"(而 `test:unit` 为 1594)提出该怀疑,实测 `npx jest --config config/test/jest.config.js` 得 **132 套件 / 1594 例全通过**,两配置 `testMatch`/`roots`/`setupFilesAfterEnv` 完全一致 ⇒ 不成立。同时纠正了对 Stryker 报告的误读:其 "All tests" 树中的 **✘ 标记含义是「该测试未覆盖当前变异体」(后缀 `(covered 0)`),不是测试失败**(✓=killed、~=covered 但未杀)。真实差异只有阈值数值不同,已归入 A-7。
|
||
7. ~~「Stryker 报告的 3 个存活变异体全为测试缺陷」~~ —— 其中 `utils.ts:25` `if (timeout)`→`if (true)` 因 `clearTimeout(null)` 本身是 no-op 而**语义等价**,存活属正常。仅 A-13、A-14 两项计为缺陷。
|
||
|
||
## 8. 修复优先级与放行条件
|
||
|
||
| 顺序 | 动作 | 理由 |
|
||
|---|---|---|
|
||
| 1 | A-1 + A-2 角色 allowlist 与改密鉴权;A-3 渲染端净化;A-4 上传白名单 + `/uploads/` 头 | 生产可利用,且 A-1→A-3/A-4 构成完整提权-存储 XSS 链 |
|
||
| 2 | A-5 摘掉 Jenkins `|| echo` 并补齐缺失 stage;A-10 E2E 改打构建产物 | 先让门禁**可信**,否则后续一切绿灯无意义 |
|
||
| 3 | **A-12 + R-3 + B-9 一并处理:67 处 `text-[var(--color-brand)]` → `text-brand-ink`,并新增 grep 门禁禁该模式** | 全站 62 页的确定性合规失败,且已有明文契约(DESIGN.md:154/232)背书;逐点修必然漏 |
|
||
| 4 | R-1 双安全区;R-2 动效回到 280ms;R-5 `MotionConfig`;B-8 BentoGrid;B-10 下拉 Escape | 用户可见 / 本分支核心意图 |
|
||
| 5 | A-6 GA4 重写为真实拦截;A-9 基线在干净参照点重采 + 删 9 张近空白基线;**A-11 `check:contrast` 改为读令牌表并补暗色/alpha 组,Lighthouse a11y 断言由 0.9 改为「critical 失败数 = 0」** | 恢复测试信号有效性;否则第 3 步修完仍无守卫 |
|
||
| 6 | A-7 阈值棘轮 + 三文档统一;A-8 加真库集成层;B-1/B-5 写入校验与 `$transaction`+WAL | 结构性 |
|
||
| 7 | B-2/B-6/B-7、§4.2 其余、§5 P2、死码清理 | 常规 |
|
||
| 8 | **A-13/A-14 补 `lerp` 非零 `start` 用例与 `randomBetween` 分布断言;A-15 把 `src/lib/constants/**` 从 `stryker.config.json` 的 `mutate` 排除中移出;`test:mutation` 去 `--inPlace`** | 变异门禁是本次唯一直接度量「断言是否有内容」的手段,且已实测可在 74s 内跑完单文件,成本极低 |
|
||
|
||
**放行条件(全部满足方可验收通过)**:①§3.1/§3.2 四项 P0 安全关闭并有回归测试;②E2E 92 → 0(若按 §7-4 判定品牌测试过期,须同时修订 `CONTEXT.md` 或测试并在提交信息留痕);③Jenkins 无 `|| echo` 后连续两次全绿;④AGENTS.md §5 列出的 lighthouse / contrast / headings 至少各执行一次并留结果;⑤A-12 全站对比度关闭后,以两个引擎各复跑一次并留 axe 节点计数 = 0 的证据;⑥R-1/R-3 在真机(iPhone SE + 系统深色)截图复核。
|
||
|
||
> 注:本次性能门禁的 0 违规**不构成本项豁免**——性能维度已实测达标(§6),但 a11y 维度是「断言通过而实际失败」,二者性质不同。
|
||
|
||
## 9. 本次未覆盖(诚实标注)
|
||
|
||
- **未做真机/真浏览器人工走查**:R-1 双安全区、触摸目标 <44px 的具体元素,均由构建产物 CSS、axe 节点选择器与 HTML 计数推断,未经肉眼截图确认。(A-12 / B-8 例外 —— 二者已由 axe 引擎在真浏览器中直接报出节点,非推断。)
|
||
- **`npm run lighthouse`(lhci autorun)未直接执行**,原因有两条,均为**独立缺陷**:
|
||
1. `config/test/lighthouserc.json` 配 `upload.target: "temporary-public-storage"` ⇒ 一旦运行即把含站点结构的结果上传至第三方公共存储(Lighthouse 官方示例报告库),对未发布站点属外泄风险。故本次改为逐 URL 调 `npx lighthouse@13` 复现同一断言集。建议改为 `upload: {target:'filesystem'}`。
|
||
2. lighthouserc 的 `startServerCommand: "npm run start"` **与 `output:'standalone'` 不兼容** —— 本次以 `next start` 启动 :3100 时 Next 自身告警 `"next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.`。即 `package.json` 的 `start`/`preview` 与 lighthouse 的起服命令在 standalone 模式下**是半失效的**,与 §5 中 Dockerfile 仍假设静态导出同源。这解释了 Jenkins 为何从未跑 lighthouse —— 跑不起来。
|
||
- **k6 四个性能脚本未执行**(依赖为占位包),仓库内 `tests/performance/*-summary.json`(Aug 12)是手工产物而非 CI 结果。
|
||
- **变异测试仅跑了 `quick` 作用域(`src/lib/utils.ts`,34 变异体 / 74s)**;`stryker.config.json` 声明的全量作用域(`src/lib/**`、`src/hooks/**` 及 7 个组件目录)**未执行** —— 按单文件外推需数小时,且 `--inPlace` 在全量下会长时间改写整个工作树,不宜在验收轮次内冒险。故 §3.5 的 91.18% **只代表 utils.ts 一个文件**,不可作为全仓变异得分引用。
|
||
- **渗透测试 / 授权验证矩阵**:仅静态审计 A-1/A-2/B-1,未真实构造请求验证提权链可用性。
|
||
- **Lighthouse 仅测 desktop preset、每 URL 单次运行**(`lighthouserc.json` 配 `numberOfRuns: 3`);未测移动网络节流下的性能,也未做 3 次取中位以消除抖动。
|
||
- **运维观察(实测泄漏,已在本报告提交前清理)**:`scripts/utils/check-heading-hierarchy.ts:47` 与 `scripts/accessibility-test.js:31` 均以 `spawn('npm', ['run','preview'])` 起服,而 `accessibility-test.js:7` 的自述是「扫描完成后关闭服务」。实测本次跑完 `check:headings` 后,:3000 上仍残留一个 PPID=**1** 的 `next-server (v16.3.0)`(父为已 orphan 的 `npm run preview`)—— 因为脚本终止的是 `npm` 包装进程,真正 LISTEN 的 `next-server` 孙进程被 init 收养而继续占端口,且存活 **39 分钟**。这不是环境噪声而是**门禁脚本的进程回收缺陷**:它与 A-10 的 `reuseExistingServer: true` 叠加后,会让后续 E2E / Lighthouse 静默复用一个陈旧构建的服务,从而使「测的是当前代码」这一前提失效。
|
||
- 附带发现:`CLAUDE.md:15` 称 `npm run preview` 是「Serve dist/ on port 3000 (npx serve)」,实际 `package.json` 为 `next start -p 3000`;`CLAUDE.md:197` 称 Playwright「Auto-starts `npm run preview`」,实际 `playwright.config.ts:130` 用 `npm run dev`。两处文档与实现相反,属 §5 配置/文档矛盾族。
|
||
- 仍有 2 处歧义未能闭合:`mega-dropdown` 在 `md:`(768px) 的 `w-[640px]` 是否于真实平板宽度溢出;`useFocusTrap` 的 `offsetParent` 过滤对 `position:fixed` 抽屉是否如预期工作。
|