122 lines
6.2 KiB
JavaScript
122 lines
6.2 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const planPath = path.join(root, "tests/golden/WBV2/feature-smoke-plan.json");
|
|
const reportPath = path.join(root, "tests/golden/WBV2/feature-smoke-report.json");
|
|
const plan = JSON.parse(fs.readFileSync(planPath, "utf8"));
|
|
const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/WBV2-P1424-001/product-implementation-report.json"), "utf8"));
|
|
const args = new Set(process.argv.slice(2));
|
|
const execute = args.has("--execute");
|
|
|
|
function fail(message) {
|
|
throw new Error(`feature-smoke-plan-invalid: ${message}`);
|
|
}
|
|
|
|
if (plan.operation !== "ONE_PASS_FEATURE_SMOKE" || plan.policy.oneSmokePerFeature !== true) fail("missing one-pass policy");
|
|
if (plan.cases.length !== report.implementationTasks.length) fail(`case count ${plan.cases.length} != task count ${report.implementationTasks.length}`);
|
|
const ids = new Set();
|
|
for (const smoke of plan.cases) {
|
|
if (ids.has(smoke.featureId) || ids.has(smoke.smokeId)) fail(`duplicate feature or smoke id ${smoke.featureId}`);
|
|
ids.add(smoke.featureId); ids.add(smoke.smokeId);
|
|
if (smoke.runOnce !== true || smoke.maxAttempts !== 1) fail(`non-idempotent case ${smoke.featureId}`);
|
|
if (smoke.kind === "playwright") {
|
|
const file = path.join(root, "web", smoke.testFile);
|
|
if (!fs.existsSync(file)) fail(`${smoke.featureId} test file missing: ${smoke.testFile}`);
|
|
if (!fs.readFileSync(file, "utf8").includes(smoke.testTitle)) fail(`${smoke.featureId} test title missing: ${smoke.testTitle}`);
|
|
}
|
|
if (smoke.kind === "command" && !smoke.command) fail(`${smoke.featureId} command missing`);
|
|
}
|
|
const expected = new Set(report.implementationTasks.map((task) => task.id));
|
|
for (const id of expected) if (!ids.has(id)) fail(`implementation task has no smoke case: ${id}`);
|
|
|
|
const runCheck = (command, cwd = root) => {
|
|
const result = spawnSync("bash", ["-lc", command], { cwd, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 });
|
|
return { command, cwd: path.relative(root, cwd) || ".", exitCode: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
};
|
|
|
|
if (!execute) {
|
|
process.stdout.write(`feature-smoke-plan-ok cases=${plan.cases.length} onePass=true playwright=${plan.cases.filter((item) => item.kind === "playwright").length} command=${plan.cases.filter((item) => item.kind === "command").length}\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (fs.existsSync(reportPath)) {
|
|
const previous = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
if (previous.planId === plan.planId && Array.isArray(previous.cases) && previous.cases.some((item) => Number(item.attempts ?? 0) > 0)) {
|
|
process.stderr.write(`feature-smoke-once-only: plan ${plan.planId} already has recorded attempts; create a new focused-fix batch instead of rerunning\n`);
|
|
process.exit(2);
|
|
}
|
|
}
|
|
|
|
const startedAt = new Date().toISOString();
|
|
const smokeReport = {
|
|
schemaVersion: 1,
|
|
planId: plan.planId,
|
|
operation: "ONE_PASS_FEATURE_SMOKE_EXECUTION",
|
|
browser: "chromium",
|
|
startedAt,
|
|
policy: plan.policy,
|
|
status: "RUNNING",
|
|
cases: plan.cases.map((item) => ({ featureId: item.featureId, smokeId: item.smokeId, ordinal: item.ordinal, status: "PENDING", attempts: 0 })),
|
|
checks: [],
|
|
};
|
|
const flush = () => fs.writeFileSync(reportPath, `${JSON.stringify(smokeReport, null, 2)}\n`);
|
|
flush();
|
|
|
|
// These are global preconditions and run once; they are not counted as feature cases.
|
|
for (const command of [
|
|
"node tools/web/check-blender-feature-route-catalog.mjs",
|
|
"node tools/web/check-web-engine-command-registry.mjs",
|
|
]) {
|
|
const result = runCheck(command);
|
|
smokeReport.checks.push(result);
|
|
if (result.exitCode !== 0) {
|
|
smokeReport.status = "BLOCKED";
|
|
smokeReport.finishedAt = new Date().toISOString();
|
|
flush();
|
|
process.stderr.write(result.stderr || result.stdout);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Group only cases that share one Playwright file. Each title is still selected
|
|
// exactly once, and a failed group is never retried in this execution.
|
|
const playwrightGroups = new Map();
|
|
for (const item of plan.cases.filter((candidate) => candidate.kind === "playwright")) {
|
|
const list = playwrightGroups.get(item.testFile) ?? [];
|
|
list.push(item);
|
|
playwrightGroups.set(item.testFile, list);
|
|
}
|
|
for (const [testFile, items] of playwrightGroups) {
|
|
const pattern = `(?:${items.map((item) => item.testTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`;
|
|
const result = runCheck(`npx playwright test --config playwright.config.ts ${testFile} --grep '${pattern}' --workers=1`, path.join(root, "web"));
|
|
smokeReport.checks.push(result);
|
|
for (const item of items) {
|
|
const entry = smokeReport.cases.find((candidate) => candidate.featureId === item.featureId);
|
|
entry.attempts = 1;
|
|
entry.status = result.exitCode === 0 ? "PASS" : "FAIL";
|
|
entry.evidence = { testFile, testTitle: item.testTitle, exitCode: result.exitCode };
|
|
}
|
|
flush();
|
|
}
|
|
for (const item of plan.cases.filter((candidate) => candidate.kind === "command")) {
|
|
const result = runCheck(item.command);
|
|
smokeReport.checks.push(result);
|
|
const entry = smokeReport.cases.find((candidate) => candidate.featureId === item.featureId);
|
|
entry.attempts = 1;
|
|
entry.status = result.exitCode === 0 ? "PASS" : "FAIL";
|
|
entry.evidence = { command: item.command, exitCode: result.exitCode };
|
|
flush();
|
|
}
|
|
|
|
const failed = smokeReport.cases.filter((item) => item.status === "FAIL");
|
|
const pending = smokeReport.cases.filter((item) => item.status !== "PASS");
|
|
smokeReport.status = failed.length ? "FAILED_FOCUSED_FIX_REQUIRED" : pending.length ? "BLOCKED" : "PASS";
|
|
smokeReport.finishedAt = new Date().toISOString();
|
|
smokeReport.summary = { total: smokeReport.cases.length, passed: smokeReport.cases.filter((item) => item.status === "PASS").length, failed: failed.length, pending: pending.length, retries: 0 };
|
|
flush();
|
|
process.stdout.write(`feature-smoke-${smokeReport.status.toLowerCase()} total=${smokeReport.summary.total} passed=${smokeReport.summary.passed} failed=${smokeReport.summary.failed} pending=${smokeReport.summary.pending} retries=0\n`);
|
|
process.exit(failed.length || pending.length ? 1 : 0);
|