| 1 | // Golden tests: dotter must send exactly the commands the official NextDraw
|
| 2 | // Python API sends for the same scenario (status polls excluded, since their
|
| 3 | // count depends on timing). Fixtures are recorded by
|
| 4 | // scripts/golden/gen_golden.py; scenarios without a fixture are skipped.
|
| 5 |
|
| 6 | import { describe, expect, test } from 'bun:test';
|
| 7 | import { existsSync, readFileSync } from 'node:fs';
|
| 8 | import { join } from 'node:path';
|
| 9 | import { commands, makeMachine } from './helpers.js';
|
| 10 |
|
| 11 | const dir = join(import.meta.dir, 'golden');
|
| 12 | const scenarios = JSON.parse(readFileSync(join(dir, 'scenarios.json'), 'utf8'));
|
| 13 |
|
| 14 | const OPTION_NAMES = {
|
| 15 | handling: 'handling', speed_pendown: 'speedPendown', speed_penup: 'speedPenup', accel: 'accel',
|
| 16 | pen_pos_up: 'penPosUp', pen_pos_down: 'penPosDown', pen_rate_lower: 'penRateLower',
|
| 17 | pen_rate_raise: 'penRateRaise', units: 'units', model: 'model',
|
| 18 | };
|
| 19 | const OP_NAMES = { draw_path: 'drawPath' };
|
| 20 |
|
| 21 | export async function runScenario(spec) {
|
| 22 | const { nd, sim } = makeMachine({}, (n) => {
|
| 23 | n.options.homing = false;
|
| 24 | for (const [k, v] of Object.entries(spec.options)) n.options[OPTION_NAMES[k]] = v;
|
| 25 | });
|
| 26 | if (!(await nd.connect())) throw new Error('connect failed');
|
| 27 | for (const [op, ...args] of spec.ops) {
|
| 28 | if (op === 'draw_path_circle') {
|
| 29 | const [cx, cy, r, n] = args;
|
| 30 | const pts = [];
|
| 31 | for (let i = 0; i <= n; i++) pts.push([cx + r * Math.cos(2 * Math.PI * i / n), cy + r * Math.sin(2 * Math.PI * i / n)]);
|
| 32 | await nd.drawPath(pts);
|
| 33 | } else {
|
| 34 | await nd[OP_NAMES[op] ?? op](...args);
|
| 35 | }
|
| 36 | }
|
| 37 | await nd.disconnect();
|
| 38 | return { cmds: commands(sim), sim };
|
| 39 | }
|
| 40 |
|
| 41 | describe('golden command streams vs. the Python API', () => {
|
| 42 | for (const [name, spec] of Object.entries(scenarios)) {
|
| 43 | const fixture = join(dir, 'expected', `${name}.json`);
|
| 44 | const run = existsSync(fixture) ? test : test.skip;
|
| 45 | run(name, async () => {
|
| 46 | const expected = JSON.parse(readFileSync(fixture, 'utf8'));
|
| 47 | const { cmds } = await runScenario(spec);
|
| 48 | expect(cmds).toEqual(expected);
|
| 49 | });
|
| 50 | }
|
| 51 |
|
| 52 | // These run regardless: every scenario must be safe on the simulator.
|
| 53 | for (const [name, spec] of Object.entries(scenarios)) {
|
| 54 | test(`${name} is safe on the simulator`, async () => {
|
| 55 | const { sim } = await runScenario(spec);
|
| 56 | expect(sim.violations).toEqual([]);
|
| 57 | });
|
| 58 | }
|
| 59 | });
|