完成五人团队正式内用最小收口(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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import test from "node:test";
|
||||
|
||||
const repoRoot = path.resolve(new URL("../../..", import.meta.url).pathname);
|
||||
const sourceScript = path.join(repoRoot, "scripts/check-minimal.sh");
|
||||
const localCandidateRunner = path.join(repoRoot, "scripts/qa/run-local-candidate.sh");
|
||||
|
||||
function writeExecutable(file, source) {
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
writeFileSync(file, source, "utf8");
|
||||
chmodSync(file, 0o755);
|
||||
}
|
||||
|
||||
test("local-candidate 基础 profile 在没有 issues 目录时不调用历史 CSV 门禁", () => {
|
||||
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "hyb-local-candidate-profile-"));
|
||||
try {
|
||||
const scriptSource = readFileSync(sourceScript, "utf8");
|
||||
const fixtureScript = path.join(fixtureRoot, "scripts/check-minimal.sh");
|
||||
writeExecutable(fixtureScript, scriptSource);
|
||||
mkdirSync(path.join(fixtureRoot, "server"), { recursive: true });
|
||||
mkdirSync(path.join(fixtureRoot, "client"), { recursive: true });
|
||||
|
||||
for (const match of scriptSource.matchAll(/\bbash -n ([^\s]+)/gu)) {
|
||||
const relativeFile = match[1];
|
||||
for (const base of [fixtureRoot, path.join(fixtureRoot, "server")]) {
|
||||
const file = path.join(base, relativeFile);
|
||||
mkdirSync(path.dirname(file), { recursive: true });
|
||||
writeFileSync(file, "#!/usr/bin/env bash\n", "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
const binDir = path.join(fixtureRoot, "bin");
|
||||
const commandLog = path.join(fixtureRoot, "commands.log");
|
||||
writeExecutable(path.join(binDir, "node"), `#!/usr/bin/env bash
|
||||
printf 'node %s\\n' "$*" >> "$CHECK_MINIMAL_COMMAND_LOG"
|
||||
if [[ "$*" == *check-process-contract.mjs* ]]; then exit 91; fi
|
||||
exit 0
|
||||
`);
|
||||
writeExecutable(path.join(binDir, "npm"), `#!/usr/bin/env bash
|
||||
printf 'npm %s\\n' "$*" >> "$CHECK_MINIMAL_COMMAND_LOG"
|
||||
exit 0
|
||||
`);
|
||||
writeExecutable(path.join(binDir, "python3"), `#!/usr/bin/env bash
|
||||
printf 'python3 %s\\n' "$*" >> "$CHECK_MINIMAL_COMMAND_LOG"
|
||||
exit 92
|
||||
`);
|
||||
|
||||
const result = spawnSync("bash", [fixtureScript, "local-candidate"], {
|
||||
cwd: fixtureRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
||||
CHECK_MINIMAL_COMMAND_LOG: commandLog,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
|
||||
const commands = readFileSync(commandLog, "utf8");
|
||||
assert.doesNotMatch(commands, /check-process-contract|validate-feature-matrix|python3/u);
|
||||
assert.equal(existsSync(path.join(fixtureRoot, "issues")), false);
|
||||
} finally {
|
||||
rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("local-candidate 结果标签严格区分 full、focused 与 list", () => {
|
||||
const source = readFileSync(localCandidateRunner, "utf8");
|
||||
const blockStart = source.lastIndexOf('if [[ "$MODE" == "full" ]]; then');
|
||||
const blockEnd = source.indexOf("\nfi", blockStart);
|
||||
|
||||
assert.notEqual(blockStart, -1, "缺少 full/focused 结果标签分支");
|
||||
assert.notEqual(blockEnd, -1, "full/focused 结果标签分支不完整");
|
||||
const resultLabelBlock = source.slice(blockStart, blockEnd + "\nfi".length);
|
||||
|
||||
const runLabelBlock = (mode) => spawnSync("bash", ["-c", resultLabelBlock], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, MODE: mode },
|
||||
});
|
||||
const full = runLabelBlock("full");
|
||||
const focused = runLabelBlock("focused");
|
||||
|
||||
assert.equal(full.status, 0, full.stderr);
|
||||
assert.match(full.stdout, /LOCAL_CANDIDATE_PASS/u);
|
||||
assert.doesNotMatch(full.stdout, /FOCUSED_BROWSER_PASS/u);
|
||||
assert.equal(focused.status, 0, focused.stderr);
|
||||
assert.match(focused.stdout, /FOCUSED_BROWSER_PASS/u);
|
||||
assert.doesNotMatch(focused.stdout, /LOCAL_CANDIDATE_PASS/u);
|
||||
assert.equal(source.match(/LOCAL_CANDIDATE_PASS/gu)?.length, 1);
|
||||
assert.match(
|
||||
source,
|
||||
/if \[\[ "\$MODE" == "list" \]\]; then[\s\S]*?LIST_ONLY[\s\S]*?exit 0[\s\S]*?\nfi/u,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { validateLoopbackHttpUrl, validateTcpPort } from "../validate-loopback-url.mjs";
|
||||
|
||||
for (const [input, expected] of [
|
||||
["http://127.0.0.1:5178", "http://127.0.0.1:5178"],
|
||||
["http://localhost:3000/", "http://localhost:3000"],
|
||||
["http://[::1]:4173", "http://[::1]:4173"],
|
||||
]) {
|
||||
test(`accepts loopback origin ${input}`, () => {
|
||||
assert.equal(validateLoopbackHttpUrl(input), expected);
|
||||
});
|
||||
}
|
||||
|
||||
for (const input of ["1", "80", "5178", "65535"]) {
|
||||
test(`accepts canonical TCP port ${input}`, () => {
|
||||
assert.equal(validateTcpPort(input), input);
|
||||
});
|
||||
}
|
||||
|
||||
for (const input of ["", "0", "065", "65536", "5178/path", "[email protected]", "1.5", "-1"]) {
|
||||
test(`rejects ambiguous or invalid TCP port ${input || "<empty>"}`, () => {
|
||||
assert.throws(() => validateTcpPort(input));
|
||||
});
|
||||
}
|
||||
|
||||
for (const input of [
|
||||
"https://127.0.0.1:5178",
|
||||
"http://example.invalid:5178",
|
||||
"http://localhost.example.invalid:5178",
|
||||
"http://[email protected]:5178",
|
||||
"http://remote.invalid@localhost:5178",
|
||||
"http://user:[email protected]:5178",
|
||||
"http://127.0.0.1:5178/path",
|
||||
"not-a-url",
|
||||
]) {
|
||||
test(`rejects non-loopback or ambiguous target ${input}`, () => {
|
||||
assert.throws(() => validateLoopbackHttpUrl(input));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { verifyLocalCandidateResults } from "../verify-local-candidate-results.mjs";
|
||||
|
||||
const bundles = [
|
||||
"login-logout",
|
||||
"reimbursement",
|
||||
"approval",
|
||||
"invoice",
|
||||
"procurement",
|
||||
"project-contract",
|
||||
"payment-ap",
|
||||
"people-audit",
|
||||
"attendance-archive",
|
||||
"mobile-workspace",
|
||||
];
|
||||
|
||||
function manifest() {
|
||||
return {
|
||||
localCandidateFlows: bundles.map((bundle) => ({
|
||||
bundle,
|
||||
witnessTag: `@local-candidate:${bundle}`,
|
||||
witnessSpec: "client/tests/e2e/synthetic-local-candidate.spec.ts",
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function spec(tag, status = "passed", overrides = {}) {
|
||||
return {
|
||||
title: tag,
|
||||
file: "client/tests/e2e/synthetic-local-candidate.spec.ts",
|
||||
line: 12,
|
||||
tags: [],
|
||||
tests: [
|
||||
{
|
||||
expectedStatus: "passed",
|
||||
results: [{ status }],
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function results(specs, overrides = {}) {
|
||||
return {
|
||||
suites: [{ title: "local candidate", specs }],
|
||||
errors: [],
|
||||
stats: { expected: specs.length, skipped: 0, unexpected: 0 },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("10 个唯一 witness tag 全部 passed 时通过", () => {
|
||||
const summary = verifyLocalCandidateResults(
|
||||
manifest(),
|
||||
results(bundles.map((bundle) => spec(`@local-candidate:${bundle}`))),
|
||||
);
|
||||
assert.equal(summary.ok, true);
|
||||
assert.equal(summary.passedTagCount, 10);
|
||||
assert.equal(summary.skippedTagCount, 0);
|
||||
});
|
||||
|
||||
test("结果缺失 tag 必须阻断", () => {
|
||||
const summary = verifyLocalCandidateResults(
|
||||
manifest(),
|
||||
results(bundles.slice(1).map((bundle) => spec(`@local-candidate:${bundle}`))),
|
||||
);
|
||||
assert.equal(summary.ok, false);
|
||||
assert.deepEqual(summary.missingTags, ["@local-candidate:login-logout"]);
|
||||
assert.ok(summary.issues.includes("missing_tag_result"));
|
||||
});
|
||||
|
||||
test("重复 tag 结果必须阻断", () => {
|
||||
const specs = bundles.map((bundle) => spec(`@local-candidate:${bundle}`));
|
||||
specs.push(spec("@local-candidate:login-logout"));
|
||||
const summary = verifyLocalCandidateResults(manifest(), results(specs));
|
||||
assert.equal(summary.ok, false);
|
||||
assert.deepEqual(summary.duplicateTags, ["@local-candidate:login-logout"]);
|
||||
});
|
||||
|
||||
test("未知 tag 结果必须阻断", () => {
|
||||
const specs = bundles.map((bundle) => spec(`@local-candidate:${bundle}`));
|
||||
specs.push(spec("@local-candidate:unknown"));
|
||||
const summary = verifyLocalCandidateResults(manifest(), results(specs));
|
||||
assert.equal(summary.ok, false);
|
||||
assert.deepEqual(summary.unknownTags, ["@local-candidate:unknown"]);
|
||||
});
|
||||
|
||||
test("全部 witness skipped 必须阻断", () => {
|
||||
const summary = verifyLocalCandidateResults(
|
||||
manifest(),
|
||||
results(bundles.map((bundle) => spec(`@local-candidate:${bundle}`, "skipped"))),
|
||||
);
|
||||
assert.equal(summary.ok, false);
|
||||
assert.equal(summary.skippedTagCount, 10);
|
||||
assert.equal(summary.passedTagCount, 0);
|
||||
assert.ok(summary.issues.includes("skipped_tag_result"));
|
||||
});
|
||||
|
||||
test("任一 witness 或整体 Playwright suite 失败必须阻断", () => {
|
||||
const specs = bundles.map((bundle) =>
|
||||
spec(`@local-candidate:${bundle}`, bundle === "payment-ap" ? "failed" : "passed"),
|
||||
);
|
||||
const summary = verifyLocalCandidateResults(
|
||||
manifest(),
|
||||
results(specs, { errors: [{}], stats: { expected: 9, skipped: 0, unexpected: 1 } }),
|
||||
);
|
||||
assert.equal(summary.ok, false);
|
||||
assert.deepEqual(summary.failedTags, ["@local-candidate:payment-ap"]);
|
||||
assert.ok(summary.issues.includes("playwright_suite_failed"));
|
||||
assert.deepEqual(summary.failedTests, [{
|
||||
file: "synthetic-local-candidate.spec.ts",
|
||||
line: 12,
|
||||
title: "@local-candidate:payment-ap",
|
||||
status: "failed",
|
||||
}]);
|
||||
});
|
||||
|
||||
test("非 witness acceptance 用例 skipped 也必须阻断且诊断不泄露绝对路径", () => {
|
||||
const specs = bundles.map((bundle) => spec(`@local-candidate:${bundle}`));
|
||||
specs.push(spec("本地附加验收", "skipped", {
|
||||
file: "/private/workspace/client/tests/e2e/acceptance.spec.ts",
|
||||
line: 27,
|
||||
}));
|
||||
const summary = verifyLocalCandidateResults(
|
||||
manifest(),
|
||||
results(specs, { stats: { expected: 10, skipped: 1, unexpected: 0 } }),
|
||||
);
|
||||
assert.equal(summary.ok, false);
|
||||
assert.ok(summary.issues.includes("playwright_suite_skipped"));
|
||||
assert.equal(summary.suiteSkippedTestCount, 1);
|
||||
assert.deepEqual(summary.skippedTests, [{
|
||||
file: "acceptance.spec.ts",
|
||||
line: 27,
|
||||
title: "本地附加验收",
|
||||
status: "skipped",
|
||||
}]);
|
||||
});
|
||||
|
||||
test("单一测试合并多个 witness tag 不能冒充十个独立流程", () => {
|
||||
const mergedTitle = bundles.map((bundle) => `@local-candidate:${bundle}`).join(" ");
|
||||
const summary = verifyLocalCandidateResults(manifest(), results([spec(mergedTitle)]));
|
||||
assert.equal(summary.ok, false);
|
||||
assert.equal(summary.totalTestResultCount, 1);
|
||||
assert.ok(summary.issues.includes("multi_tag_test_result"));
|
||||
assert.equal(summary.multiTagTests.length, 1);
|
||||
});
|
||||
|
||||
test("witness tag 必须来自 manifest 指定的测试文件", () => {
|
||||
const specs = bundles.map((bundle) => spec(`@local-candidate:${bundle}`));
|
||||
specs[0] = spec("@local-candidate:login-logout", "passed", {
|
||||
file: "/private/workspace/client/tests/e2e/unrelated.spec.ts",
|
||||
});
|
||||
const summary = verifyLocalCandidateResults(manifest(), results(specs));
|
||||
assert.equal(summary.ok, false);
|
||||
assert.ok(summary.issues.includes("wrong_witness_spec_result"));
|
||||
assert.deepEqual(summary.wrongWitnessSpecs, [{
|
||||
tag: "@local-candidate:login-logout",
|
||||
expectedFile: "synthetic-local-candidate.spec.ts",
|
||||
observedFile: "unrelated.spec.ts",
|
||||
}]);
|
||||
});
|
||||
|
||||
test("Playwright JSON 将 file 放在父 suite 时仍能核对 witness 来源", () => {
|
||||
const specs = bundles.map((bundle) => {
|
||||
const item = spec(`@local-candidate:${bundle}`);
|
||||
delete item.file;
|
||||
return item;
|
||||
});
|
||||
const report = results([], {
|
||||
suites: [{
|
||||
title: "local candidate witnesses",
|
||||
file: "tests/e2e/synthetic-local-candidate.spec.ts",
|
||||
specs,
|
||||
}],
|
||||
});
|
||||
const summary = verifyLocalCandidateResults(manifest(), report);
|
||||
assert.equal(summary.ok, true);
|
||||
assert.deepEqual(summary.wrongWitnessSpecs, []);
|
||||
});
|
||||
|
||||
test("Playwright JSON 只提供 spec basename 时按 manifest 唯一 basename 核对", () => {
|
||||
const specs = bundles.map((bundle) => spec(`@local-candidate:${bundle}`, "passed", {
|
||||
file: "synthetic-local-candidate.spec.ts",
|
||||
}));
|
||||
const summary = verifyLocalCandidateResults(manifest(), results(specs));
|
||||
assert.equal(summary.ok, true);
|
||||
assert.deepEqual(summary.wrongWitnessSpecs, []);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户