实现 Codex 风格上下文压缩时间线交互
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
completeCompaction,
|
||||
createPendingCompaction,
|
||||
getOrderedSessionParts,
|
||||
getOrderedSessionCompactions,
|
||||
hydrateOpenCodeSession,
|
||||
reduceOpenCodeEvent,
|
||||
removeCompaction,
|
||||
} from '@/lib/opencode-session-state';
|
||||
|
||||
describe('OpenCode native session state', () => {
|
||||
@@ -171,4 +175,261 @@ describe('OpenCode native session state', () => {
|
||||
});
|
||||
expect(getOrderedSessionParts(secondRealChunk, 'msg_1')[0]?.text).toBe('xx');
|
||||
});
|
||||
|
||||
it('hydrates historical compaction Parts as completed timeline events in transcript order', () => {
|
||||
const state = hydrateOpenCodeSession('ses_1', [{
|
||||
info: { id: 'msg_1', role: 'assistant', sessionID: 'ses_1' },
|
||||
parts: [
|
||||
{ id: 'compact_1', type: 'compaction', auto: false },
|
||||
{ id: 'compact_2', type: 'compaction', auto: true },
|
||||
],
|
||||
}]);
|
||||
|
||||
expect(getOrderedSessionCompactions(state)).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'compaction:ses_1:part:compact_1',
|
||||
source: 'manual',
|
||||
status: 'completed',
|
||||
order: 0,
|
||||
anchorMessageID: 'msg_1',
|
||||
anchorPartID: 'compact_1',
|
||||
nativePartID: 'compact_1',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
source: 'automatic',
|
||||
status: 'completed',
|
||||
order: 1,
|
||||
nativePartID: 'compact_2',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows only the latest compaction in an active snapshot to remain running', () => {
|
||||
const state = hydrateOpenCodeSession('ses_1', [{
|
||||
info: { id: 'msg_1', role: 'assistant', sessionID: 'ses_1' },
|
||||
parts: [
|
||||
{ id: 'compact_1', type: 'compaction', auto: true },
|
||||
{ id: 'compact_2', type: 'compaction', auto: true },
|
||||
],
|
||||
}], 'busy');
|
||||
|
||||
expect(getOrderedSessionCompactions(state).map((event) => event.status))
|
||||
.toEqual(['completed', 'running']);
|
||||
});
|
||||
|
||||
it('does not downgrade a completed compaction during busy history hydration', () => {
|
||||
const snapshot = [{
|
||||
info: { id: 'msg_1', role: 'assistant', sessionID: 'ses_1' },
|
||||
parts: [{ id: 'compact_1', type: 'compaction', auto: true }],
|
||||
}];
|
||||
const running = hydrateOpenCodeSession('ses_1', snapshot, 'busy');
|
||||
const completed = reduceOpenCodeEvent(running, {
|
||||
type: 'session.compacted',
|
||||
payload: { sessionID: 'ses_1', eventID: 'evt_compacted_1' },
|
||||
});
|
||||
|
||||
const rehydrated = hydrateOpenCodeSession('ses_1', snapshot, 'busy', completed);
|
||||
|
||||
expect(getOrderedSessionCompactions(rehydrated)).toEqual([
|
||||
expect.objectContaining({
|
||||
nativePartID: 'compact_1',
|
||||
status: 'completed',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('merges a manual pending event with its native Part without changing the UI id', () => {
|
||||
const empty = hydrateOpenCodeSession('ses_1', [], 'busy');
|
||||
const pending = createPendingCompaction(empty, {
|
||||
id: 'ui_compact_1',
|
||||
source: 'manual',
|
||||
runID: 'run_1',
|
||||
generation: 3,
|
||||
startedAt: 1_700_000_000_000,
|
||||
});
|
||||
const withNativePart = reduceOpenCodeEvent(pending, {
|
||||
type: 'message.part.updated',
|
||||
payload: {
|
||||
sessionID: 'ses_1',
|
||||
eventID: 'evt_part_1',
|
||||
messageID: 'msg_1',
|
||||
part: {
|
||||
id: 'native_compact_1',
|
||||
type: 'compaction',
|
||||
auto: false,
|
||||
runID: 'run_1',
|
||||
generation: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getOrderedSessionCompactions(withNativePart)).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'ui_compact_1',
|
||||
nativePartID: 'native_compact_1',
|
||||
nativeEventID: 'evt_part_1',
|
||||
anchorMessageID: 'msg_1',
|
||||
status: 'running',
|
||||
}),
|
||||
]);
|
||||
|
||||
const replayed = reduceOpenCodeEvent(withNativePart, {
|
||||
type: 'message.part.updated',
|
||||
payload: {
|
||||
sessionID: 'ses_1',
|
||||
eventID: 'evt_part_2',
|
||||
messageID: 'msg_1',
|
||||
part: { id: 'native_compact_1', type: 'compaction', auto: false },
|
||||
},
|
||||
});
|
||||
expect(getOrderedSessionCompactions(replayed)).toHaveLength(1);
|
||||
expect(getOrderedSessionCompactions(replayed)[0]?.id).toBe('ui_compact_1');
|
||||
});
|
||||
|
||||
it('preserves unmatched running pending events across active hydration only', () => {
|
||||
const current = createPendingCompaction(
|
||||
hydrateOpenCodeSession('ses_1', [], 'busy'),
|
||||
{ id: 'ui_pending', source: 'manual', runID: 'run_1', generation: 1 },
|
||||
);
|
||||
|
||||
const active = hydrateOpenCodeSession('ses_1', [], 'busy', current);
|
||||
expect(getOrderedSessionCompactions(active).map((event) => event.id)).toEqual(['ui_pending']);
|
||||
|
||||
const historical = hydrateOpenCodeSession('ses_1', [], 'idle', current);
|
||||
expect(getOrderedSessionCompactions(historical)).toEqual([]);
|
||||
});
|
||||
|
||||
it('merges an active snapshot Part into the current manual pending UI identity', () => {
|
||||
const current = createPendingCompaction(
|
||||
hydrateOpenCodeSession('ses_1', [], 'busy'),
|
||||
{ id: 'ui_pending', source: 'manual', runID: 'run_1', generation: 1 },
|
||||
);
|
||||
const hydrated = hydrateOpenCodeSession('ses_1', [{
|
||||
info: { id: 'msg_1', role: 'assistant', sessionID: 'ses_1' },
|
||||
parts: [{ id: 'native_compact_1', type: 'compaction', auto: false }],
|
||||
}], 'busy', current);
|
||||
|
||||
expect(getOrderedSessionCompactions(hydrated)).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'ui_pending',
|
||||
nativePartID: 'native_compact_1',
|
||||
status: 'running',
|
||||
runID: 'run_1',
|
||||
generation: 1,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('closes an older running native compaction when a later one arrives', () => {
|
||||
let state = hydrateOpenCodeSession('ses_1', [], 'busy');
|
||||
for (const [messageID, partID] of [['msg_1', 'compact_1'], ['msg_2', 'compact_2']]) {
|
||||
state = reduceOpenCodeEvent(state, {
|
||||
type: 'message.part.updated',
|
||||
payload: {
|
||||
sessionID: 'ses_1',
|
||||
messageID,
|
||||
part: { id: partID, type: 'compaction', auto: true },
|
||||
},
|
||||
})!;
|
||||
}
|
||||
|
||||
expect(getOrderedSessionCompactions(state).map((event) => event.status))
|
||||
.toEqual(['completed', 'running']);
|
||||
});
|
||||
|
||||
it('completes and removes only the compaction matching all supplied correlation keys', () => {
|
||||
let state = createPendingCompaction(
|
||||
hydrateOpenCodeSession('ses_1', [], 'busy'),
|
||||
{ id: 'ui_1', source: 'manual', runID: 'run_1', generation: 1 },
|
||||
);
|
||||
state = createPendingCompaction(state, {
|
||||
id: 'ui_2',
|
||||
source: 'manual',
|
||||
runID: 'run_2',
|
||||
generation: 2,
|
||||
nativePartID: 'native_2',
|
||||
});
|
||||
|
||||
const wrongGeneration = completeCompaction(
|
||||
state,
|
||||
{ runID: 'run_2', generation: 1, nativePartID: 'native_2' },
|
||||
);
|
||||
expect(wrongGeneration).toBe(state);
|
||||
|
||||
state = completeCompaction(
|
||||
state,
|
||||
{ runID: 'run_2', generation: 2, nativePartID: 'native_2' },
|
||||
{ nativeEventID: 'evt_complete_2', completedAt: 1_700_000_000_100 },
|
||||
);
|
||||
expect(state.compactionsById.ui_2).toEqual(expect.objectContaining({
|
||||
status: 'completed',
|
||||
nativeEventID: 'evt_complete_2',
|
||||
completedAt: 1_700_000_000_100,
|
||||
}));
|
||||
expect(state.compactionsById.ui_1?.status).toBe('running');
|
||||
|
||||
const untouched = removeCompaction(state, { runID: 'run_1', generation: 2 });
|
||||
expect(untouched).toBe(state);
|
||||
state = removeCompaction(state, { runID: 'run_1', generation: 1 });
|
||||
expect(getOrderedSessionCompactions(state).map((event) => event.id)).toEqual(['ui_2']);
|
||||
});
|
||||
|
||||
it('completes compaction on session.compacted without marking the transcript idle', () => {
|
||||
const busy = createPendingCompaction(
|
||||
hydrateOpenCodeSession('ses_1', [], 'busy'),
|
||||
{ id: 'ui_compact_1', source: 'automatic', runID: 'run_1', generation: 4 },
|
||||
);
|
||||
const compacted = reduceOpenCodeEvent(busy, {
|
||||
type: 'session.compacted',
|
||||
payload: {
|
||||
sessionID: 'ses_1',
|
||||
eventID: 'evt_complete_1',
|
||||
runID: 'run_1',
|
||||
generation: 4,
|
||||
},
|
||||
});
|
||||
|
||||
expect(compacted?.status).toBe('busy');
|
||||
expect(compacted?.compactionsById.ui_compact_1).toEqual(expect.objectContaining({
|
||||
status: 'completed',
|
||||
nativeEventID: 'evt_complete_1',
|
||||
}));
|
||||
|
||||
const idle = reduceOpenCodeEvent(compacted, {
|
||||
type: 'session.idle',
|
||||
payload: { sessionID: 'ses_1' },
|
||||
});
|
||||
expect(idle?.status).toBe('idle');
|
||||
});
|
||||
|
||||
it('uses session.idle as a successful completion fallback', () => {
|
||||
const busy = createPendingCompaction(
|
||||
hydrateOpenCodeSession('ses_1', [], 'busy'),
|
||||
{ id: 'ui_compact_idle', source: 'manual', runID: 'run_idle', generation: 5 },
|
||||
);
|
||||
|
||||
const idle = reduceOpenCodeEvent(busy, {
|
||||
type: 'session.idle',
|
||||
payload: { sessionID: 'ses_1' },
|
||||
});
|
||||
|
||||
expect(idle?.status).toBe('idle');
|
||||
expect(idle?.compactionsById.ui_compact_idle?.status).toBe('completed');
|
||||
});
|
||||
|
||||
it('removes unfinished compactions on a fatal session error', () => {
|
||||
const busy = createPendingCompaction(
|
||||
hydrateOpenCodeSession('ses_1', [], 'busy'),
|
||||
{ id: 'ui_compact_error', source: 'automatic', runID: 'run_error', generation: 6 },
|
||||
);
|
||||
|
||||
const failed = reduceOpenCodeEvent(busy, {
|
||||
type: 'session.error',
|
||||
payload: { sessionID: 'ses_1', error: 'provider failed' },
|
||||
});
|
||||
|
||||
expect(failed?.status).toBe('idle');
|
||||
expect(failed?.compactionOrder).toEqual([]);
|
||||
expect(failed?.compactionsById.ui_compact_error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user