// tests/lib/autonomy-gate.test.mjs // Cover the autonomy-gate state machine (lib/util/autonomy-gate.mjs): // every legal transition + every invalid-transition error + idempotent // re-entry to `completed` + CLI-shim JSON-on-stdout contract. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { execFileSync } from 'node:child_process'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { transition, isTerminal, STATES, EVENTS } from '../../lib/util/autonomy-gate.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const SHIM = join(HERE, '..', '..', 'lib', 'util', 'autonomy-gate.mjs'); function runShim(args) { try { const out = execFileSync(process.execPath, [SHIM, ...args], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], }); return { code: 0, out }; } catch (e) { return { code: e.status ?? 1, out: e.stdout?.toString() ?? '' }; } } test('idle + start + gates=true → gates_on', () => { const r = transition(STATES.IDLE, EVENTS.START, { gates: true }); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.GATES_ON); }); test('idle + start + gates=false → auto_running', () => { const r = transition(STATES.IDLE, EVENTS.START, { gates: false }); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.AUTO_RUNNING); }); test('idle + start + gates omitted defaults to auto_running', () => { const r = transition(STATES.IDLE, EVENTS.START); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.AUTO_RUNNING); }); test('gates_on + phase_boundary → paused_for_gate', () => { const r = transition(STATES.GATES_ON, EVENTS.PHASE_BOUNDARY); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.PAUSED_FOR_GATE); }); test('gates_on + finish → completed', () => { const r = transition(STATES.GATES_ON, EVENTS.FINISH); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.COMPLETED); }); test('auto_running + phase_boundary → auto_running (no pause)', () => { const r = transition(STATES.AUTO_RUNNING, EVENTS.PHASE_BOUNDARY); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.AUTO_RUNNING); }); test('auto_running + finish → completed', () => { const r = transition(STATES.AUTO_RUNNING, EVENTS.FINISH); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.COMPLETED); }); test('paused_for_gate + resume → gates_on', () => { const r = transition(STATES.PAUSED_FOR_GATE, EVENTS.RESUME); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.GATES_ON); }); test('paused_for_gate + finish → completed', () => { const r = transition(STATES.PAUSED_FOR_GATE, EVENTS.FINISH); assert.equal(r.ok, true); assert.equal(r.next_state, STATES.COMPLETED); }); test('completed + any event → completed (idempotent re-entry)', () => { for (const ev of Object.values(EVENTS)) { const r = transition(STATES.COMPLETED, ev); assert.equal(r.ok, true, `event ${ev} should be tolerated from completed`); assert.equal(r.next_state, STATES.COMPLETED, `event ${ev} broke idempotency`); } }); test('idle + non-start event → invalid transition error', () => { const r = transition(STATES.IDLE, EVENTS.PHASE_BOUNDARY); assert.equal(r.ok, false); assert.match(r.error, /invalid transition.*idle/); }); test('gates_on + resume → invalid (resume is only valid from paused_for_gate)', () => { const r = transition(STATES.GATES_ON, EVENTS.RESUME); assert.equal(r.ok, false); }); test('auto_running + resume → invalid (auto-mode never pauses)', () => { const r = transition(STATES.AUTO_RUNNING, EVENTS.RESUME); assert.equal(r.ok, false); }); test('unknown state rejected with descriptive error', () => { const r = transition('zombie', EVENTS.START); assert.equal(r.ok, false); assert.match(r.error, /unknown state/); }); test('unknown event rejected with descriptive error', () => { const r = transition(STATES.IDLE, 'snooze'); assert.equal(r.ok, false); assert.match(r.error, /unknown event/); }); test('isTerminal — only completed is terminal', () => { assert.equal(isTerminal(STATES.COMPLETED), true); for (const s of [STATES.IDLE, STATES.GATES_ON, STATES.AUTO_RUNNING, STATES.PAUSED_FOR_GATE]) { assert.equal(isTerminal(s), false, `${s} should not be terminal`); } }); test('CLI shim returns valid JSON on success (exit 0)', () => { const r = runShim(['--state', 'idle', '--event', 'start', '--gates', 'true']); assert.equal(r.code, 0); const parsed = JSON.parse(r.out.trim()); assert.equal(parsed.ok, true); assert.equal(parsed.next_state, 'gates_on'); }); test('CLI shim returns JSON error on invalid transition (exit 1)', () => { const r = runShim(['--state', 'idle', '--event', 'phase_boundary']); assert.equal(r.code, 1); const parsed = JSON.parse(r.out.trim()); assert.equal(parsed.ok, false); assert.match(parsed.error, /invalid transition/); }); test('CLI shim missing required args returns usage error (exit 1)', () => { const r = runShim(['--state', 'idle']); assert.equal(r.code, 1); const parsed = JSON.parse(r.out.trim()); assert.equal(parsed.ok, false); assert.match(parsed.error, /usage:/); });