Makelore 2.0 initial clean snapshot
This commit is contained in:
231
tests/unit/opencode-session-run-machine.test.ts
Normal file
231
tests/unit/opencode-session-run-machine.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user