232 lines
6.7 KiB
TypeScript
232 lines
6.7 KiB
TypeScript
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<TestPrompt>();
|
|
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<TestPrompt>(),
|
|
{ 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<TestPrompt>([{ 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<TestPrompt>(),
|
|
{ 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<TestPrompt>(),
|
|
{ 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<TestPrompt>(),
|
|
{ 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<TestPrompt>(),
|
|
{ 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<TestPrompt>([{ 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);
|
|
});
|
|
});
|