import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const outputPath = path.join(root, "tests/golden/WBV2/dynamic-inventory.json"); const generatorPath = path.join(root, "tools/web/generate-source-dynamic-inventory.mjs"); const reportPath = path.join(root, "tests/golden/WBV2/dynamic-inventory-check-report.json"); const manifestPath = path.join(root, "tests/golden/WBV2/dynamic-inventory-manifest.json"); const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); const taskArgument = process.argv.indexOf("--task"); if (taskArgument >= 0) { const task = process.argv[taskArgument + 1]; if (!task) throw new Error("--task requires a task id"); const taskManifestPath = path.join(root, `tests/golden/${task}/manifest.json`); const taskManifest = readJson(taskManifestPath); assert.equal(taskManifest.status, "done"); const taskPlan = readJson(path.join(root, taskManifest.artifacts.plan.path)); const taskRecord = taskPlan.tasks.find((candidate) => candidate.id === task); const selectedUnits = taskRecord?.module?.coverageBindings ?? []; assert.ok(selectedUnits.length, `task ${task} has no selected coverage units`); const inventory = readJson(outputPath); assert.equal(inventory.authority.sourceTreeSha256, inventory.coverageLedger?.[0]?.sourceTreeSha256 ?? inventory.authority.sourceTreeSha256); assert.equal(inventory.coverage.verifiedUnits + inventory.coverage.unverifiedUnits, inventory.summary.unitCount); const sourceArtifacts = Object.values(taskManifest.artifacts ?? {}).filter((artifact) => artifact?.path?.startsWith("blender-5.2.0/source/blender/")); assert.ok(sourceArtifacts.length, `task ${task} has no source artifacts`); for (const artifact of sourceArtifacts) { assert.ok(fs.existsSync(path.join(root, artifact.path)), artifact.path); assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path); } for (const unitId of selectedUnits) { const unit = inventory.units.find((candidate) => candidate.id === unitId); assert.ok(unit, `dynamic unit missing: ${unitId}`); assert.equal(unit.coverageStatus, "VERIFIED", unitId); assert.equal(unit.coverage?.task, task, `${unitId} is bound to another task`); } const report = { schemaVersion: 1, operation: "BLENDER_52_SOURCE_DYNAMIC_INVENTORY_CHECK", scope: "batch", task, status: "PASS", sourceTreeSha256: inventory.authority.sourceTreeSha256, sourceFiles: sourceArtifacts.length, unitCount: inventory.summary.unitCount, planningEstimate: inventory.policy.planningEstimate, verifiedUnits: inventory.coverage.verifiedUnits, unverifiedUnits: inventory.coverage.unverifiedUnits, selectedUnits, deterministic: "batch_hash_bound", }; fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`source-dynamic-inventory-batch-ok task=${task} sourceFiles=${sourceArtifacts.length} units=${inventory.summary.unitCount} selected=${selectedUnits.length} verified=${inventory.coverage.verifiedUnits} unverified=${inventory.coverage.unverifiedUnits} sourceTree=${inventory.authority.sourceTreeSha256}\n`); process.exit(0); } const inventory = readJson(outputPath); const manifest = readJson(manifestPath); assert.equal(inventory.schemaVersion, 1); assert.equal(inventory.operation, "BLENDER_52_SOURCE_DYNAMIC_INVENTORY"); assert.ok(["SOURCE_DERIVED_UNVERIFIED", "SOURCE_DERIVED_WITH_EXPLICIT_COVERAGE"].includes(inventory.status)); assert.equal(inventory.authority.sourceRoot, "blender-5.2.0/source/blender"); const expectedSourceDirectories = fs.readdirSync(path.join(root, "blender-5.2.0/source/blender"), { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name !== "tests" && entry.name !== "test") .map((entry) => entry.name) .sort(); assert.deepEqual(inventory.authority.sourceDirectories, expectedSourceDirectories); assert.deepEqual(inventory.authority.excludedDirectories, ["tests", "test"]); assert.equal(inventory.policy.initialLegacyEstimate, 6900); assert.equal(inventory.policy.planningEstimate, 5200); assert.equal(inventory.policy.hardTarget, false); assert.equal(inventory.coverage.verifiedUnits + inventory.coverage.unverifiedUnits, inventory.summary.unitCount); assert.ok(["forbidden", "explicit_source_case_bindings_only"].includes(inventory.coverage.inventoryMutation)); assert.equal(inventory.coverageLedger?.length ?? 0, inventory.coverage.verifiedUnits); assert.equal(new Set((inventory.coverageLedger ?? []).map((binding) => binding.unitId)).size, inventory.coverage.verifiedUnits); assert.equal(inventory.sourceFiles.length, inventory.summary.sourceFiles); assert.equal(inventory.units.length, inventory.summary.unitCount); assert.equal(manifest.schemaVersion, 1); assert.equal(manifest.operation, "BLENDER_52_SOURCE_DYNAMIC_INVENTORY"); assert.equal(manifest.status, "done"); assert.equal(inventory.summary.unitCount, Object.values(inventory.summary.byKind).reduce((total, count) => total + count, 0)); assert.equal(inventory.summary.unitCount, Object.values(inventory.summary.byFamily).reduce((total, count) => total + count, 0)); assert.deepEqual(inventory.units.map((unit) => unit.id), [...inventory.units].map((unit) => unit.id).sort()); assert.equal(new Set(inventory.units.map((unit) => unit.id)).size, inventory.units.length); assert.equal(inventory.sourceTreeSha256 ?? inventory.authority.sourceTreeSha256, inventory.authority.sourceTreeSha256); const sourceFiles = new Map(inventory.sourceFiles.map((file) => [file.path, file])); assert.equal(sourceFiles.size, inventory.sourceFiles.length); for (const file of inventory.sourceFiles) { const absolute = path.join(root, file.path); assert.ok(fs.existsSync(absolute), file.path); const bytes = fs.readFileSync(absolute); assert.equal(bytes.length, file.bytes, file.path); assert.equal(sha256(bytes), file.sha256, file.path); assert.ok(!file.sourcePath.includes("/tests/"), file.sourcePath); } for (const [name, artifact] of Object.entries(manifest.artifacts)) { assert.ok(fs.existsSync(path.join(root, artifact.path)), `${name}: ${artifact.path}`); assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, `${name}: ${artifact.path}`); } const treeDigest = sha256(inventory.sourceFiles.map((file) => `${file.sourcePath}\0${file.sha256}\n`).join("")); assert.equal(treeDigest, inventory.authority.sourceTreeSha256); for (const unit of inventory.units) { assert.match(unit.id, /^[a-z-]+:[^\n]+$/u); assert.ok(["operator", "operator-macro", "rna-struct", "node", "modifier-source", "web-command"].includes(unit.kind)); assert.ok(["EDITOR", "RNA", "NODES", "MODIFIER", "ANIMATION", "SEQUENCER", "CORE", "WINDOW_MANAGER", "WEB_ENGINE", "OTHER"].includes(unit.family)); assert.ok(["UNVERIFIED", "VERIFIED"].includes(unit.coverageStatus)); assert.ok(unit.locations.length > 0); for (const location of unit.locations) { const source = sourceFiles.get(location.path); assert.ok(source, location.path); assert.ok(Number.isSafeInteger(location.line) && location.line > 0, `${unit.id}:${location.line}`); } if (unit.coverageStatus === "VERIFIED") assert.ok(unit.coverage?.task && unit.coverage?.evidenceManifest && unit.coverage?.productionEvidence); } const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "wbv2-source-inventory-")); try { const regenerated = path.join(temporary, "dynamic-inventory.json"); execFileSync(process.execPath, [generatorPath, "--output", regenerated], { cwd: root, stdio: "pipe" }); assert.equal( sha256(fs.readFileSync(regenerated)), sha256(fs.readFileSync(outputPath)), "dynamic inventory is not deterministic", ); } finally { fs.rmSync(temporary, { recursive: true, force: true }); } const report = { schemaVersion: 1, operation: "BLENDER_52_SOURCE_DYNAMIC_INVENTORY_CHECK", status: "PASS", sourceTreeSha256: inventory.authority.sourceTreeSha256, sourceFiles: inventory.summary.sourceFiles, sourceBytes: inventory.summary.sourceBytes, unitCount: inventory.summary.unitCount, byKind: inventory.summary.byKind, byFamily: inventory.summary.byFamily, planningEstimate: inventory.policy.planningEstimate, hardTarget: inventory.policy.hardTarget, verifiedUnits: inventory.coverage.verifiedUnits, unverifiedUnits: inventory.coverage.unverifiedUnits, deterministic: true, }; fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`source-dynamic-inventory-ok files=${report.sourceFiles} units=${report.unitCount} planningEstimate=${report.planningEstimate} verified=${report.verifiedUnits} unverified=${report.unverifiedUnits} sourceTree=${report.sourceTreeSha256}\n`);