diff --git a/frontend/scripts/defect-ledger-regression.mjs b/frontend/scripts/defect-ledger-regression.mjs index 2f97b043..f824b607 100644 --- a/frontend/scripts/defect-ledger-regression.mjs +++ b/frontend/scripts/defect-ledger-regression.mjs @@ -18,6 +18,26 @@ const artifactDir = fs.mkdirSync(artifactDir, { recursive: true }); +const FORMAL_DEFECTS = [ + { id: 1, title: "管理员初始化后仍无法登录" }, + { id: 2, title: "member 无法下载自己生成的合同" }, + { id: 3, title: "审查详情页自动触发 401 报告下载" }, + { id: 4, title: "审查列表页下载报告不触发真实下载" }, + { id: 5, title: "无效 review 详情缺少清晰 404 空态" }, + { id: 6, title: "无效 contract 详情缺少清晰 404 空态" }, + { id: 7, title: "前端仍接受 PNG 进入已选文件状态" }, + { id: 8, title: "Chat 页面出现矛盾的未返回回答提示" }, + { id: 9, title: "知识库旧入口路由与权限预期不一致" }, + { id: 10, title: "PDF 噪声内容被当作有效审查文本" }, + { id: 11, title: "错误登录提示仍为英文" }, + { id: 12, title: "登录页暴露默认管理员初始化入口" }, + { id: 13, title: "member 误入仅管理员文件中心" }, + { id: 14, title: "中文按钮文本被自动插空格" }, + { id: 15, title: "PNG 异常文件可直接提交审查" }, + { id: 16, title: "EXE 异常文件可直接提交审查" }, + { id: 17, title: "慢网抖动期间提交按钮未锁定" }, +]; + const results = []; const state = { api: null, @@ -44,12 +64,24 @@ function assert(condition, message, details = undefined) { } } -async function step(id, title, fn) { +function buildCoverage(resultsList) { + return FORMAL_DEFECTS.map((defect) => { + const linkedSteps = resultsList.filter((item) => Array.isArray(item.covers) && item.covers.includes(defect.id)); + return { + id: defect.id, + title: defect.title, + step_ids: linkedSteps.map((item) => item.id), + status: linkedSteps.length > 0 && linkedSteps.every((item) => item.status === "PASS") ? "PASS" : "FAIL", + }; + }); +} + +async function step(id, title, covers, fn) { const startedAt = new Date().toISOString(); const started = Date.now(); try { const details = await fn(); - const item = { id, title, status: "PASS", duration_ms: Date.now() - started, started_at: startedAt, details: details || {} }; + const item = { id, title, covers, status: "PASS", duration_ms: Date.now() - started, started_at: startedAt, details: details || {} }; results.push(item); console.log(`PASS ${id} ${title}`); return item; @@ -57,6 +89,7 @@ async function step(id, title, fn) { const item = { id, title, + covers, status: "FAIL", duration_ms: Date.now() - started, started_at: startedAt, @@ -158,7 +191,7 @@ async function run() { state.api = await request.newContext(); state.browser = await chromium.launch({ headless: true }); - await step("ENV-001", "frontend and backend health", async () => { + await step("ENV-001", "frontend and backend health", [], async () => { const frontend = await state.api.get(`${webBase}/login`); const health = await state.api.get(healthUrl); assert(frontend.status() === 200, `frontend /login status ${frontend.status()}`); @@ -167,7 +200,7 @@ async function run() { return { webBase, apiBase, healthUrl }; }); - await step("AUTH-UX-001", "login page hides seed admin and shows chinese error", async () => { + await step("AUTH-UX-001", "login page hides seed admin and shows chinese error", [1, 11, 12], async () => { const page = await state.browser.newPage({ viewport: { width: 1365, height: 900 } }); await page.goto(`${webBase}/login`, { waitUntil: "domcontentloaded" }); await page.waitForTimeout(1200); @@ -184,7 +217,7 @@ async function run() { return { screenshot: "auth-login-ux.png" }; }); - await step("ROUTE-UX-001", "invalid detail pages and legacy knowledge route are clear", async () => { + await step("ROUTE-UX-001", "invalid detail pages and legacy knowledge route are clear", [5, 6, 9], async () => { const context = await createAuthenticatedContext(state.admin); const page = await context.newPage(); await page.goto(`${webBase}/reviews/not-a-real-id`, { waitUntil: "domcontentloaded" }); @@ -203,7 +236,7 @@ async function run() { return { screenshot: "route-ux.png", finalUrl: page.url() }; }); - await step("REVIEW-DOWNLOAD-001", "review detail does not auto-401 and list download works", async () => { + await step("REVIEW-DOWNLOAD-001", "review detail does not auto-401 and list download works", [3, 4], async () => { const reviewId = await createReview(); const context = await createAuthenticatedContext(state.admin); const page = await context.newPage(); @@ -230,7 +263,7 @@ async function run() { return { screenshot: "review-download.png", file }; }); - await step("CHAT-STATE-001", "chat page does not show contradictory empty-answer copy", async () => { + await step("CHAT-STATE-001", "chat page does not show contradictory empty-answer copy", [8], async () => { const reviewPayload = { reviewId: state.reviewId, prompt: "请总结当前审查结果,并给出最值得优先处理的修改建议。", @@ -257,7 +290,54 @@ async function run() { return { screenshot: "chat-state.png" }; }); - await step("UPLOAD-GUARD-001", "invalid upload formats are rejected and duplicate submit is locked", async () => { + await step("PDF-NOISE-001", "noisy pdf content is rejected instead of creating a fake review result", [10], async () => { + const noisyPdf = Buffer.from( + [ + "%PDF-1.4", + "1 0 obj", + "<< /Type /Catalog /Pages 2 0 R >>", + "endobj", + "2 0 obj", + "<< /Type /Pages /Count 1 /Kids [3 0 R] >>", + "endobj", + "3 0 obj", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>", + "endobj", + "4 0 obj", + "<< /Length 44 >>", + "stream", + "BT /F1 12 Tf 72 720 Td (Catalog Pages Type Parent) Tj ET", + "endstream", + "endobj", + "trailer", + "<< /Root 1 0 R >>", + "startxref", + "0", + "%%EOF", + ].join("\n"), + "utf8", + ); + const response = await apiRequest("post", "/reviews", { + authToken: state.admin.access_token, + multipart: { + review_as: "party_a", + file: { name: `${runId}-noise.pdf`, mimeType: "application/pdf", buffer: noisyPdf }, + }, + }); + assert(response.response.status() === 200, "noisy pdf business rejection should still return structured response", { + status: response.response.status(), + body: response.body, + }); + assert(response.body?.code === 40002, "noisy pdf should return 40002", { body: response.body }); + assert( + String(response.body?.message || "").includes("无法从上传文件中提取可审查文本"), + "noisy pdf should return extraction guidance", + { body: response.body }, + ); + return { http_status: response.response.status(), body: response.body }; + }); + + await step("UPLOAD-GUARD-001", "invalid upload formats are rejected and duplicate submit is locked", [7, 15, 16, 17], async () => { const context = await createAuthenticatedContext(state.admin); const page = await context.newPage(); await page.goto(`${webBase}/review`, { waitUntil: "domcontentloaded" }); @@ -312,7 +392,7 @@ async function run() { return { screenshot: "upload-guard.png", reviewRequestCount, disabledAfterClick }; }); - await step("MEMBER-CONTRACT-001", "member can download own contract and does not see file-center trap", async () => { + await step("MEMBER-CONTRACT-001", "member can download own contract and does not see file-center trap", [2, 13], async () => { const contractId = await createMemberContract(); const apiDownload = await apiRequest("get", `/contracts/${contractId}/download`, { authToken: state.member.access_token, @@ -343,7 +423,7 @@ async function run() { return { screenshot: "member-contract.png", file }; }); - await step("TEXT-UX-001", "button labels do not expose split chinese spacing", async () => { + await step("TEXT-UX-001", "button labels do not expose split chinese spacing", [14], async () => { const context = await createAuthenticatedContext(state.admin); const page = await context.newPage(); await page.goto(`${webBase}/contracts`, { waitUntil: "domcontentloaded" }); @@ -361,6 +441,19 @@ async function run() { return { screenshot: "text-ux.png" }; }); + await step("COVERAGE-001", "all 17 formal defects are covered by passing regression checks", [], async () => { + const coverage = buildCoverage(results); + const missing = coverage.filter((item) => item.status !== "PASS"); + assert(missing.length === 0, "formal defect coverage is incomplete", { missing, coverage }); + return { + total_defects: coverage.length, + passed_defects: coverage.filter((item) => item.status === "PASS").length, + coverage, + }; + }); + + const defectCoverage = buildCoverage(results); + const summary = { run_id: runId, web_base: webBase, @@ -370,6 +463,9 @@ async function run() { total: results.length, passed: results.filter((item) => item.status === "PASS").length, failed: results.filter((item) => item.status === "FAIL").length, + formal_defects_total: FORMAL_DEFECTS.length, + formal_defects_passed: defectCoverage.filter((item) => item.status === "PASS").length, + defect_coverage: defectCoverage, results, }; fs.writeFileSync(path.join(artifactDir, "defect-ledger-regression-summary.json"), JSON.stringify(summary, null, 2), "utf8"); @@ -382,10 +478,19 @@ async function run() { `- Web: \`${webBase}\``, `- API: \`${apiBase}\``, `- Passed: ${summary.passed}/${summary.total}`, + `- Formal Defects Passed: ${summary.formal_defects_passed}/${summary.formal_defects_total}`, "", "| ID | Status | Title |", "|---|---|---|", ...results.map((item) => `| ${item.id} | ${item.status} | ${item.title.replace(/\|/g, "/")} |`), + "", + "## Formal Defect Coverage", + "", + "| Defect | Status | Covered By | Title |", + "|---|---|---|---|", + ...defectCoverage.map( + (item) => `| ${item.id} | ${item.status} | ${(item.step_ids.join(", ") || "N/A").replace(/\|/g, "/")} | ${item.title.replace(/\|/g, "/")} |`, + ), ].join("\n"), "utf8", ); @@ -408,6 +513,8 @@ run().catch(async (error) => { total: results.length + 1, passed: results.filter((item) => item.status === "PASS").length, failed: results.filter((item) => item.status === "FAIL").length + 1, + formal_defects_total: FORMAL_DEFECTS.length, + defect_coverage: buildCoverage(results), results: [ ...results, { diff --git a/project-docs/defect_ledger修复记录-2026-05-03.md b/project-docs/defect_ledger修复记录-2026-05-03.md index 723865a3..b0accfbd 100644 --- a/project-docs/defect_ledger修复记录-2026-05-03.md +++ b/project-docs/defect_ledger修复记录-2026-05-03.md @@ -61,11 +61,35 @@ ### 部署环境回归 - `frontend/scripts/defect-ledger-regression.mjs` - - 结果:`8/8 PASS` + - 已补强为“17 条正式缺陷覆盖矩阵”版本 + - 结果:`10/10 PASS` + - 正式缺陷覆盖:`17/17 PASS` + - 产物:`project-docs/qa-artifacts/defect-ledger-regression/2026-05-03T04-38-01-630Z` - `frontend/scripts/phase1-closed-loop-e2e.mjs` - 结果:`18/18 PASS` + - 产物:`project-docs/qa-artifacts/phase1-closed-loop/2026-05-03T04-33-14-184Z` - `frontend/scripts/phase2-closed-loop-e2e.mjs` - 结果:`7/7 PASS` + - 产物:`project-docs/qa-artifacts/phase2-closed-loop/2026-05-03T04-33-14-217Z` +- `frontend/scripts/phase3-closed-loop-e2e.mjs` + - 结果:`5/5 PASS` + - 产物:`project-docs/qa-artifacts/phase3-closed-loop/2026-05-03T04-38-01-644Z` + +### 正式缺陷覆盖矩阵 + +本轮将 `defect_ledger.md` 的 17 条正式缺陷逐条映射到自动化场景,并在回归结果中输出覆盖表,避免“场景通过但无法证明缺陷无遗漏”的问题。 + +| 回归场景 | 覆盖缺陷 | +|---|---| +| `AUTH-UX-001` | #1, #11, #12 | +| `ROUTE-UX-001` | #5, #6, #9 | +| `REVIEW-DOWNLOAD-001` | #3, #4 | +| `CHAT-STATE-001` | #8 | +| `PDF-NOISE-001` | #10 | +| `UPLOAD-GUARD-001` | #7, #15, #16, #17 | +| `MEMBER-CONTRACT-001` | #2, #13 | +| `TEXT-UX-001` | #14 | +| `COVERAGE-001` | 校验 17 条正式缺陷是否全部被通过中的场景覆盖 | ### PDF 运行态验证 @@ -79,4 +103,4 @@ ## 结论 -当前 `defect_ledger.md` 中的正式 BUG 已完成对照修复,并通过代码级验证和部署环境自动化回归验证。 +当前 `defect_ledger.md` 中的 17 条正式 BUG 已完成对照修复,并通过代码级验证、17 条缺陷覆盖矩阵回归、Phase 1 / 2 / 3 主链路回归验证。