import test from 'node:test' import assert from 'node:assert/strict' import { CAMOTICS_SOURCE_REVISION, OPEN_CAM_LIB_ARTIFACT_SHA256, generateCamoticsSourceStageGcode, nativeCamCapabilities, runOpenCamLibDropCutter, sampleFlatEndPath, } from '../src/facade/camNativeSimulation' test('native CAM capability report distinguishes workspace CAMotics CLI from browser execution', () => { const capabilities = nativeCamCapabilities() assert.equal(capabilities.openCamLib.backend, 'upstream-opencamlib-wasm') assert.equal(capabilities.openCamLib.artifactSha256, OPEN_CAM_LIB_ARTIFACT_SHA256) assert.deepEqual(capabilities.openCamLib.algorithms, ['path-drop-cutter', 'adaptive-path-drop-cutter', 'waterline', 'adaptive-waterline']) assert.equal(capabilities.solidRemoval.backend, 'bitbybit-occt-wasm') assert.equal(capabilities.solidRemoval.nativeSolid, true) assert.equal(capabilities.solidRemoval.freeCadNativeEquivalent, false) assert.deepEqual(capabilities.solidRemoval.sweepModes, ['sampled-flat-end', 'continuous-segment']) assert.equal(capabilities.camotics.status, 'native-host-available') assert.equal(capabilities.camotics.backend, 'camotics-native-host-qt5-tpl') assert.equal(capabilities.camotics.workspaceNativeExecutable, true) assert.equal(capabilities.camotics.nativeGuiPath, 'CAMotics/camotics') assert.equal(capabilities.camotics.nativeTplPath, 'CAMotics/tplang') assert.equal(capabilities.camotics.nativeArtifactEvidence, 'config/camotics-native-artifact.json') assert.equal(capabilities.camotics.browserExecutable, false) assert.equal(capabilities.camotics.browserWasmKernelExecutable, true) assert.equal(capabilities.camotics.nativeCliPath, 'CAMotics/camsim') assert.equal(capabilities.camotics.wasmKernel.backend, 'camotics-upstream-sweep-wasm') assert.equal(capabilities.camotics.wasmKernel.fullCamoticsProgram, false) assert.equal(capabilities.camotics.wasmKernel.gcodeParserIncluded, false) assert.equal(capabilities.camotics.sourceRevision, CAMOTICS_SOURCE_REVISION) assert.equal(capabilities.camotics.generationAuthority, 'camotics-source-stage') assert.equal(capabilities.camotics.parserAuthority, 'linuxcnc-wasm') assert.equal(capabilities.camotics.camoticsParsesCanonicalGcode, false) assert.match(capabilities.camotics.reason, /workspace-verified/) }) test('CAMotics source stage only generates canonical G-code for LinuxCNC WASM', () => { const result = generateCamoticsSourceStageGcode([ { type: 'rapid', point: [0, 0, 5] }, { type: 'cut', point: [1.25, 2, 3.5] }, { type: 'cut', point: [2, 2, 3.5] }, ], { toolNumber: 2, feed: 120 }) assert.equal(result.backend, 'camotics-source-stage-contract') assert.equal(result.nativeExecutable, false) assert.equal(result.parserAuthority, 'linuxcnc-wasm') assert.equal(result.camoticsParsesCanonicalGcode, false) assert.equal(result.lineCount, 7) assert.equal(result.gcode, 'G21G90\nM6 T2\nG0 X0. Y0. Z5.\nF120.\nG1 X1.25 Y2. Z3.5\nG1 X2.\nM2\n') assert.throws(() => generateCamoticsSourceStageGcode([], { feed: 120 }), /must not be empty/) assert.throws(() => generateCamoticsSourceStageGcode([{ type: 'cut', point: [0, 0, 0] }], { feed: 0 }), /greater than zero/) }) test('flat-end solid-removal sampling has a deterministic chord bound', () => { const samples = sampleFlatEndPath([[0, 0, 1], [3, 4, 1]], 1) assert.equal(samples.length, 6) assert.deepEqual(samples[0], [0, 0, 1]) assert.deepEqual(samples.at(-1), [3, 4, 1]) for (let index = 1; index < samples.length; index += 1) { assert.ok(Math.hypot(...samples[index].map((value, axis) => value - samples[index - 1][axis])) <= 1 + 1e-12) } assert.deepEqual(sampleFlatEndPath([[1, 2, 3]], 0.5), [[1, 2, 3]]) assert.throws(() => sampleFlatEndPath([], 1), /must not be empty/) assert.throws(() => sampleFlatEndPath([[0, 0, 0]], 0), /greater than zero/) }) test('OpenCAMLib adapter validates unsafe input before requesting the browser WASM runtime', async () => { await assert.rejects(() => runOpenCamLibDropCutter({ triangles: [], path: [[0, 0, 0], [1, 0, 0]], cutter: { shape: 'endmill', diameter: 1, length: 10 }, sampling: 0.1 }), /at least one surface triangle/) await assert.rejects(() => runOpenCamLibDropCutter({ triangles: [[[0, 0, 0], [1, 0, 0], [0, 1, 0]]], path: [[0, 0, 0]], cutter: { shape: 'endmill', diameter: 1, length: 10 }, sampling: 0.1 }), /at least two points/) await assert.rejects(() => runOpenCamLibDropCutter({ triangles: [[[0, 0, 0], [1, 0, 0], [0, 1, 0]]], path: [[0, 0, 0], [1, 0, 0]], cutter: { shape: 'endmill', diameter: 0, length: 10 }, sampling: 0.1 }), /greater than zero/) })