From dc83408ec270814ed1ae0a76a5d7a069267ba57d Mon Sep 17 00:00:00 2001 From: suyx Date: Mon, 3 Aug 2026 19:26:39 +0800 Subject: [PATCH] test: validate WP5 task lineage for WP4-07 gate --- scripts/lib/wp4-07-gate.mjs | 55 +++++++++++++++++-- scripts/run-wp4-07-layer.mjs | 6 +- scripts/run-wp4-07-validation.mjs | 7 ++- tests/visual-performance/wp4-07-gate.test.mjs | 31 +++++++++++ 4 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 tests/visual-performance/wp4-07-gate.test.mjs diff --git a/scripts/lib/wp4-07-gate.mjs b/scripts/lib/wp4-07-gate.mjs index 86fd8f3..f728d37 100644 --- a/scripts/lib/wp4-07-gate.mjs +++ b/scripts/lib/wp4-07-gate.mjs @@ -4,9 +4,10 @@ import { existsSync, readFileSync } from "node:fs"; import { WP4_07_RED_RESOURCE_VERSION, WP4_07_SOURCE_HASHES, assertWp407Fixture } from "../../tests/visual-performance/wp4-07-fixture.mjs"; -export const WP4_07_REQUIRED_WP5_BRANCHES = Object.freeze( - Array.from({ length: 7 }, (_, index) => `codex/wp5-0${index + 1}`), +export const WP4_07_REQUIRED_WP5_TASKS = Object.freeze( + Array.from({ length: 7 }, (_, index) => `TASK-WP5-0${index + 1}`), ); +export const WP4_07_FINAL_WP5_BRANCH = "codex/wp5-07"; export function validateWp407FrozenInputs() { const mismatches = []; @@ -26,6 +27,28 @@ export function validateWp407FrozenInputs() { return assertWp407Fixture(); } +export function inspectWp5TaskLineage(heads, commits) { + const candidate_branch = [...WP4_07_REQUIRED_WP5_TASKS] + .reverse() + .map((taskId) => taskId.replace("TASK-WP5-", "codex/wp5-")) + .find((branch) => /^[0-9a-f]{40}$/.test(heads[branch] ?? "")) ?? null; + const task_shas = Object.fromEntries(WP4_07_REQUIRED_WP5_TASKS.flatMap((taskId) => { + const commit = commits.find((entry) => entry.subject.includes(taskId)); + return commit && /^[0-9a-f]{40}$/.test(commit.sha) ? [[taskId, commit.sha]] : []; + })); + const missing_tasks = WP4_07_REQUIRED_WP5_TASKS.filter((taskId) => !task_shas[taskId]); + return { + candidate_baseline_branch: candidate_branch, + candidate_baseline_sha: candidate_branch ? heads[candidate_branch] : null, + complete: candidate_branch === WP4_07_FINAL_WP5_BRANCH && missing_tasks.length === 0, + final_branch_sha: heads[WP4_07_FINAL_WP5_BRANCH] ?? null, + missing_tasks, + required_final_branch: WP4_07_FINAL_WP5_BRANCH, + required_tasks: WP4_07_REQUIRED_WP5_TASKS, + task_shas, + }; +} + export function readWp5RemoteGate() { const result = spawnSync("git", ["ls-remote", "--heads", "origin", "codex/wp5-*"], { encoding: "utf8", timeout: 30_000 }); if ((result.status ?? 1) !== 0) { @@ -37,12 +60,32 @@ export function readWp5RemoteGate() { const [sha, reference] = line.split(/\s+/); return [reference.replace("refs/heads/", ""), sha]; })); - const missing_branches = WP4_07_REQUIRED_WP5_BRANCHES.filter((branch) => !/^[0-9a-f]{40}$/.test(heads[branch] ?? "")); + const candidateBranch = [...WP4_07_REQUIRED_WP5_TASKS] + .reverse() + .map((taskId) => taskId.replace("TASK-WP5-", "codex/wp5-")) + .find((branch) => /^[0-9a-f]{40}$/.test(heads[branch] ?? "")); + let commits = []; + if (candidateBranch) { + const fetch = spawnSync("git", ["fetch", "--quiet", "--no-tags", "origin", `refs/heads/${candidateBranch}`], { encoding: "utf8", timeout: 60_000 }); + if ((fetch.status ?? 1) !== 0) { + const error = new Error("WP4_07_GITEA_BASELINE_FETCH_FAILED"); + error.details = { branch: candidateBranch, exit_code: fetch.status ?? 1 }; + throw error; + } + const log = spawnSync("git", ["log", "--format=%H%x09%s", heads[candidateBranch]], { encoding: "utf8", timeout: 30_000 }); + if ((log.status ?? 1) !== 0) { + const error = new Error("WP4_07_GITEA_BASELINE_HISTORY_UNREADABLE"); + error.details = { branch: candidateBranch, exit_code: log.status ?? 1 }; + throw error; + } + commits = log.stdout.trim().split(/\r?\n/).filter(Boolean).map((line) => { + const separator = line.indexOf("\t"); + return { sha: line.slice(0, separator), subject: line.slice(separator + 1) }; + }); + } return { - complete: missing_branches.length === 0, heads, - missing_branches, - required_branches: WP4_07_REQUIRED_WP5_BRANCHES, + ...inspectWp5TaskLineage(heads, commits), }; } diff --git a/scripts/run-wp4-07-layer.mjs b/scripts/run-wp4-07-layer.mjs index 700aebc..7fe0544 100644 --- a/scripts/run-wp4-07-layer.mjs +++ b/scripts/run-wp4-07-layer.mjs @@ -26,8 +26,12 @@ try { code: "WP4_07_WP5_GATE_INCOMPLETE", fixture_sha256: fixture.fixture_sha256, layer, - missing_remote_branches: gate.missing_branches, + candidate_baseline_branch: gate.candidate_baseline_branch, + candidate_baseline_sha: gate.candidate_baseline_sha, + missing_remote_tasks: gate.missing_tasks, observed_remote_heads: gate.heads, + observed_task_shas: gate.task_shas, + required_final_branch: gate.required_final_branch, status: "red", }, null, 2)); process.exit(1); diff --git a/scripts/run-wp4-07-validation.mjs b/scripts/run-wp4-07-validation.mjs index b4f293b..fa39642 100644 --- a/scripts/run-wp4-07-validation.mjs +++ b/scripts/run-wp4-07-validation.mjs @@ -68,7 +68,7 @@ function runDirect(name, executable, args, expected) { const commands = phase === "red" ? [ - run("fixture-contract", "node --test tests/visual-performance/wp4-07-fixture.test.mjs", "zero"), + run("fixture-contract", "node --test tests/visual-performance/wp4-07-fixture.test.mjs tests/visual-performance/wp4-07-gate.test.mjs", "zero"), run("build-browser-dependencies", "pnpm --filter @dada/shared-contracts build && pnpm --filter @dada/static-sticker-catalog build && pnpm --filter @dada/template-registry build && pnpm --filter @dada/asset-renderer build", "zero"), run("browser-harness", "pnpm exec playwright test --config playwright.wp4-07.config.ts", "zero"), runDirect("visual-diff", process.execPath, ["scripts/compare-wp4-07-screenshots.mjs", "--evidence", visualDirectory, "--phase", "red"], "zero"), @@ -110,8 +110,11 @@ const observation = { eligible_for_green: false, expected_failure: "Final WP-5 task SHAs, immutable release inputs, real fonts, and the final renderer are unavailable, so Chrome/Edge visual and performance results cannot become Green.", fixture_sha256: fixture.fixture_sha256, - missing_remote_branches: remoteGate.missing_branches, + candidate_baseline_branch: remoteGate.candidate_baseline_branch, + candidate_baseline_sha: remoteGate.candidate_baseline_sha, + missing_remote_tasks: remoteGate.missing_tasks, observed_remote_heads: remoteGate.heads, + observed_task_shas: remoteGate.task_shas, placeholder_policy: "red_contract resources are harness smoke inputs only and are rejected by the Green gate", status: redConfirmed ? "red_confirmed" : "failed", }; diff --git a/tests/visual-performance/wp4-07-gate.test.mjs b/tests/visual-performance/wp4-07-gate.test.mjs new file mode 100644 index 0000000..056d27f --- /dev/null +++ b/tests/visual-performance/wp4-07-gate.test.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { inspectWp5TaskLineage } from "../../scripts/lib/wp4-07-gate.mjs"; + +const sha = (digit) => digit.repeat(40); +const commit = (index) => ({ sha: sha(String(index)), subject: `feat: complete TASK-WP5-0${index}` }); + +test("WP5 lineage recognizes tasks already merged into later remote heads", () => { + const gate = inspectWp5TaskLineage({ + "codex/wp5-03": sha("3"), + "codex/wp5-04": sha("4"), + }, [commit(4), commit(3), commit(2), commit(1)]); + + assert.equal(gate.complete, false); + assert.equal(gate.candidate_baseline_branch, "codex/wp5-04"); + assert.deepEqual(gate.missing_tasks, ["TASK-WP5-05", "TASK-WP5-06", "TASK-WP5-07"]); + assert.equal(gate.task_shas["TASK-WP5-01"], sha("1")); + assert.equal(gate.task_shas["TASK-WP5-02"], sha("2")); +}); + +test("WP5 lineage becomes complete only at a wp5-07 head containing every task", () => { + const heads = { "codex/wp5-07": sha("7") }; + const complete = inspectWp5TaskLineage(heads, Array.from({ length: 7 }, (_, index) => commit(7 - index))); + const incomplete = inspectWp5TaskLineage(heads, [commit(7), commit(6), commit(5), commit(4), commit(3), commit(2)]); + + assert.equal(complete.complete, true); + assert.deepEqual(complete.missing_tasks, []); + assert.equal(incomplete.complete, false); + assert.deepEqual(incomplete.missing_tasks, ["TASK-WP5-01"]); +});