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); }