'use strict'; import test from 'node:test'; import assert from 'node:assert/strict'; import { TURN_STATES, TURN_EVENTS, isTurnState, nextState, statusLabel, } from '../src/turn-state.js'; // Every state the machine can be in when a delta arrives. const BUSY_STATES = [ TURN_STATES.CONNECTING, TURN_STATES.THINKING, TURN_STATES.WORKING, TURN_STATES.STREAMING, ]; const DELTA_EVENTS = [TURN_EVENTS.REASONING, TURN_EVENTS.TOOL_CALL, TURN_EVENTS.CONTENT]; test('the state set is closed and enumerated', () => { assert.deepEqual(Object.values(TURN_STATES), [ 'idle', 'connecting', 'thinking', 'working', 'streaming', ]); for (const state of Object.values(TURN_STATES)) { assert.equal(isTurnState(state), true, `${state} is a known state`); } for (const value of ['', 'thinking…', 'IDLE', null, undefined, 3, {}]) { assert.equal(isTurnState(value), false, `${String(value)} is not a state`); } }); test('turn_start always yields connecting, from any state', () => { for (const current of Object.values(TURN_STATES)) { assert.equal( nextState(current, TURN_EVENTS.TURN_START), TURN_STATES.CONNECTING, `turn_start from ${current}`, ); } }); test('turn end, cancel, and error always resolve to idle', () => { for (const current of Object.values(TURN_STATES)) { for (const event of [TURN_EVENTS.TURN_END, TURN_EVENTS.CANCEL, TURN_EVENTS.ERROR]) { assert.equal( nextState(current, event), TURN_STATES.IDLE, `${event} from ${current}`, ); } } }); test('each delta kind maps to its state from every state', () => { const expected = { [TURN_EVENTS.REASONING]: TURN_STATES.THINKING, [TURN_EVENTS.TOOL_CALL]: TURN_STATES.WORKING, [TURN_EVENTS.CONTENT]: TURN_STATES.STREAMING, }; for (const from of [...BUSY_STATES, TURN_STATES.IDLE]) { for (const event of DELTA_EVENTS) { assert.equal( nextState(from, event), expected[event], `${event} from ${from}`, ); } } }); test('a later delta moves the state backward, never stalling on the furthest point', () => { // Streaming back to thinking, and thinking back to working. assert.equal( nextState(TURN_STATES.STREAMING, TURN_EVENTS.REASONING), TURN_STATES.THINKING, 'a reasoning delta after content moves back to thinking', ); assert.equal( nextState(TURN_STATES.THINKING, TURN_EVENTS.TOOL_CALL), TURN_STATES.WORKING, 'a tool-call delta after reasoning moves back to working', ); assert.equal( nextState(TURN_STATES.WORKING, TURN_EVENTS.CONTENT), TURN_STATES.STREAMING, 'content after a tool call moves forward to streaming', ); }); test('a repeated delta of the same kind still updates the state', () => { for (const event of DELTA_EVENTS) { const first = nextState(TURN_STATES.CONNECTING, event); const again = nextState(first, event); assert.equal(again, first, `${event} is idempotent on its own state`); } }); test('a turn with no reasoning and no tools goes connecting -> streaming', () => { let state = nextState(TURN_STATES.IDLE, TURN_EVENTS.TURN_START); assert.equal(state, TURN_STATES.CONNECTING); for (let i = 0; i < 3; i++) state = nextState(state, TURN_EVENTS.CONTENT); assert.equal(state, TURN_STATES.STREAMING); state = nextState(state, TURN_EVENTS.TURN_END); assert.equal(state, TURN_STATES.IDLE); }); test('a reasoning-then-tool turn goes connecting -> thinking -> working', () => { let state = nextState(TURN_STATES.IDLE, TURN_EVENTS.TURN_START); const states = [state]; for (const event of [TURN_EVENTS.REASONING, TURN_EVENTS.TOOL_CALL, TURN_EVENTS.TURN_END]) { state = nextState(state, event); states.push(state); } assert.deepEqual(states, ['connecting', 'thinking', 'working', 'idle']); }); test('no turn ever begins in a busy state and no delta strands the session', () => { for (const terminal of [TURN_EVENTS.TURN_END, TURN_EVENTS.CANCEL, TURN_EVENTS.ERROR]) { const state = nextState(TURN_STATES.STREAMING, terminal); const restarted = nextState(state, TURN_EVENTS.TURN_START); assert.equal(restarted, TURN_STATES.CONNECTING, 'a new turn always starts at connecting'); } }); test('the reducer is total: an unknown event never throws and never invents a state', () => { for (const current of [...Object.values(TURN_STATES), 'bogus', undefined, null]) { for (const event of ['nope', undefined, null, 42]) { const next = nextState(current, event); assert.equal(isTurnState(next), true, `unknown event keeps a real state`); } } // A known current state survives an unknown event; an invalid one resets. assert.equal(nextState(TURN_STATES.WORKING, 'nope'), TURN_STATES.WORKING); assert.equal(nextState('bogus', 'nope'), TURN_STATES.IDLE); }); test('statusLabel names each busy state and leaves idle to the placeholder', () => { assert.equal(statusLabel(TURN_STATES.CONNECTING), 'connecting…'); assert.equal(statusLabel(TURN_STATES.THINKING), 'thinking…'); assert.equal(statusLabel(TURN_STATES.WORKING), 'working…'); assert.equal(statusLabel(TURN_STATES.STREAMING), 'streaming…'); assert.equal(statusLabel(TURN_STATES.IDLE), '', 'idle is not rendered by the spinner'); assert.equal(statusLabel('bogus'), '', 'an unknown value renders no busy label'); });