import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { execFileSync } from "node:child_process"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const planPath = path.join(root, "tests/golden/M15-03A/next-task-plan.json"); const reportPath = path.join(root, "tests/golden/M15-03B/next-task-spec-check-report.json"); const manifestPath = path.join(root, "tests/golden/M15-03B/manifest.json"); const generator = path.join(root, "tools/web/generate-blender-next-task-plan.mjs"); const sha256 = (b) => crypto.createHash("sha256").update(b).digest("hex"); const fileSha256 = (f) => sha256(fs.readFileSync(f)); const relative = (f) => path.relative(root, f).replaceAll(path.sep, "/"); const plan = JSON.parse(fs.readFileSync(planPath, "utf8")); assert.equal(plan.tasks.length, 6900); const required = ["fixture", "desktopCommand", "webCommand", "comparator", "exitCriteria"]; const missing = []; const malformed = []; for (const task of plan.tasks) { for (const field of required) { if (field === "fixture") { if (!task.fixture?.path || !task.fixture?.state) missing.push({ id: task.id, field }); } else if (field === "exitCriteria") { if (!Array.isArray(task.exitCriteria) || task.exitCriteria.length < 3) missing.push({ id: task.id, field }); } else if (typeof task[field] !== "string" || task[field].length < 8) missing.push({ id: task.id, field }); } if (!task.desktopCommand.includes("blender") || !task.webCommand.includes("npm") || !task.comparator.includes("check-generated-gap")) malformed.push(task.id); } assert.deepEqual(missing, []); assert.deepEqual(malformed, []); const report = { schemaVersion: 1, task: "M15-03B", operation: "BLENDER_NEXT_TASK_SPEC_CHECK", source: { path: "tests/golden/M15-03A/next-task-plan.json", sha256: fileSha256(planPath) }, summary: { tasks: plan.tasks.length, completeSpecs: plan.tasks.length, missingFields: missing.length, malformed: malformed.length }, requiredFields: required, nextTask: "M15-03C" }; if (process.env.UPDATE_M15_03B_MANIFEST === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); const paths = { parentManifest: path.join(root, "tests/golden/M15-03A/manifest.json"), generator, checker: path.join(root, "tools/web/check-blender-next-task-spec.mjs"), plan: planPath, report: reportPath, package: path.join(root, "web/package.json") }; const artifacts = Object.fromEntries(Object.entries(paths).map(([name, file]) => [name, { path: relative(file), sha256: fileSha256(file) }])); fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M15-03B", parentTask: "M15-03A", enablingTask: false, parityStateChange: false, operation: "BLENDER_NEXT_TASK_SPEC_CHECK", artifacts, nextTask: "M15-03C" }, null, 2)}\n`); } const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); process.stdout.write(`blender-next-task-spec-ok tasks=${report.summary.tasks} complete=${report.summary.completeSpecs} missing=${report.summary.missingFields} malformed=${report.summary.malformed} next=${manifest.nextTask}\n`);