velxio/frontend/src/__tests__/load-ngspice-for-node.test.ts

49 lines
1.7 KiB
TypeScript
Raw Normal View History

feat(sim): Phase 1c F1 — NgSpiceNodeAdapter runs real WASM in Node tests Loads the vendored ngspice-interactive WASM directly in the Vitest Node process, no Web Worker required. Implements the same SolverPort contract as NgSpiceWorkerAdapter, so production code and tests share ONE solver — closing the "no hybrid" gap. loadNgSpiceForNode (Node-only loader): - Reads ngspice-lib.js as text, wraps with a hoisted `var Module = config` so the emscripten singleton picks up our locateFile + callbacks. - Re-wires Module.onRuntimeInitialized to copy closure-local FS / HEAP* into Module._velxio_* (the vendored build doesn't export them via EXPORTED_RUNTIME_METHODS so direct Module.FS triggers an abort accessor). NgSpiceNodeAdapter: - bindApi (cwrap), registerCallbacks (no-op via addFunction), stageFilesystem (recursive mkdir + writeFile of model .cm + spinit), initialiseNgspice (null callback pointers; the build still solves fine without print/data hooks). - loadCircuit writes the netlist to /circuit.spc on the FS and issues `source /circuit.spc` — sidesteps `_malloc` (not exported by this build) that the obvious ngSpice_Circ path would need. - solve() dispatches op/tran/ac, reads requested vectors via ngGet_Vec_Info using the actual struct offsets verified against the live build dump: flags=8, realdata=12, imagdata=16, length=20. - alterSource issues `alter` for incremental re-solves. 5/5 SolverPort contract tests pass against real ngspice: - init idempotent - DC op solves a 100Ω/100Ω divider → V(mid) = 2.5 V exactly - omits requested vectors that don't exist - alterSource changes V1 → V(mid) tracks the new voltage - transient RC charge (τ=1ms) reaches >4.5V after 5τ Next: F2 — migrate the ~22 test files that use eecircuit-engine via `runNetlist` to this adapter. After F2, F3 deletes eecircuit-engine and `SpiceEngine.ts` for good. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 01:49:39 +07:00
/**
* Phase 1c F1 smoke test — does the vendored WASM load cleanly in
* the Vitest Node process? If this fails, the entire Node adapter
* path is dead and we have to rethink F1.
*/
import { describe, it, expect } from 'vitest';
import {
loadNgSpiceForNode,
__resetNgSpiceForTests,
} from '../simulation/spice/adapters/node/loadNgSpiceForNode';
describe('Phase 1c F1 — Node WASM loader', () => {
it(
'boots the ngspice emscripten module and exposes cwrap',
{ timeout: 30_000 },
async () => {
__resetNgSpiceForTests();
const stdout: string[] = [];
const stderr: string[] = [];
const Module = await loadNgSpiceForNode({
onStdout: (t) => stdout.push(t),
onStderr: (t) => stderr.push(t),
});
expect(typeof Module.cwrap).toBe('function');
expect(typeof Module.addFunction).toBe('function');
expect(typeof Module.UTF8ToString).toBe('function');
// The vendored build doesn't export FS; the loader exposes it
// via _velxio_fs to esquivar the abort accessor.
expect(Module._velxio_fs).toBeDefined();
expect(typeof Module._velxio_fs?.writeFile).toBe('function');
},
);
it('cwrap binds ngSpice_Reset (a known no-arg ngspice symbol)', async () => {
const Module = await loadNgSpiceForNode();
const reset = Module.cwrap('ngSpice_Reset', 'number', []);
expect(typeof reset).toBe('function');
});
it('addFunction is callable and returns a function-table pointer', async () => {
const Module = await loadNgSpiceForNode();
expect(typeof Module.addFunction).toBe('function');
const noop = () => 0;
const ptr = Module.addFunction(noop, 'iiii');
expect(typeof ptr).toBe('number');
expect(ptr).toBeGreaterThan(0);
});
});