175 lines
5.0 KiB
TypeScript
175 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
getOrderedSessionParts,
|
|
hydrateOpenCodeSession,
|
|
reduceOpenCodeEvent,
|
|
} from '@/lib/opencode-session-state';
|
|
|
|
describe('OpenCode native session state', () => {
|
|
it('hydrates native parts without dropping execution metadata', () => {
|
|
const state = hydrateOpenCodeSession('ses_1', [{
|
|
info: {
|
|
id: 'msg_1',
|
|
role: 'assistant',
|
|
sessionID: 'ses_1',
|
|
time: { created: 1_700_000_000_000 },
|
|
},
|
|
parts: [
|
|
{ id: 'text_1', type: 'text', text: 'Final answer' },
|
|
{ id: 'reason_1', type: 'reasoning', text: 'Checking the repository' },
|
|
{
|
|
id: 'tool_1',
|
|
type: 'tool',
|
|
tool: 'read',
|
|
callID: 'call_1',
|
|
state: {
|
|
status: 'completed',
|
|
input: { path: 'README.md' },
|
|
output: 'ok',
|
|
time: { start: 1_700_000_000_010, end: 1_700_000_000_110 },
|
|
},
|
|
},
|
|
{ id: 'step_1', type: 'step-finish', reason: 'stop', cost: 0.01 },
|
|
{ id: 'future_1', type: 'future-native-part', detail: 'preserve me' },
|
|
],
|
|
}]);
|
|
|
|
const parts = getOrderedSessionParts(state, 'msg_1');
|
|
expect(parts.map((part) => part.type)).toEqual([
|
|
'text',
|
|
'reasoning',
|
|
'tool',
|
|
'step-finish',
|
|
'unknown',
|
|
]);
|
|
expect(parts[2]).toEqual(expect.objectContaining({
|
|
tool: 'read',
|
|
callID: 'call_1',
|
|
state: expect.objectContaining({ status: 'completed' }),
|
|
}));
|
|
expect(parts.at(-1)).toEqual(expect.objectContaining({
|
|
rawType: 'future-native-part',
|
|
detail: expect.stringContaining('preserve me'),
|
|
}));
|
|
});
|
|
|
|
it('applies full updates, deltas, and removals by stable part id', () => {
|
|
const base = hydrateOpenCodeSession('ses_1', []);
|
|
const updated = reduceOpenCodeEvent(base, {
|
|
type: 'message.part.updated',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
part: { id: 'part_1', type: 'text', text: 'Hello' },
|
|
},
|
|
});
|
|
const withDelta = reduceOpenCodeEvent(updated, {
|
|
type: 'message.part.delta',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
partID: 'part_1',
|
|
field: 'text',
|
|
delta: ' world',
|
|
},
|
|
});
|
|
const duplicateDelta = reduceOpenCodeEvent(withDelta, {
|
|
type: 'message.part.delta',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
partID: 'part_1',
|
|
field: 'text',
|
|
delta: ' world',
|
|
},
|
|
});
|
|
|
|
expect(getOrderedSessionParts(duplicateDelta, 'msg_1')[0]?.text).toBe('Hello world');
|
|
|
|
const removed = reduceOpenCodeEvent(duplicateDelta, {
|
|
type: 'message.part.removed',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
partID: 'part_1',
|
|
},
|
|
});
|
|
expect(getOrderedSessionParts(removed, 'msg_1')).toEqual([]);
|
|
});
|
|
|
|
it('ignores events from another session and tracks native lifecycle status', () => {
|
|
const state = hydrateOpenCodeSession('ses_1', []);
|
|
const untouched = reduceOpenCodeEvent(state, {
|
|
type: 'message.part.delta',
|
|
payload: {
|
|
sessionID: 'ses_2',
|
|
messageID: 'msg_2',
|
|
partID: 'part_2',
|
|
delta: 'wrong session',
|
|
},
|
|
});
|
|
expect(untouched).toBe(state);
|
|
|
|
const busy = reduceOpenCodeEvent(state, {
|
|
type: 'session.status',
|
|
payload: { sessionID: 'ses_1', status: { type: 'retry' } },
|
|
});
|
|
expect(busy?.status).toBe('retry');
|
|
|
|
const idle = reduceOpenCodeEvent(busy, {
|
|
type: 'session.idle',
|
|
payload: { sessionID: 'ses_1' },
|
|
});
|
|
expect(idle?.status).toBe('idle');
|
|
});
|
|
|
|
it('deduplicates replayed delta events by event identity without dropping equal real chunks', () => {
|
|
let state = hydrateOpenCodeSession('ses_1', []);
|
|
state = reduceOpenCodeEvent(state, {
|
|
type: 'message.part.updated',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
part: { id: 'part_1', type: 'text', text: '' },
|
|
},
|
|
});
|
|
state = reduceOpenCodeEvent(state, {
|
|
type: 'message.part.delta',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
partID: 'part_1',
|
|
delta: 'x',
|
|
eventID: 'evt_1',
|
|
},
|
|
});
|
|
state = reduceOpenCodeEvent(state, {
|
|
type: 'session.status',
|
|
payload: { sessionID: 'ses_1', status: { type: 'busy' } },
|
|
});
|
|
state = reduceOpenCodeEvent(state, {
|
|
type: 'message.part.delta',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
partID: 'part_1',
|
|
delta: 'x',
|
|
eventID: 'evt_1',
|
|
},
|
|
});
|
|
expect(getOrderedSessionParts(state, 'msg_1')[0]?.text).toBe('x');
|
|
|
|
const secondRealChunk = reduceOpenCodeEvent(state, {
|
|
type: 'message.part.delta',
|
|
payload: {
|
|
sessionID: 'ses_1',
|
|
messageID: 'msg_1',
|
|
partID: 'part_1',
|
|
delta: 'x',
|
|
eventID: 'evt_2',
|
|
},
|
|
});
|
|
expect(getOrderedSessionParts(secondRealChunk, 'msg_1')[0]?.text).toBe('xx');
|
|
});
|
|
});
|