import { describe, expect, it } from 'vitest'; import { canStartQueuedPrompt, createIdleSessionRunState, didSuppressAbortError, isCurrentRunEvent, isSessionRunActive, takeNextQueuedPrompt, transitionSessionRunState, } from '@/stores/opencode-session-run-machine'; type TestPrompt = { id: string; text: string; }; describe('opencode session run machine', () => { it('starts a run and marks it active while posting or running', () => { const idle = createIdleSessionRunState(); const posting = transitionSessionRunState(idle, { type: 'send_started', runId: 1, promptId: 'msg_1', }); expect(posting).toMatchObject({ phase: 'posting', runId: 1, promptId: 'msg_1', terminalReason: null, error: null, suppressNextAbortError: false, }); expect(isSessionRunActive(posting)).toBe(true); const running = transitionSessionRunState(posting, { type: 'post_accepted', runId: 1, }); expect(running.phase).toBe('running'); expect(isSessionRunActive(running)).toBe(true); }); it('queues and dequeues prompts without changing the current run', () => { const running = transitionSessionRunState( createIdleSessionRunState(), { type: 'send_started', runId: 2, promptId: 'msg_active' }, ); const withQueue = transitionSessionRunState(running, { type: 'queue_prompt', prompt: { id: 'queued_1', text: 'second' }, }); expect(withQueue.queue).toEqual([{ id: 'queued_1', text: 'second' }]); expect(withQueue.runId).toBe(2); expect(canStartQueuedPrompt(withQueue)).toBe(false); const completed = transitionSessionRunState(withQueue, { type: 'remote_idle', runId: 2, }); const taken = takeNextQueuedPrompt(completed); expect(canStartQueuedPrompt(completed)).toBe(true); expect(taken.prompt).toEqual({ id: 'queued_1', text: 'second' }); expect(taken.state.queue).toEqual([]); }); it('clears queued prompts immediately when abort is requested', () => { const running = transitionSessionRunState( createIdleSessionRunState([{ id: 'queued_1', text: 'queued' }]), { type: 'send_started', runId: 3, promptId: 'msg_active' }, ); const aborting = transitionSessionRunState(running, { type: 'abort_requested', runId: 3, }); expect(aborting).toMatchObject({ phase: 'aborting', runId: 3, queue: [], terminalReason: 'aborted', error: null, suppressNextAbortError: true, }); expect(isSessionRunActive(aborting)).toBe(false); }); it('ignores stale run-scoped events from an older run after a new prompt starts', () => { const firstRun = transitionSessionRunState( createIdleSessionRunState(), { type: 'send_started', runId: 4, promptId: 'msg_first' }, ); const aborting = transitionSessionRunState(firstRun, { type: 'abort_requested', runId: 4, }); const secondRun = transitionSessionRunState(aborting, { type: 'send_started', runId: 5, promptId: 'msg_second', }); expect(isCurrentRunEvent(secondRun, 4)).toBe(false); expect(isCurrentRunEvent(secondRun, 5)).toBe(true); const afterStaleAbort = transitionSessionRunState(secondRun, { type: 'remote_aborted', runId: 4, }); expect(afterStaleAbort).toEqual(secondRun); }); it('suppresses one late abort-like error delivered to the new run after an explicit stop', () => { const firstRun = transitionSessionRunState( createIdleSessionRunState(), { type: 'send_started', runId: 7, promptId: 'msg_first' }, ); const aborting = transitionSessionRunState(firstRun, { type: 'abort_requested', runId: 7, }); const secondRun = transitionSessionRunState(aborting, { type: 'send_started', runId: 8, promptId: 'msg_second', }); expect(secondRun.suppressNextAbortError).toBe(true); const afterLateAbort = transitionSessionRunState(secondRun, { type: 'remote_aborted', runId: 8, }); expect(afterLateAbort).toMatchObject({ phase: 'posting', runId: 8, promptId: 'msg_second', terminalReason: null, error: null, suppressNextAbortError: false, }); expect(didSuppressAbortError(secondRun, afterLateAbort)).toBe(true); }); it('keeps provider failures visible but still makes the run inactive', () => { const running = transitionSessionRunState( createIdleSessionRunState(), { type: 'send_started', runId: 6, promptId: 'msg_fail' }, ); const failed = transitionSessionRunState(running, { type: 'remote_failed', runId: 6, error: 'Authentication failed', }); expect(failed).toMatchObject({ phase: 'idle', runId: 6, terminalReason: 'failed', error: 'Authentication failed', }); expect(isSessionRunActive(failed)).toBe(false); expect(canStartQueuedPrompt(failed)).toBe(false); }); it('does not let post acceptance reopen a terminal run', () => { const posting = transitionSessionRunState( createIdleSessionRunState(), { type: 'send_started', runId: 9, promptId: 'compact-ses_1' }, ); const completed = transitionSessionRunState(posting, { type: 'remote_idle', runId: 9, }); const afterLatePost = transitionSessionRunState(completed, { type: 'post_accepted', runId: 9, }); expect(afterLatePost).toEqual(completed); expect(afterLatePost).toMatchObject({ phase: 'idle', terminalReason: 'completed', }); }); it.each([ [ 'failure', { type: 'remote_failed' as const, runId: 10, error: 'POST failed after early idle' }, { phase: 'idle', queue: [{ id: 'queued_1', text: 'second' }], terminalReason: 'failed', error: 'POST failed after early idle', }, ], [ 'abort', { type: 'remote_aborted' as const, runId: 10 }, { phase: 'idle', queue: [], terminalReason: 'aborted', error: null, }, ], ])('lets authoritative %s override an early-idle completion', ( _caseName, terminalEvent, expected, ) => { const posting = transitionSessionRunState( createIdleSessionRunState([{ id: 'queued_1', text: 'second' }]), { type: 'send_started', runId: 10, promptId: 'msg_first' }, ); const earlyIdle = transitionSessionRunState(posting, { type: 'remote_idle', runId: 10, }); const terminal = transitionSessionRunState(earlyIdle, terminalEvent); expect(terminal).toMatchObject(expected); expect(canStartQueuedPrompt(terminal)).toBe(false); }); });