87 lines
5.7 KiB
JavaScript
87 lines
5.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { loadWasmProbe } from './wasm-probe-runtime.mjs';
|
|
|
|
const root = process.cwd();
|
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'part-native-route-'));
|
|
const output = path.join(root, 'docs/current/part-native-route-diagnostics', new Date().toISOString().replace(/[:.]/g, '-'));
|
|
fs.mkdirSync(output, { recursive: true });
|
|
const checks = [];
|
|
let kernel;
|
|
try {
|
|
execFileSync(path.join(root, 'web/node_modules/.bin/tsc'), ['--target', 'ES2022', '--module', 'ESNext',
|
|
'--moduleResolution', 'Bundler', '--strict', '--skipLibCheck', '--outDir', temporary,
|
|
'web/node_modules/vite/client.d.ts', 'web/src/part-desktop-adapter.ts'], { cwd: root, stdio: 'pipe' });
|
|
const adapter = await import(pathToFileURL(path.join(temporary, 'part-desktop-adapter.js')));
|
|
const family = await import(pathToFileURL(path.join(temporary, 'part-python-family-facade.js')));
|
|
const facade = await import(pathToFileURL(path.join(temporary, 'facade-envelope.js')));
|
|
({ kernel } = await loadWasmProbe('freecad-source/build-web/configure/bin', 'FreeCADWebPartBoxProbe'));
|
|
const dispatch = (cmd, args) => JSON.parse(kernel.ccall('fc_web_dispatch_json', 'string', ['string'], [JSON.stringify({ cmd, args })]));
|
|
let calls = 0;
|
|
const router = { request: async (module, command, payload) => {
|
|
assert.equal(module, 'Part');
|
|
assert.equal(command, family.PART_PYTHON_FAMILY_COMMAND);
|
|
++calls;
|
|
try { return { kind: 'result', result: await family.handlePartPythonFamily(payload, new AbortController().signal, kernel) }; }
|
|
catch (error) { return facade.createFailure('diagnostic', error.code, error.message); }
|
|
} };
|
|
const run = (command, args) => adapter.executePartFixedBoxDesktop(router, command, args, 'RouteDocument');
|
|
const check = (name, actual, passed) => {
|
|
checks.push({ name, actual, passed: Boolean(passed) });
|
|
assert.ok(passed, `${name}: ${JSON.stringify(actual)}`);
|
|
};
|
|
for (const points of [[[0, 0, 0], [10, 0, 0], [10, 10, 0], [0, 10, 0]],
|
|
[[5, 7, 3], [17, 7, 3], [17, 11, 3]]]) {
|
|
const reply = await run('Part_Builder', { points, action: 'accept' });
|
|
const value = reply.result;
|
|
check('Builder batch route returns valid native BREP without claiming document mutation', reply,
|
|
reply.kind === 'result' && value.valid && value.brep && value.documentOperation === 'build-shape' &&
|
|
value.resultScope === 'native-geometry' && value.documentChanged === false);
|
|
const measured = dispatch('Part_ShapeInfo', { shapes: [value.brep] });
|
|
check('Independent native BREP read measures actual face area', measured,
|
|
measured.ok && Math.abs(measured.result.surfaceArea - (points.length === 4 ? 100 : 24)) < 1e-6);
|
|
}
|
|
const crossed = await run('Part_Builder', { points: [[0, 0, 0], [10, 10, 0], [0, 10, 0], [10, 0, 0]] });
|
|
check('Invalid builder shape preserves the native error through facade', crossed,
|
|
crossed.kind === 'error' && crossed.error.code === 'command_failed' && /shape builder/.test(crossed.error.message));
|
|
const box = dispatch('Part_Box', { length: 10, width: 20, height: 30 });
|
|
assert.equal(box.ok, true);
|
|
const selection = [{ documentId: 'RouteDocument', object: 'Box', subelements: ['Edge1'] }];
|
|
const chamfer = await run('Part_Chamfer', { shapes: [box.result.brep], selection, distance: 1 });
|
|
check('Chamfer batch route executes the selected native edge', chamfer,
|
|
chamfer.kind === 'result' && chamfer.result.valid && chamfer.result.volume > 0 && chamfer.result.volume < 6000);
|
|
const checked = await run('Part_CheckGeometry', { shapes: [chamfer.result.brep], selection });
|
|
check('Geometry checking reparses the real chamfer output', checked,
|
|
checked.kind === 'result' && checked.result.valid && checked.result.documentOperation === 'check-geometry');
|
|
const corrupt = await run('Part_CheckGeometry', { shapes: ['not a BREP'], selection });
|
|
check('Corrupt geometry is rejected instead of returning placeholder validity', corrupt,
|
|
corrupt.kind === 'error' && corrupt.error.code === 'command_failed');
|
|
const before = calls;
|
|
const mismatch = await run('Part_Builder', { points: [[0, 0, 0], [1, 0, 0], [0, 1, 0]],
|
|
selection: [{ documentId: 'OtherDocument', object: 'Box' }] });
|
|
check('Cross-document selection never reaches the native kernel', mismatch, mismatch.kind === 'error' && calls === before);
|
|
const stale = { request: async () => ({ kind: 'result', result: { schema: family.PART_PYTHON_FAMILY_SCHEMA,
|
|
records: [{ apiId: 'Part:desktop:Part_Builder', name: 'Part_Builder', status: 'exposed',
|
|
result: { documentOperation: 'build-shape', selectionCount: 0 } }] } }) };
|
|
const placeholder = await adapter.executePartFixedBoxDesktop(stale, 'Part_Builder',
|
|
{ points: [[0, 0, 0], [1, 0, 0], [0, 1, 0]] }, 'RouteDocument');
|
|
check('Legacy metadata-only response remains rejected', placeholder,
|
|
placeholder.kind === 'error' && placeholder.error.code === 'native_protocol_error');
|
|
fs.writeFileSync(path.join(output, 'result.json'), JSON.stringify({ status: 'passed',
|
|
scope: 'Native geometry routing only; not full desktop command business acceptance', checks, kernelLoads: 1 }, null, 2));
|
|
console.log(JSON.stringify({ status: 'passed', checks: checks.length, kernelLoads: 1, output }));
|
|
} catch (error) {
|
|
fs.writeFileSync(path.join(output, 'failure.json'), JSON.stringify({ error: String(error), checks,
|
|
stdout: error.stdout?.toString(), stderr: error.stderr?.toString() }, null, 2));
|
|
console.error(String(error));
|
|
process.exitCode = 1;
|
|
} finally {
|
|
kernel?.PThread?.terminateAllThreads();
|
|
fs.rmSync(temporary, { recursive: true, force: true });
|
|
}
|
|
process.exit(process.exitCode ?? 0);
|