import assert from "node:assert/strict"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { buildReport, hasBlockingGaps } from "../check-flow-coverage.mjs"; const WITNESS_SPEC = ` import { test } from "@playwright/test"; export const LOCAL_CANDIDATE_MANIFEST_DRIVEN_WITNESSES = true; for (const flow of localCandidateFlows) { test(flow.witnessTag, async ({ page }) => { installBrowserErrorMonitor(page); installSyntheticApis(page, flow.roleFixture); await loginToWitness(page, flow.witnessRoute); await expectUniqueH1(page, flow.expectedH1); await expectPrimaryActionContract(page, flow); }); } `; function candidateFlow(bundle = "core-flow", witnessRoute = "/core", overrides = {}) { return { bundle, witnessRoute, witnessTag: `@local-candidate:${bundle}`, expectedH1: "核心页面", roleFixture: "employee", primaryAction: { allowed: "开始", forbidden: "越权操作" }, witnessSpec: "client/tests/e2e/local-candidate-witnesses.spec.ts", ...overrides, }; } function writeFixture({ routes = ["/core"], coreBundles = ["core-flow"], manifestRoutes = [ { path: "/core", classification: "core", bundle: "core-flow", behaviorSpecs: ["client/tests/e2e/core.spec.ts"], }, ], localCandidateFlows = [candidateFlow()], acceptanceSpecs = ["client/tests/e2e/acceptance.spec.ts"], specs = {}, } = {}) { const root = mkdtempSync(path.join(tmpdir(), "hyb-flow-coverage-")); const routerPath = path.join(root, "client/src/router/index.tsx"); const manifestPath = path.join(root, "scripts/qa/route-manifest.json"); mkdirSync(path.dirname(routerPath), { recursive: true }); mkdirSync(path.dirname(manifestPath), { recursive: true }); writeFileSync( routerPath, `export function RouterFixture() { return ${routes.map((route) => `} />`).join("")}; }\n`, "utf8", ); writeFileSync( manifestPath, `${JSON.stringify({ schemaVersion: 1, coreBundles, acceptanceSpecs, localCandidateFlows, routes: manifestRoutes, }, null, 2)}\n`, "utf8", ); const defaultSpecs = { "client/tests/e2e/core.spec.ts": 'import { test } from "@playwright/test";\ntest("core", async ({ page }) => { await page.goto("/core"); });\n', "client/tests/e2e/acceptance.spec.ts": 'import { test } from "@playwright/test";\ntest("acceptance", async ({ page }) => { await page.goto("/core"); });\n', "client/tests/e2e/local-candidate-witnesses.spec.ts": WITNESS_SPEC, ...specs, }; for (const [relativePath, source] of Object.entries(defaultSpecs)) { if (source == null) continue; const file = path.join(root, relativePath); mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, source, "utf8"); } return root; } function runFixture(root) { const report = buildReport(root); return { status: hasBlockingGaps(report) ? 1 : 0, report }; } function withFixture(options, callback) { const root = writeFixture(options); try { callback(root); } finally { rmSync(root, { recursive: true, force: true }); } } test("完整 manifest、acceptance spec 与可信 witness 通过", () => { withFixture({}, (root) => { const result = runFixture(root); assert.equal(result.status, 0); assert.equal(result.report.summary.blockingGapCount, 0); assert.equal(result.report.summary.coveredCoreBundleCount, 1); assert.equal(result.report.summary.coveredWitnessCount, 1); assert.equal(result.report.summary.validAcceptanceSpecCount, 1); }); }); test("未分类注册路由必须阻断", () => { withFixture({ routes: ["/core", "/unmapped"] }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.unclassified, 1); }); }); test("403 与 wildcard 注册路由不能从分类扫描中豁免", () => { withFixture({ routes: ["/core", "/403", "*"] }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.routeCount, 3); assert.equal(result.report.summary.gapCounts.unclassified, 2); }); }); test("manual 或 live spec 不能充当核心行为证据", () => { withFixture({ manifestRoutes: [ { path: "/core", classification: "core", bundle: "core-flow", behaviorSpecs: ["client/tests/e2e/manual/core.spec.ts"], }, ], specs: { "client/tests/e2e/manual/core.spec.ts": 'import { test } from "@playwright/test";\ntest("manual", async ({ page }) => { await page.goto("/core"); });\n', }, }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.manual_only, 1); }); }); test("核心路由缺失行为测试必须阻断", () => { withFixture({ manifestRoutes: [{ path: "/core", classification: "core", bundle: "core-flow" }], }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.missing_behavior, 1); }); }); test("visualRequired 缺失可执行视觉 spec 必须阻断", () => { withFixture({ manifestRoutes: [ { path: "/core", classification: "core", bundle: "core-flow", behaviorSpecs: ["client/tests/e2e/core.spec.ts"], visualRequired: true, }, ], }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.missing_visual, 1); }); }); test("核心 bundle 缺失 witness 必须阻断", () => { withFixture({ localCandidateFlows: [] }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.missing_local_candidate_witness, 1); }); }); test("重复 bundle 与重复 witness tag 必须阻断", () => { withFixture({ localCandidateFlows: [candidateFlow(), candidateFlow()] }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.duplicate_local_candidate_bundle, 1); assert.equal(result.report.summary.gapCounts.duplicate_witness_tag, 1); }); }); test("未知 local-candidate bundle 必须阻断", () => { withFixture({ localCandidateFlows: [candidateFlow(), candidateFlow("unknown-flow", "/core")], }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.unknown_local_candidate_bundle, 1); }); }); test("重复 core bundle 必须阻断", () => { withFixture({ coreBundles: ["core-flow", "core-flow"] }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.duplicate_core_bundle, 1); }); }); test("orphan manifest 路由必须阻断", () => { withFixture({ manifestRoutes: [ { path: "/core", classification: "core", bundle: "core-flow", behaviorSpecs: ["client/tests/e2e/core.spec.ts"], }, { path: "/orphan", classification: "supported-noncore" }, ], }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.orphan_manifest_route, 1); }); }); test("witnessRoute 指向未注册路由必须阻断", () => { withFixture({ localCandidateFlows: [candidateFlow("core-flow", "/missing")] }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.orphan_witness_route, 1); }); }); test("witnessRoute 绑定到其他 bundle 必须阻断", () => { const otherSpec = 'import { test } from "@playwright/test";\ntest("other", async ({ page }) => { await page.goto("/other"); });\n'; withFixture({ routes: ["/core", "/other"], coreBundles: ["core-flow", "other-flow"], manifestRoutes: [ { path: "/core", classification: "core", bundle: "core-flow", behaviorSpecs: ["client/tests/e2e/core.spec.ts"], }, { path: "/other", classification: "core", bundle: "other-flow", behaviorSpecs: ["client/tests/e2e/other.spec.ts"], }, ], localCandidateFlows: [candidateFlow("core-flow", "/other"), candidateFlow("other-flow", "/other")], specs: { "client/tests/e2e/other.spec.ts": otherSpec }, }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.wrong_witness_association, 1); }); }); test("witness 字段不完整或 tag 不稳定必须阻断", () => { withFixture({ localCandidateFlows: [candidateFlow("core-flow", "/core", { witnessTag: "@local-candidate:wrong", expectedH1: "", roleFixture: "unknown", primaryAction: { allowed: "相同", forbidden: "相同" }, })], }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.invalid_witness_tag, 1); assert.equal(result.report.summary.gapCounts.missing_expected_h1, 1); assert.equal(result.report.summary.gapCounts.invalid_role_fixture, 1); assert.equal(result.report.summary.gapCounts.invalid_primary_action, 1); }); }); test("manual/live acceptance spec 必须阻断", () => { withFixture({ acceptanceSpecs: ["client/tests/e2e/manual/acceptance.spec.ts"], specs: { "client/tests/e2e/manual/acceptance.spec.ts": 'import { test } from "@playwright/test";\ntest("manual", async () => {});\n', }, }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.equal(result.report.summary.gapCounts.manual_or_live_acceptance_spec, 1); }); }); test("候选 spec 不同路径使用同一 basename 必须阻断", () => { withFixture({ acceptanceSpecs: ["client/tests/e2e/nested/local-candidate-witnesses.spec.ts"], specs: { "client/tests/e2e/nested/local-candidate-witnesses.spec.ts": 'import { test } from "@playwright/test";\ntest("acceptance", async ({ page }) => { await page.goto("/core"); });\n', }, }, (root) => { const result = runFixture(root); assert.equal(result.status, 1); assert.deepEqual(result.report.duplicateCandidateSpecBasenames, ["local-candidate-witnesses.spec.ts"]); assert.equal(result.report.summary.gapCounts.duplicate_candidate_spec_basename, 1); }); });