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>
90 lines
4.5 KiB
JavaScript
90 lines
4.5 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { fileURLToPath } from 'node:url';
|
|
import createEngine from '../build_web_blender6/bin/web_engine.js';
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const engine = await createEngine({ wasmBinary: fs.readFileSync(path.join(root, 'build_web_blender6/bin/web_engine.wasm')) });
|
|
const handle = engine._web_engine_create();
|
|
const pointers = engine._malloc(8);
|
|
const started = performance.now();
|
|
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
const output = () => {
|
|
const pointer = engine.HEAPU32[pointers / 4];
|
|
const length = engine.HEAPU32[pointers / 4 + 1];
|
|
return { pointer, bytes: engine.HEAPU8.slice(pointer, pointer + length) };
|
|
};
|
|
const open = file => {
|
|
const bytes = fs.readFileSync(path.join(root, file));
|
|
const pointer = engine._malloc(bytes.length);
|
|
try {
|
|
engine.HEAPU8.set(bytes, pointer);
|
|
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.length), 0);
|
|
} finally { engine._free(pointer); }
|
|
};
|
|
const invoke = (cmd, params = {}, ok = true) => {
|
|
const bytes = Buffer.from(JSON.stringify({ cmd, params }));
|
|
const pointer = engine._malloc(bytes.length);
|
|
try {
|
|
engine.HEAPU8.set(bytes, pointer);
|
|
assert.equal(engine._web_engine_call_json(handle, pointer, bytes.length, pointers, pointers + 4), 0);
|
|
const result = JSON.parse(new TextDecoder().decode(output().bytes));
|
|
assert.equal(result.ok, ok, result.error);
|
|
} finally { engine._free(pointer); }
|
|
};
|
|
const command = (command, ok = true) => invoke('applyCommand', { command }, ok);
|
|
const snapshot = () => {
|
|
assert.equal(engine._web_engine_get_scene_snapshot(handle, pointers, pointers + 4), 0);
|
|
return JSON.parse(new TextDecoder().decode(output().bytes));
|
|
};
|
|
const save = () => {
|
|
assert.equal(engine._web_engine_save_blend(handle, pointers, pointers + 4), 0);
|
|
const result = output();
|
|
engine._web_engine_free_buffer(result.pointer);
|
|
return result.bytes;
|
|
};
|
|
try {
|
|
open('tests/files/web/nonmesh_scene.blend');
|
|
command({ type: 'createObjectData', objectType: 'EMPTY', name: 'Save Stability Probe' });
|
|
const beforeState = snapshot();
|
|
const font = beforeState.nonMeshData.find(data => data.type === 'FONT');
|
|
assert(font);
|
|
const first = save();
|
|
const savedMemory = engine._web_engine_get_allocated_bytes();
|
|
assert.equal(hash(save()), hash(first), 'unchanged repeated saves must have identical bytes');
|
|
assert.equal(engine._web_engine_get_allocated_bytes(), savedMemory, 'repeated saves must not accumulate retained buffers');
|
|
command({ type: 'setFontLinks', dataId: font.id, links: { regular: 'vfont:MissingLocalFont' } }, false);
|
|
const rejectedState = snapshot();
|
|
assert.equal(rejectedState.revision, beforeState.revision);
|
|
assert.deepEqual(rejectedState.nonMeshData, beforeState.nonMeshData);
|
|
assert.deepEqual(rejectedState.vfonts, beforeState.vfonts);
|
|
const afterFailure = save();
|
|
assert.equal(hash(afterFailure), hash(first), 'a rejected command must preserve saved bytes for the same revision');
|
|
command({ type: 'createObjectData', objectType: 'EMPTY', name: 'Changed Save Probe' });
|
|
const changed = save();
|
|
assert.notEqual(hash(changed), hash(first), 'successful edits must invalidate saved bytes');
|
|
invoke('undo');
|
|
assert(!snapshot().nodes.some(node => node.name === 'Changed Save Probe'));
|
|
assert.notEqual(hash(save()), hash(changed), 'undo must not export the newer cached state');
|
|
invoke('redo');
|
|
assert(snapshot().nodes.some(node => node.name === 'Changed Save Probe'));
|
|
const redone = save();
|
|
assert.equal(hash(save()), hash(redone));
|
|
open('tests/files/web/basic_scene.blend');
|
|
const reopened = save();
|
|
assert.equal(hash(reopened), hash(fs.readFileSync(path.join(root, 'tests/files/web/basic_scene.blend'))),
|
|
'opening a different project must invalidate the saved byte cache');
|
|
open('tests/files/web/nonmesh_scene.blend');
|
|
assert.equal(hash(save()), hash(fs.readFileSync(path.join(root, 'tests/files/web/nonmesh_scene.blend'))));
|
|
console.log(JSON.stringify({ status: 'PASS', seconds: (performance.now() - started) / 1000,
|
|
savedHash: hash(first), afterFailureHash: hash(afterFailure), changedHash: hash(changed) }));
|
|
} catch (error) {
|
|
console.log(JSON.stringify({ status: 'FAIL', error: error.message, seconds: (performance.now() - started) / 1000 }));
|
|
process.exitCode = 1;
|
|
} finally {
|
|
engine._free(pointers);
|
|
engine._web_engine_destroy(handle);
|
|
}
|