Files
workinf_Blender_Wasm/tools/web/workflow-case-lib.mjs
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

97 lines
7.7 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import assert from 'node:assert/strict';
export const digest = value => crypto.createHash('sha256').update(value).digest('hex');
export const fileHash = file => digest(fs.readFileSync(file));
export const readJSON = file => JSON.parse(fs.readFileSync(file, 'utf8'));
export function validateSourceReview(review, hashFile) {
object(review, 'source review'); assert.equal(review.schemaVersion, 1); assert.equal(review.status, 'REVIEWED_SCOPE'); text(review.batchId, 'source review batchId'); assert.equal(review.verifiedUnitDelta, 0);
assert.ok(Array.isArray(review.bindings) && review.bindings.length > 0, 'source review bindings required');
for (const ref of review.bindings) { object(ref, 'source binding'); text(ref.path, 'source binding path'); assert.match(text(ref.sha256, 'source binding hash'), /^[a-f0-9]{64}$/); assert.equal(hashFile(ref.path), ref.sha256, `SOURCE_REVIEW_DRIFT: ${ref.path}`); }
assert.ok(Array.isArray(review.contracts) && review.contracts.length > 0, 'source contracts required');
for (const contract of review.contracts) { object(contract, 'source contract'); text(contract.id, 'contract id'); text(contract.scope, 'contract scope'); assert.ok(Array.isArray(contract.sourcePaths) && contract.sourcePaths.length > 0 && contract.sourcePaths.every(p => typeof p === 'string' && review.bindings.some(ref => ref.path === p)), 'contract sources must be bound'); assert.ok(Array.isArray(contract.proofStages) && contract.proofStages.length > 0 && contract.proofStages.every(p => typeof p === 'string' && p.length > 0), 'contract proof stages required'); }
assert.equal(new Set(review.contracts.map(c => c.id)).size, review.contracts.length, 'duplicate source contracts');
return review;
}
export function blendFormat(file) {
const fd = fs.openSync(file, 'r'), header = Buffer.alloc(7);
try { fs.readSync(fd, header, 0, 7, 0); } finally { fs.closeSync(fd); }
if (header.toString('ascii') === 'BLENDER') return 'blend';
if (header.subarray(0, 2).equals(Buffer.from([0x1f, 0x8b]))) return 'gzip';
if (header.subarray(0, 4).equals(Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))) return 'zstd';
throw new Error(`${file}: unsupported blend header`);
}
export function filesUnder(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name)).flatMap(e => {
const file = path.join(directory, e.name);
return e.isDirectory() ? filesUnder(file) : e.isFile() ? [file] : [];
});
}
export const records = files => [...new Set(files)].sort().map(file => ({ path: file, sha256: fileHash(file) }));
export function candidateInputs(directory) {
const manifestPath = path.join(directory, 'candidate.json'), manifest = object(readJSON(manifestPath), 'candidate');
const expected = object(manifest.sha256, 'candidate.sha256');
const native = ['web_engine.js', 'web_engine.wasm'].map(name => path.join(directory, 'public/vendor/blender', name));
for (const file of native) { const hash = text(expected[path.basename(file)], 'native hash'); assert.match(hash, /^[a-f0-9]{64}$/); assert.equal(fileHash(file), hash, `${file}: candidate drift`); }
assert.equal(fs.readFileSync(native[1]).subarray(0, 4).toString('hex'), '0061736d', 'invalid WASM magic');
return [manifestPath, ...filesUnder(path.join(directory, 'public'))];
}
export function reportOutcome(report) {
object(report, 'Playwright report'); object(report.stats, 'stats');
for (const key of ['expected', 'unexpected', 'flaky', 'skipped']) assert.ok(Number.isSafeInteger(report.stats[key]) && report.stats[key] >= 0, `stats.${key}: expected nonnegative integer`);
assert.ok(Array.isArray(report.suites) && Array.isArray(report.errors), 'report arrays required');
const tests = [];
const visit = suites => { for (const suite of suites) { object(suite, 'suite'); assert.ok(Array.isArray(suite.specs)); for (const spec of suite.specs) { assert.ok(Array.isArray(spec.tests)); tests.push(...spec.tests); } if (suite.suites !== undefined) { assert.ok(Array.isArray(suite.suites)); visit(suite.suites); } } };
visit(report.suites);
for (const test of tests) {
object(test, 'test'); text(test.expectedStatus, 'expectedStatus'); text(test.status, 'test.status');
assert.ok(Array.isArray(test.results), 'test.results must be array');
for (const result of test.results) { object(result, 'result'); text(result.status, 'result.status'); }
}
const passed = tests.length > 0 && tests.every(t => t.expectedStatus === 'passed' && t.status === 'expected' && t.results.length === 1 && t.results[0].status === 'passed');
return { passed: passed && report.errors.length === 0 && report.stats.expected === tests.length && ['unexpected', 'flaky', 'skipped'].every(k => report.stats[k] === 0), stats: report.stats, tests: tests.length };
}
export function object(value, label) {
assert.ok(value !== null && typeof value === 'object' && !Array.isArray(value), `${label}: expected object`);
return value;
}
export function text(value, label) {
assert.equal(typeof value, 'string', `${label}: expected string`);
assert.ok(value.length > 0, `${label}: empty string`); return value;
}
export function atomicJSON(file, value) {
fs.mkdirSync(path.dirname(file), { recursive: true });
const temp = `${file}.${process.pid}.tmp`;
fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n'); fs.renameSync(temp, file);
}
export function identity(pid) {
try { return fs.readFileSync(`/proc/${pid}/stat`, 'utf8').split(') ').at(-1).split(' ')[19]; } catch { return null; }
}
export function reusable(receipt, inputHash) {
if (!receipt || receipt.status !== 'PASS' || receipt.inputHash !== inputHash || !Array.isArray(receipt.artifacts) || receipt.artifacts.length === 0) return false;
try { return receipt.artifacts.every(a => typeof a.path === 'string' && typeof a.sha256 === 'string' && /^[a-f0-9]{64}$/.test(a.sha256) && fileHash(a.path) === a.sha256); } catch { return false; }
}
export function sameInputs(before, after) {
assert.deepEqual(after, before, 'inputs changed during the case; evidence remains stale');
}
export function validatePlan(plan) {
object(plan, 'plan'); assert.equal(plan.schemaVersion, 1);
assert.equal(typeof plan.nativeSession, 'boolean', 'nativeSession must be boolean');
assert.ok(Array.isArray(plan.remainingGates) && plan.remainingGates.every(x => typeof x === 'string'), 'remainingGates must be strings');
assert.match(text(plan.batchId, 'batchId'), /^B[1-6]$/); text(plan.workflow, 'workflow');
assert.ok(Array.isArray(plan.sourcePaths) && plan.sourcePaths.length > 0, 'source paths required');
assert.ok(Array.isArray(plan.fixtures) && plan.fixtures.length === (plan.sourceSuite ? 1 : 2), 'source suite or minimal + production required');
assert.deepEqual(plan.fixtures.map(f => object(f, 'fixture').kind), plan.sourceSuite ? ['source'] : ['minimal', 'production']);
if (plan.sourceSuite !== undefined) { assert.match(text(plan.sourceSuite, 'sourceSuite'), /^blender-5\.2\.0\/tests\/python\/[\w-]+\.py$/); assert.ok(plan.sourcePaths.includes(plan.sourceSuite), 'source suite must be hash-bound'); }
for (const f of plan.fixtures) {
text(f.path, 'fixture.path'); text(f.grep, 'fixture.grep');
assert.match(text(f.spec, 'fixture.spec'), /^tests\/e2e\/[\w-]+\.spec\.ts$/);
assert.ok(Array.isArray(f.scope) && f.scope.every(x => typeof x === 'string'), 'scope must be strings');
}
assert.ok(Number.isSafeInteger(plan.timeoutMs) && plan.timeoutMs >= 1000 && plan.timeoutMs <= 900000, 'timeoutMs must be integer milliseconds');
assert.ok(plan.sourcePaths.every(x => typeof x === 'string' && x.length > 0), 'sourcePaths must be strings');
if (plan.sourceReview !== undefined) text(plan.sourceReview, 'sourceReview');
return plan;
}