Files
makelore/tests/unit/opencode-session-run-machine.test.ts
2026-07-29 17:22:35 +08:00

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);
});
});