完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)
这个提交包含在:
@@ -0,0 +1,310 @@
|
||||
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>${routes.map((route) => `<Route path="${route}" element={<div />} />`).join("")}</Routes>; }\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);
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户