import fs from "node:fs"; import { spawnSync } from "node:child_process"; import { evidencePath, expectedEvidenceIdentity, root, sha256, } from "./v1-acceptance-lib.mjs"; const { ledgerSha256, packageSha256, plan, toolSourceSha256 } = expectedEvidenceIdentity(); const basePort = Number.parseInt(process.env.WEB_ACCEPTANCE_PORT ?? "5400", 10); if (!Number.isInteger(basePort) || basePort < 1024 || basePort > 65000 - plan.entries.length) { throw new Error(`WEB_ACCEPTANCE_PORT must leave ${plan.entries.length} sequential ports available`); } function capture(command, args) { const result = spawnSync(command, args, { cwd: root, encoding: "utf8", env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }, maxBuffer: 16 * 1024 * 1024, }); return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim(); } const blenderBin = process.env.BLENDER_BIN ?? "build_blender_5.2.0/bin/blender"; const blenderArchiveSha256 = process.env.BLENDER_ARCHIVE_SHA256 ?? ""; if (blenderArchiveSha256 && !/^[a-f0-9]{64}$/.test(blenderArchiveSha256)) { throw new Error("BLENDER_ARCHIVE_SHA256 must be a lowercase SHA-256"); } const environment = { node: process.version, npm: capture("npm", ["--version"]).split(/\r?\n/)[0], chromium: capture(process.env.CHROME_PATH ?? "google-chrome", ["--version"]).split(/\r?\n/)[0], blenderBin: process.env.BLENDER_BIN ? "BLENDER_BIN" : blenderBin, blenderExecutableSha256: fs.existsSync(blenderBin) ? sha256(fs.readFileSync(blenderBin)) : "unavailable", blenderArchiveSha256: blenderArchiveSha256 || "unavailable", blender: capture(blenderBin, [ "--background", "--factory-startup", "--python-expr", "import bpy; print('ACCEPTANCE_BLENDER=' + bpy.app.version_string + ';USD=' + str(bpy.app.build_options.usd))", ]).split(/\r?\n/).filter((line) => line.includes("ACCEPTANCE_BLENDER=")).at(-1) ?? "unavailable", }; const results = []; for (const [index, entry] of plan.entries.entries()) { const started = Date.now(); process.stdout.write(`[${index + 1}/${plan.entries.length}] ${entry.token}\n`); const result = spawnSync("npm", ["--prefix", "web", "run", entry.script, ...entry.args], { cwd: root, encoding: "utf8", env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1", WEB_TEST_PORT: String(basePort + index), }, maxBuffer: 32 * 1024 * 1024, }); const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim(); const storedOutput = output.slice(-4096) || `${entry.token} produced no output`; const exitCode = result.status ?? 1; results.push({ token: entry.token, familyIds: entry.familyIds, command: entry.command, exitCode, durationMs: Date.now() - started, outputSha256: sha256(storedOutput), output: storedOutput, }); process.stdout.write(` exit=${exitCode} durationMs=${results.at(-1).durationMs}\n`); } const manifest = { schemaVersion: 1, source: "docs/status/parity-ledger.json", sourceSha256: ledgerSha256, package: "web/package.json", packageSha256, toolSourceSha256, generatedAt: new Date().toISOString(), environment, summary: { families: plan.familyCount, declarations: plan.declarationCount, unique: plan.uniqueCount, passed: results.filter((result) => result.exitCode === 0).length, failed: results.filter((result) => result.exitCode !== 0).length, }, results, }; fs.writeFileSync(evidencePath, `${JSON.stringify(manifest, null, 2)}\n`); const failed = results.filter((result) => result.exitCode !== 0); process.stdout.write( `v1-acceptance-run-complete passed=${manifest.summary.passed} failed=${manifest.summary.failed} evidence=${evidencePath}\n`, ); if (failed.length > 0) { process.stderr.write(`failed acceptance: ${failed.map((result) => result.token).join(", ")}\n`); process.exitCode = 1; }