170 lines
9.4 KiB
JavaScript
170 lines
9.4 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { spawnSync } from "node:child_process";
|
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
import { readWp5RemoteGate, validateWp407FrozenInputs, validateWp5FinalManifest } from "./lib/wp4-07-gate.mjs";
|
|
|
|
function findFiles(directory, name) {
|
|
if (!existsSync(directory)) return [];
|
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const path = resolve(directory, entry.name);
|
|
return entry.isDirectory() ? findFiles(path, name) : entry.name === name ? [path] : [];
|
|
});
|
|
}
|
|
|
|
function readBaselineMerges() {
|
|
const log = spawnSync("git", ["log", "--merges", "--format=%H%x09%s", "HEAD"], { encoding: "utf8", timeout: 30_000 });
|
|
if ((log.status ?? 1) !== 0) throw new Error("WP4_07_BASELINE_MERGES_UNREADABLE");
|
|
return log.stdout.trim().split(/\r?\n/).filter(Boolean).map((line) => {
|
|
const [sha, ...subject] = line.split("\t");
|
|
return { sha, subject: subject.join("\t") };
|
|
}).filter((entry) => entry.subject.startsWith("merge: integrate WP5-"));
|
|
}
|
|
|
|
function readGitState() {
|
|
const commitResult = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf8", timeout: 30_000 });
|
|
const statusResult = spawnSync("git", ["status", "--porcelain"], { encoding: "utf8", timeout: 30_000 });
|
|
if ((commitResult.status ?? 1) !== 0 || (statusResult.status ?? 1) !== 0) throw new Error("WP4_07_GIT_STATE_UNREADABLE");
|
|
return {
|
|
commit: commitResult.stdout.trim(),
|
|
worktree_under_test: statusResult.stdout.trim() ? "uncommitted implementation" : "clean committed implementation",
|
|
};
|
|
}
|
|
|
|
const layer = process.argv[2];
|
|
if (!['visual', 'performance'].includes(layer)) {
|
|
console.error("Usage: node scripts/run-wp4-07-layer.mjs <visual|performance>");
|
|
process.exit(2);
|
|
}
|
|
|
|
try {
|
|
const fixture = validateWp407FrozenInputs();
|
|
const gate = readWp5RemoteGate();
|
|
if (!gate.complete) {
|
|
console.error(JSON.stringify({
|
|
code: "WP4_07_WP5_GATE_INCOMPLETE",
|
|
fixture_sha256: fixture.fixture_sha256,
|
|
layer,
|
|
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_branches: gate.required_final_branches,
|
|
status: "red",
|
|
}, null, 2));
|
|
process.exit(1);
|
|
}
|
|
const runId = process.env.DADA_TDD_RUN_ID ?? `wp4-07-${layer}-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
|
|
const runDirectory = resolve("artifacts", "tdd", runId);
|
|
const casesDirectory = resolve(runDirectory, "cases");
|
|
const releaseDirectory = resolve(runDirectory, "real-release");
|
|
mkdirSync(casesDirectory, { recursive: true });
|
|
let environment = {
|
|
...process.env,
|
|
DADA_PLAYWRIGHT_OUTPUT_DIR: resolve(runDirectory, "playwright-output"),
|
|
DADA_WP4_07_EVIDENCE_DIR: casesDirectory,
|
|
DADA_WP4_07_HARNESS_MODE: "real_archive",
|
|
DADA_WP4_07_LAYER: layer,
|
|
DADA_WP5_03_RUN_DIRECTORY: releaseDirectory,
|
|
};
|
|
const build = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", "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 && pnpm --filter @dada/asset-compiler build"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 });
|
|
if (build.stdout) process.stdout.write(build.stdout);
|
|
if (build.stderr) process.stderr.write(build.stderr);
|
|
if ((build.status ?? 1) !== 0) process.exit(build.status ?? 1);
|
|
let manifestPath = process.env.DADA_WP4_07_FINAL_ASSET_MANIFEST;
|
|
if (!manifestPath) {
|
|
const compile = spawnSync(process.execPath, ["scripts/compile-wp5-03-assets.mjs"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 });
|
|
if (compile.stdout) process.stdout.write(compile.stdout);
|
|
if (compile.stderr) process.stderr.write(compile.stderr);
|
|
if ((compile.status ?? 1) !== 0) process.exit(compile.status ?? 1);
|
|
manifestPath = resolve(releaseDirectory, "cases", "TDD-WP5-WHITE-001-p0a-allowlist", "manifest.json");
|
|
}
|
|
const manifest = validateWp5FinalManifest(manifestPath);
|
|
environment = { ...environment, DADA_WP4_07_FINAL_ASSET_MANIFEST: resolve(manifestPath) };
|
|
const playwright = spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", "pnpm exec playwright test --config playwright.wp4-07.config.ts"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 });
|
|
if (playwright.stdout) process.stdout.write(playwright.stdout);
|
|
if (playwright.stderr) process.stderr.write(playwright.stderr);
|
|
if ((playwright.status ?? 1) !== 0) process.exit(playwright.status ?? 1);
|
|
const caseDirectory = resolve(casesDirectory, layer === "visual" ? "TDD-WP4-VIS-001-browser-diff" : "TDD-WP4-PERF-001-budget");
|
|
const familyNeedle = layer === "visual" ? "editor-export-evidence" : "budget-without-dilution";
|
|
const traces = findFiles(environment.DADA_PLAYWRIGHT_OUTPUT_DIR, "trace.zip");
|
|
for (const browser of ["chrome", "edge"]) {
|
|
const evidenceTrace = resolve(caseDirectory, browser, "trace.zip");
|
|
if (existsSync(evidenceTrace)) continue;
|
|
const trace = traces.find((path) => path.toLowerCase().includes(familyNeedle) && path.toLowerCase().includes(browser));
|
|
if (!trace) throw new Error(`WP4_07_${layer.toUpperCase()}_${browser.toUpperCase()}_TRACE_REQUIRED`);
|
|
mkdirSync(resolve(caseDirectory, browser), { recursive: true });
|
|
copyFileSync(trace, resolve(caseDirectory, browser, "trace.zip"));
|
|
}
|
|
const aggregator = spawnSync(process.execPath, [layer === "visual" ? "scripts/compare-wp4-07-screenshots.mjs" : "scripts/aggregate-wp4-07-performance.mjs", "--evidence", caseDirectory, "--phase", "green"], { encoding: "utf8", env: environment, maxBuffer: 64 * 1024 * 1024 });
|
|
if (aggregator.stdout) process.stdout.write(aggregator.stdout);
|
|
if (aggregator.stderr) process.stderr.write(aggregator.stderr);
|
|
if ((aggregator.status ?? 1) !== 0) process.exit(aggregator.status ?? 1);
|
|
writeFileSync(resolve(caseDirectory, "release-inputs.json"), `${JSON.stringify({
|
|
baseline_merge_commits: readBaselineMerges(),
|
|
final_manifest: manifest,
|
|
remote_terminal_shas: gate.terminal_branch_shas,
|
|
source_task_shas: gate.task_shas,
|
|
status: "passed",
|
|
}, null, 2)}\n`);
|
|
const git = readGitState();
|
|
const manifestSha = createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase();
|
|
const visual = layer === "visual";
|
|
const evidenceRefs = visual
|
|
? [
|
|
"pixel-diff.json", "layout-boxes.json", "manual-review.json", "release-inputs.json",
|
|
"chrome/asset-sources.json", "chrome/editor.png", "chrome/canvas.png", "chrome/export-dialog.png", "chrome/layout-boxes.json", "chrome/trace.zip",
|
|
"edge/asset-sources.json", "edge/editor.png", "edge/canvas.png", "edge/export-dialog.png", "edge/layout-boxes.json", "edge/trace.zip",
|
|
]
|
|
: [
|
|
"performance.json", "memory.json", "dom-count.json", "environment.json", "release-inputs.json",
|
|
"chrome/asset-sources.json", "chrome/performance-raw.json", "chrome/trace.zip",
|
|
"edge/asset-sources.json", "edge/performance-raw.json", "edge/trace.zip",
|
|
];
|
|
const missingEvidence = evidenceRefs.filter((path) => !existsSync(resolve(caseDirectory, path)));
|
|
const manualReviewPassed = !visual
|
|
|| JSON.parse(readFileSync(resolve(caseDirectory, "manual-review.json"), "utf8")).status === "passed";
|
|
const status = missingEvidence.length > 0
|
|
? "failed"
|
|
: manualReviewPassed ? "passed" : "automated_green_pending_manual";
|
|
const result = {
|
|
acceptance_criteria: visual ? ["AC-19", "AC-23", "AC-32"] : ["AC-27", "AC-32"],
|
|
automation: visual ? ["automated", "manual_review"] : ["automated"],
|
|
commit: git.commit,
|
|
evidence_refs: evidenceRefs,
|
|
fixture_ids: ["FX-CANVAS-50"],
|
|
green_assertions: visual
|
|
? ["Chrome/Edge visual and layout differences remain within the fixed section 10.1 thresholds", "known substitutions receive manual review"]
|
|
: ["all fixed section 10.2 budgets pass in Chrome and Edge", "export failure leaves the project and latest export unchanged"],
|
|
layer: visual ? ["VIS-PERF", "MANUAL"] : ["VIS-PERF"],
|
|
manifest: { path: "tasks.manifest.json", sha256: manifestSha },
|
|
missing_evidence: missingEvidence,
|
|
phase: "green",
|
|
red_reason: visual ? "Chrome/Edge 白名单结构或导出漂移" : "50 元素、自动保存、资源面板或导出超过预算",
|
|
release_gate: ["work_package:WP-4", "release:P0-A"],
|
|
requirements: visual ? ["NFR-02"] : ["NFR-03"],
|
|
run_id: runId,
|
|
status,
|
|
task_id: "TASK-WP4-07",
|
|
test_id: visual ? "TDD-WP4-VIS-001-browser-diff" : "TDD-WP4-PERF-001-budget",
|
|
work_package: "WP-4",
|
|
worktree_under_test: git.worktree_under_test,
|
|
};
|
|
writeFileSync(resolve(caseDirectory, "result.json"), `${JSON.stringify(result, null, 2)}\n`);
|
|
writeFileSync(resolve(runDirectory, "evidence.json"), `${JSON.stringify({
|
|
cases: [{ missing_evidence: missingEvidence, status, test_id: result.test_id }],
|
|
commit: git.commit,
|
|
phase: "green",
|
|
run_id: runId,
|
|
status,
|
|
task_id: "TASK-WP4-07",
|
|
}, null, 2)}\n`);
|
|
console.log(JSON.stringify({ layer, manifest, run_id: runId, status }, null, 2));
|
|
if (status === "failed") process.exit(1);
|
|
} catch (error) {
|
|
console.error(JSON.stringify({ code: error instanceof Error ? error.message : String(error), layer, status: "failed" }, null, 2));
|
|
process.exit(1);
|
|
}
|