import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { blendFormat, candidateInputs, fileHash, reportOutcome, reusable, sameInputs, validatePlan, validateSourceReview } from '../../../tools/web/workflow-case-lib.mjs'; function temporary(t) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'wb-case-')); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); return dir; } const report = () => ({ stats: { expected: 1, unexpected: 0, flaky: 0, skipped: 0 }, errors: [], suites: [{ specs: [{ tests: [{ expectedStatus: 'passed', status: 'expected', results: [{ status: 'passed' }] }] }] }] }); test('source reviews reject stale hashes, malformed types and unbound contracts', () => { const sha256 = 'a'.repeat(64), review = { schemaVersion: 1, status: 'REVIEWED_SCOPE', batchId: 'B1', verifiedUnitDelta: 0, bindings: [{ path: 'source.cc', sha256 }], contracts: [{ id: 'rna', scope: 'location', sourcePaths: ['source.cc'], proofStages: ['changedUI'] }] }; assert.equal(validateSourceReview(review, () => sha256), review); assert.throws(() => validateSourceReview(review, () => 'b'.repeat(64)), /SOURCE_REVIEW_DRIFT/); assert.throws(() => validateSourceReview({ ...review, bindings: [{ path: 'source.cc', sha256: 42 }] }, () => sha256)); assert.throws(() => validateSourceReview({ ...review, contracts: [{ ...review.contracts[0], sourcePaths: ['unreviewed.cc'] }] }, () => sha256), /must be bound/); assert.throws(() => validateSourceReview({ ...review, contracts: [...review.contracts, ...review.contracts] }, () => sha256), /duplicate/); }); test('report types are checked before comparisons; no coercion', () => { assert.equal(reportOutcome(report()).passed, true); for (const value of ['1', null, NaN, -1]) { const r = report(); r.stats.expected = value; assert.throws(() => reportOutcome(r), /integer/); } assert.throws(() => reportOutcome({ ...report(), suites: {} }), /arrays/); }); test('empty or inconsistent counters cannot produce a pass', () => { const r = report(); r.suites = []; r.stats.expected = 0; assert.equal(reportOutcome(r).passed, false); assert.equal(reportOutcome({ ...report(), stats: { ...report().stats, expected: 2 } }).passed, false); }); test('skip, expected failure, retry, flaky outcome and global error remain failures', () => { for (const status of ['skipped', 'flaky', 'unexpected']) { const r = report(); r.suites[0].specs[0].tests[0].status = status; assert.equal(reportOutcome(r).passed, false); } const r = report(), t = r.suites[0].specs[0].tests[0]; t.expectedStatus = 'failed'; assert.equal(reportOutcome(r).passed, false); t.expectedStatus = 'passed'; t.results.push({ status: 'passed' }); assert.equal(reportOutcome(r).passed, false); assert.equal(reportOutcome({ ...report(), errors: [{ message: 'setup failed' }] }).passed, false); }); test('reuse requires matching identity and all original artifacts', t => { const file = path.join(temporary(t), 'evidence'); fs.writeFileSync(file, 'first'); const saved = { status: 'PASS', inputHash: 'input', artifacts: [{ path: file, sha256: fileHash(file) }] }; assert.equal(reusable(saved, 'input'), true); assert.equal(reusable(saved, 'different'), false); fs.writeFileSync(file, 'drift'); assert.equal(reusable(saved, 'input'), false); fs.unlinkSync(file); assert.equal(reusable(saved, 'input'), false); assert.equal(reusable({ ...saved, artifacts: [] }, 'input'), false); }); test('candidate SHA and actual WASM bytes are required', t => { const dir = temporary(t), native = path.join(dir, 'public/vendor/blender'); fs.mkdirSync(native, { recursive: true }); const js = path.join(native, 'web_engine.js'), wasm = path.join(native, 'web_engine.wasm'); fs.writeFileSync(js, 'export default {}'); fs.writeFileSync(wasm, Buffer.from([0,97,115,109,1,0,0,0])); fs.writeFileSync(path.join(dir, 'candidate.json'), JSON.stringify({ sha256: { 'web_engine.js': fileHash(js), 'web_engine.wasm': fileHash(wasm) } })); assert.equal(candidateInputs(dir).length, 3); fs.appendFileSync(wasm, 'changed'); assert.throws(() => candidateInputs(dir), /drift/); }); test('blend compression is detected from bytes, and numeric strings stay distinct', t => { const file = path.join(temporary(t), 'scene.blend'); for (const [bytes, format] of [[Buffer.from('BLENDER'), 'blend'], [Buffer.from([0x1f,0x8b]), 'gzip'], [Buffer.from([0x28,0xb5,0x2f,0xfd]), 'zstd']]) { fs.writeFileSync(file, bytes); assert.equal(blendFormat(file), format); } fs.writeFileSync(file, 'notblend'); assert.throws(() => blendFormat(file), /header/); assert.throws(() => sameInputs({ revision: 1 }, { revision: '1' }), /inputs changed/); sameInputs({ revision: 1, values: [0, 1] }, { revision: 1, values: [0, 1] }); }); test('case plans preserve minimal/production order and reject coercible options', () => { const p = JSON.parse(fs.readFileSync(new URL('../../../tools/web/workflow-cases/B1.json', import.meta.url), 'utf8')); assert.equal(validatePlan(p).batchId, 'B1'); assert.throws(() => validatePlan({ ...p, timeoutMs: '180000' }), /integer/); assert.throws(() => validatePlan({ ...p, nativeSession: 'true' }), /boolean/); assert.throws(() => validatePlan({ ...p, fixtures: [...p.fixtures].reverse() })); }); test('source suites require one source fixture and a bound Blender test path', () => { const p = JSON.parse(fs.readFileSync(new URL('../../../tools/web/workflow-cases/B1.json', import.meta.url), 'utf8')), suite = 'blender-5.2.0/tests/python/bl_blendfile_io.py'; const source = { ...p, sourceSuite: suite, sourcePaths: [suite], fixtures: [{ ...p.fixtures[0], kind: 'source' }] }; assert.equal(validatePlan(source).sourceSuite, suite); assert.throws(() => validatePlan({ ...source, sourcePaths: ['unbound.py'] })); assert.throws(() => validatePlan({ ...source, sourceSuite: '../outside.py' })); assert.throws(() => validatePlan({ ...source, fixtures: p.fixtures })); });