import { describe, expect, it } from 'vitest' import type { SessionEvent, SurfaceEvent, SurfaceEventType } from '@deepseek-ai/dsh-session' import { Session, SessionId, SessionLogOffset, SessionSeq, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, } from '@deepseek-ai/dsh-session' import { SurfaceManager } from '@deepseek-ai/dsh-session/surface' import { createMessage, createSystemMessage, createToolResultMessage, createUserMessage, freezeMessage, ToolCallId, MessageId, } from '@deepseek-ai/dsh-llm' type TestSurfaceOp = 'append' | { op: 'replace'; start: number; end: number } function surfaceOp(value: TestSurfaceOp): SurfaceEvent['surfaceOp'] { return value === 'append' ? value : { op: 'replace', start: SessionSeq(value.start), end: SessionSeq(value.end) } } function replacementMessage(text: string) { return createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'plugin', plugin: 'test' }, }) } function sourceSeqs(...values: number[]) { return values.map(SessionSeq) } /** Build a minimal session with turn boundaries and a single user message. */ function surfaceSession(): Session { const s = Session.create(SessionId('ss')) s.append('turn/start', { turn: 1 }) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) s.append('assistant/message', { stream: [], turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [{ type: 'text', text: 'hi' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, }, }), }, { surfaceOp: 'append' }) s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) return s } function provenanceEvent(seq: SessionSeq, sourceEventSeqs: unknown): SessionEvent { return { type: 'user/message', seq, time: seq, data: createUserMessage({ content: [], source: { kind: 'user' }, }), surfaceOp: 'append', ...sourceEventSeqs === undefined ? {} : { sourceEventSeqs }, } as unknown as SessionEvent } function toolResultEvent( seq: SessionSeq, callId: string, op: TestSurfaceOp = 'append', sourceEventSeqs?: number[], ): SessionEvent { return { type: 'tool/result', seq, time: seq, data: { turn: 1, step: 1, message: createToolResultMessage({ callId: ToolCallId(callId), content: [{ type: 'text', text: `result ${seq}` }], isError: false, }), }, surfaceOp: surfaceOp(op), ...sourceEventSeqs === undefined ? {} : { sourceEventSeqs: sourceSeqs(...sourceEventSeqs) }, } } describe('foldSurface source-event references', () => { it('accepts absent or valid source-event references and complete replacement coverage', () => { const events = [ provenanceEvent(SessionSeq(0), undefined), provenanceEvent(SessionSeq(1), undefined), { ...provenanceEvent(SessionSeq(2), [0, 1]), surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 1 }), }, ] as SessionEvent[] expect(() => foldSurface(events)).not.toThrow() }) it('rejects source-event references on a non-surface event', () => { const event = { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 }, sourceEventSeqs: sourceSeqs(0), } as unknown as SessionEvent expect(() => foldSurface([event])).toThrow(/cannot carry sourceEventSeqs/) }) it('rejects obsolete source-event provenance on an assistant message', () => { const event = { type: 'assistant/message', seq: SessionSeq(0), time: 0, data: { turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, }, }), stream: [], }, surfaceOp: 'append', sourceEventSeqs: [], } as SessionEvent expect(() => foldSurface([event])).toThrow(/embeds its source stream/) }) it.each([ ['a non-array', [{ ...provenanceEvent(SessionSeq(0), undefined), sourceEventSeqs: 'invalid' }], /must be an array/], ['an empty array', [provenanceEvent(SessionSeq(0), [])], /must not be empty/], ['duplicates', [provenanceEvent(SessionSeq(0), undefined), provenanceEvent(SessionSeq(1), [0, 0])], /must not contain duplicates/], ['a sparse array', [provenanceEvent(SessionSeq(0), Array(1))], /densely contain/], ['a non-number', [{ ...provenanceEvent(SessionSeq(0), undefined), sourceEventSeqs: ['0'] }], /non-negative safe integers/], ['a fractional number', [provenanceEvent(SessionSeq(0), [0.5])], /non-negative safe integers/], ['a negative number', [provenanceEvent(SessionSeq(0), [-1])], /non-negative safe integers/], ['a self reference', [provenanceEvent(SessionSeq(0), [0])], /must reference earlier events/], ['a non-contiguous event seq', [provenanceEvent(SessionSeq(0), undefined), provenanceEvent(SessionSeq(2), [1])], /seq 2 is not contiguous; expected 1/], ['incomplete replacement coverage', [ provenanceEvent(SessionSeq(0), undefined), provenanceEvent(SessionSeq(1), undefined), { ...provenanceEvent(SessionSeq(2), [0]), surfaceOp: { op: 'replace', start: 0, end: 1 } }, ], /missing 1/], ] as const)( 'rejects %s', (_name, events, expected) => { expect(() => foldSurface(events as unknown as SessionEvent[])).toThrow(expected) }, ) }) describe('foldSurface tool-result rewrites', () => { it('rejects a replacement spanning multiple current nodes', () => { const events = [ provenanceEvent(SessionSeq(0), undefined), provenanceEvent(SessionSeq(1), undefined), toolResultEvent(SessionSeq(2), 'rewrite', { op: 'replace', start: 0, end: 1 }, [0, 1]), ] expect(() => foldSurface(events)).toThrow(/must rewrite exactly one current node/) }) it('rejects a replacement targeting a non-result node', () => { const events = [ provenanceEvent(SessionSeq(0), undefined), toolResultEvent(SessionSeq(1), 'rewrite', { op: 'replace', start: 0, end: 0 }, [0]), ] expect(() => foldSurface(events)).toThrow(/must target a current tool\/result/) }) it('rejects changes outside tool-result content', () => { const events = [ toolResultEvent(SessionSeq(0), 'original'), toolResultEvent(SessionSeq(1), 'changed', { op: 'replace', start: 0, end: 0 }, [0]), ] expect(() => foldSurface(events)).toThrow(/may change only content/) }) it.each([ ['toolCallId', { toolCallId: ToolCallId('changed') }], ['isError', { isError: true }], ] as const)('rejects a replacement that changes the result block %s', (_field, patch) => { const original = toolResultEvent(SessionSeq(0), 'original') const data = original.data as Extract['data'] const result = data.message.content[0] const replacement = { ...original, seq: SessionSeq(1), time: 1, data: { ...data, message: freezeMessage({ ...data.message, content: [{ ...result, ...patch }] as [typeof result], }), }, surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 0 }), sourceEventSeqs: sourceSeqs(0), } as SessionEvent expect(() => foldSurface([original, replacement])).toThrow(/may change only content/) }) it('compares array-valued rest fields structurally (meta arrays: equal accepted, drifted rejected)', () => { const withMeta = (seq: SessionSeq, meta: unknown, op: TestSurfaceOp = 'append', sourceEventSeqs?: number[]): SessionEvent => { const event = toolResultEvent(SessionSeq(seq), 'c-meta', op, sourceEventSeqs) const data = event.data as Extract['data'] return { ...event, data: { ...data, message: freezeMessage({ ...data.message, id: MessageId('meta-message') }), meta, }, } as SessionEvent } // Structurally equal arrays (fresh references) pass the rest-field equality. expect(() => foldSurface([ withMeta(SessionSeq(0), { tags: ['a', { n: 1 }] }), withMeta(SessionSeq(1), { tags: ['a', { n: 1 }] }, { op: 'replace', start: 0, end: 0 }, [0]), ])).not.toThrow() // Same length, drifted element: the array branch must reject. expect(() => foldSurface([ withMeta(SessionSeq(0), { tags: ['a'] }), withMeta(SessionSeq(1), { tags: ['b'] }, { op: 'replace', start: 0, end: 0 }, [0]), ])).toThrow(/may change only content/) // Array vs non-array on one side: the mixed-shape guard rejects. expect(() => foldSurface([ withMeta(SessionSeq(0), { tags: ['a'] }), withMeta(SessionSeq(1), { tags: 'a' }, { op: 'replace', start: 0, end: 0 }, [0]), ])).toThrow(/may change only content/) // Same key count, different key names: the hasOwn branch rejects. expect(() => foldSurface([ withMeta(SessionSeq(0), { left: 1 }), withMeta(SessionSeq(1), { right: 1 }, { op: 'replace', start: 0, end: 0 }, [0]), ])).toThrow(/may change only content/) // Different key counts: the key-length branch rejects. expect(() => foldSurface([ withMeta(SessionSeq(0), { one: 1 }), withMeta(SessionSeq(1), { one: 1, two: 2 }, { op: 'replace', start: 0, end: 0 }, [0]), ])).toThrow(/may change only content/) }) }) describe('SurfaceManager', () => { it('folds a contiguous window without materializing earlier event sequences', () => { const baseSeq = 400_000 const events = [ provenanceEvent(SessionSeq(baseSeq), undefined), provenanceEvent(SessionSeq(baseSeq + 1), undefined), { ...provenanceEvent(SessionSeq(baseSeq + 2), [baseSeq]), surfaceOp: surfaceOp({ op: 'replace', start: baseSeq, end: baseSeq }), }, ] as SessionEvent[] const surface = new SurfaceManager(events, SessionLogOffset(baseSeq)) expect(surface.nodes).toEqual([baseSeq + 2, baseSeq + 1]) expect(surface.replaceGeneration).toBe(1) }) it('validates tool-result rewrites against a nonzero window offset', () => { const baseSeq = 400_000 const original = toolResultEvent(SessionSeq(baseSeq), 'call') const events: SessionEvent[] = [ original, { ...original, seq: SessionSeq(baseSeq + 1), time: baseSeq + 1, surfaceOp: surfaceOp({ op: 'replace', start: baseSeq, end: baseSeq }), sourceEventSeqs: sourceSeqs(baseSeq), } as SessionEvent, ] expect(new SurfaceManager(events, SessionLogOffset(baseSeq)).nodes).toEqual([baseSeq + 1]) }) it('rejects a replacement that crosses a loaded window head', () => { const baseSeq = 400_000 const events = [ provenanceEvent(SessionSeq(baseSeq), undefined), { ...provenanceEvent(SessionSeq(baseSeq + 1), [baseSeq - 1, baseSeq]), surfaceOp: surfaceOp({ op: 'replace', start: baseSeq - 1, end: baseSeq }), }, ] as SessionEvent[] expect(() => new SurfaceManager(events, SessionLogOffset(baseSeq)).nodes) .toThrow(`surface replace: start seq ${baseSeq - 1} not found in surface`) }) it('shares ordered entries and nested replacement ranges with foldSurface', () => { const s = Session.create(SessionId('shared-fold')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'b' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) s.append('user/message', replacementMessage('summary'), { surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 0 }), sourceEventSeqs: sourceSeqs(0), }) s.append('user/message', replacementMessage('summary 2'), { surfaceOp: surfaceOp({ op: 'replace', start: 2, end: 1 }), sourceEventSeqs: sourceSeqs(2, 1), }) const folded = foldSurface(s.snapshotEvents()) expect(folded.nodes).toEqual(s.surface.nodes) expect(folded.replacements).toEqual([ { seq: 2, start: 0, end: 0, shadowedSeqs: [0] }, { seq: 3, start: 2, end: 1, shadowedSeqs: [2, 1] }, ]) folded.nodes[0] = SessionSeq(99) folded.replacements[0]!.shadowedSeqs.push(SessionSeq(99)) expect(s.surface.nodes).toEqual([3]) expect(foldSurface(s.snapshotEvents()).nodes).toEqual([3]) expect(foldSurface(s.snapshotEvents()).replacements[0]!.shadowedSeqs).toEqual([0]) }) it('does not retain fold-only replacement history in incremental state', () => { const s = Session.create(SessionId('incremental-state')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) s.append('user/message', replacementMessage('b'), { surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 0 }), sourceEventSeqs: sourceSeqs(0), }) expect(s.surface.nodes).toEqual([1]) const manager = s.surface as unknown as { _state: object } expect(Object.hasOwn(manager._state, 'replacements')).toBe(false) expect(foldSurface(s.snapshotEvents()).replacements).toEqual([ { seq: 1, start: 0, end: 0, shadowedSeqs: [0] }, ]) }) it('foldSurface reports the same invalid replacement failures as the incremental manager', () => { const events = [ provenanceEvent(SessionSeq(0), undefined), { ...provenanceEvent(SessionSeq(1), [0]), surfaceOp: { op: 'replace', start: 42, end: 0 } }, ] as SessionEvent[] expect(() => foldSurface(events)).toThrow(/start seq 42 not found/) expect(() => Session.create(SessionId('shared-fold-invalid'), events)) .toThrow(/start seq 42 not found/) }) it('rejects negative-zero replacement event sequences', () => { const event = { ...provenanceEvent(SessionSeq(0), [0]), surfaceOp: { op: 'replace', start: -0, end: -0 }, } as SessionEvent expect(() => foldSurface([event])).toThrow(/invalid replace surfaceOp/) }) it('leaves incremental state unchanged when candidate validation fails', () => { const s = Session.create(SessionId('atomic-validation')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) const surface = s.surface const nodes = surface.nodes expect(nodes).toEqual(foldSurface(s.snapshotEvents()).nodes) expect(surface.replaceGeneration).toBe(0) expect(() => s.append( 'assistant/message', { stream: [], turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [{ type: 'text', text: 'invalid' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, }, }), }, { surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 0 }) }, )).toThrow(/missing 0/) expect(s.snapshotEvents()).toHaveLength(1) expect(s.surface).toBe(surface) expect(surface.nodes).toEqual([0]) expect(surface.replaceGeneration).toBe(0) expect(surface.nodes).toEqual(foldSurface(s.snapshotEvents()).nodes) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'b' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) expect(surface.nodes).toBe(nodes) expect(surface.nodes).toEqual([0, 1]) expect(surface.replaceGeneration).toBe(0) expect(surface.nodes).toEqual(foldSurface(s.snapshotEvents()).nodes) }) it('foldSurface rejects a surface-eligible event without its mandatory marker', () => { const malformed: SessionEvent = { type: 'user/message', seq: SessionSeq(0), time: 1, data: createUserMessage({ content: [{ type: 'text', text: 'hidden' }], source: { kind: 'user' }, }), } expect(() => foldSurface([malformed])) .toThrow(/surface-eligible and requires a surfaceOp marker/) }) it('foldSurface rejects surfaceOp on a non-surface event', () => { const malformed = { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 }, surfaceOp: 'append', } as unknown as SessionEvent expect(() => foldSurface([malformed])) .toThrow(/not surface-eligible and cannot carry surfaceOp/) }) it('folds an ordered sequence list from surfaceOp: append markers', () => { const s = surfaceSession() const nodes = s.surface.nodes // Only the user/message and assistant/message carry surfaceOp: 'append'. // The turn boundaries do not have surface markers. expect(nodes).toEqual([1, 2]) }) it('empty surface yields empty nodes', () => { const s = Session.create(SessionId('empty')) s.append('turn/start', { turn: 1 }) s.append('step/start', { turn: 1, step: 1 }) s.append('step/end', { turn: 1, step: 1 }) s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) expect(s.surface.nodes.length).toBe(0) expect(s.deriveMessages()).toEqual([]) }) it('picks up new events incrementally (delta processing)', () => { const s = surfaceSession() expect(s.surface.nodes.length).toBe(2) s.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: ToolCallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false, }), }, { surfaceOp: 'append' }) expect(s.surface.nodes.length).toBe(3) expect(s.surface.nodes[2]!).toBe(4) // seq 4: after turn/end at seq 3 }) it('replays identically from a seeded log with surface markers', () => { const original = surfaceSession() original.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ callId: ToolCallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false, }), }, { surfaceOp: 'append' }) const replayed = Session.create(SessionId('replay'), original.snapshotEvents()) expect(replayed.surface.nodes).toEqual([1, 2, 4]) expect(replayed.deriveMessages()).toEqual(original.deriveMessages()) }) it('rebuild with replace operation splices out shadowed nodes', () => { const s = surfaceSession() s.append('user/message', replacementMessage('summary'), { surfaceOp: surfaceOp({ op: 'replace', start: 1, end: 2 }), sourceEventSeqs: sourceSeqs(1, 2) }, ) expect(s.surface.nodes).toEqual([4]) }) it('replace with both ends at real nodes splices only the range', () => { const s = Session.create(SessionId('range')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 0 s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'b' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 1 s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'c' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 2 // Replace seq 0 through 1 inclusive: shadow a and b, keep c. s.append('user/message', replacementMessage('summary'), { surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 1 }), sourceEventSeqs: sourceSeqs(0, 1) }, ) // seq 3 expect(s.surface.nodes).toEqual([3, 2]) }) it('single-node replacement (start === end)', () => { const s = Session.create(SessionId('single')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 0 s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'b' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 1 // Replace only seq 1 (single node). s.append('user/message', replacementMessage('x'), { surfaceOp: surfaceOp({ op: 'replace', start: 1, end: 1 }), sourceEventSeqs: sourceSeqs(1) }, ) // seq 2 expect(s.surface.nodes).toEqual([0, 2]) }) it('throws when replace start is not found', () => { const s = Session.create(SessionId('bad-start')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 0 expect(() => s.append('user/message', replacementMessage('y'), { surfaceOp: surfaceOp({ op: 'replace', start: 5, end: 0 }), sourceEventSeqs: sourceSeqs(0) }, )).toThrow(/surface replace: start seq 5 not found/) }) it('throws when replace end is not found', () => { const s = Session.create(SessionId('bad-end')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 0 expect(() => s.append('user/message', replacementMessage('y'), { surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 99 }), sourceEventSeqs: sourceSeqs(0) }, )).toThrow(/surface replace: end seq 99 not found/) }) it('throws when start is after end', () => { const s = Session.create(SessionId('reversed')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 0 s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'b' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 1 // start=1, end=0 would be reversed order. expect(() => s.append('user/message', replacementMessage('y'), { surfaceOp: surfaceOp({ op: 'replace', start: 1, end: 0 }), sourceEventSeqs: sourceSeqs(1, 0) }, )).toThrow(/start seq 1.*after end seq 0/) }) it('sourceEventSeqs is snapshot so caller mutation does not affect logged event', () => { const s = Session.create(SessionId('immutable')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'source' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) const sources = sourceSeqs(0) s.append('user/message', replacementMessage('h'), { surfaceOp: 'append', sourceEventSeqs: sources, }) // Mutate caller's array after append. sources.push(SessionSeq(1)) sources[0] = SessionSeq(99) const logged = s.snapshotEvents()[1]! as SurfaceEvent expect(logged.sourceEventSeqs).toEqual([0]) }) it('replace starting at non-head position preserves surrounding order', () => { const s = Session.create(SessionId('mid-replace')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 0 s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'b' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 1 s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'c' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // seq 2 // Replace the middle node (seq 1) only, keeping seq 0 and seq 2. s.append('user/message', replacementMessage('x'), { surfaceOp: surfaceOp({ op: 'replace', start: 1, end: 1 }), sourceEventSeqs: sourceSeqs(1) }, ) // seq 3 expect(s.surface.nodes).toEqual([0, 3, 2]) }) it('surfaceOp replace object is snapshot so caller mutation is isolated', () => { const s = Session.create(SessionId('immutable-op')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'a' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) const op = { op: 'replace' as const, start: SessionSeq(0), end: SessionSeq(0) } s.append('user/message', replacementMessage('s'), { surfaceOp: op, sourceEventSeqs: sourceSeqs(0), }) // Mutate caller's object after append. op.start = SessionSeq(99) const logged = s.snapshotEvents()[1]! as SurfaceEvent expect(logged.surfaceOp).toEqual({ op: 'replace', start: 0, end: 0 }) }) }) describe('deriveMessages with surface', () => { it('uses the surface path when surface markers are present', () => { const s = surfaceSession() const messages = s.deriveMessages() expect(messages).toHaveLength(2) expect(messages[0]!.role).toBe('user') expect(messages[0]!.content[0]).toMatchObject({ type: 'text', text: 'hello' }) expect(messages[1]!.role).toBe('assistant') expect(messages[1]!.content[0]).toMatchObject({ type: 'text', text: 'hi' }) }) it('surface path skips non-surface events (attempts, boundaries)', () => { const s = Session.create(SessionId('filter')) s.append('turn/start', { turn: 1 }) s.append('assistant/attempt', { turn: 1, step: 1, stream: [{ type: 'text-chunks', time0: 1, index: 0, dt: [1], texts: ['h', 'i'] }], }) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) s.append('assistant/message', { stream: [], turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [{ type: 'text', text: 'hi' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, }, }), }, { surfaceOp: 'append' }) s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) // Attempts and boundaries are NOT in the surface, so only 2 messages. expect(s.deriveMessages()).toHaveLength(2) }) it('deriveMessages via surface respects replace (shadowed nodes are excluded)', () => { const s = Session.create(SessionId('compacted')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'original' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) s.append('user/message', replacementMessage('compacted'), { surfaceOp: surfaceOp({ op: 'replace', start: 0, end: 0 }), sourceEventSeqs: sourceSeqs(0), }) // Only the compaction node is visible. const messages = s.deriveMessages() expect(messages).toHaveLength(1) expect(messages[0]!.content[0]).toMatchObject({ type: 'text', text: 'compacted' }) }) it('injected-context and user messages appear on surface', () => { const s = Session.create(SessionId('ctx')) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'file changed' }], source: { kind: 'plugin', plugin: 'watcher' }, }), { surfaceOp: 'append' }) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'focus' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) const messages = s.deriveMessages() expect(messages).toHaveLength(2) expect(messages[0]!.content).toEqual([{ type: 'text', text: 'file changed' }]) expect(messages[1]!.content).toEqual([{ type: 'text', text: 'focus' }]) }) }) describe('Session.append surface opts', () => { it('records sourceEventSeqs and surfaceOp on a source-derived event', () => { const s = Session.create(SessionId('opts')) s.append('turn/start', { turn: 1 }) s.append('step/start', { turn: 1, step: 1 }) const event = s.append( 'user/message', createUserMessage({ content: [{ type: 'text', text: 'h' }], source: { kind: 'plugin', plugin: 'test' }, }), { surfaceOp: 'append', sourceEventSeqs: sourceSeqs(0, 1) }, ) expect(event.sourceEventSeqs).toEqual([0, 1]) expect(event.surfaceOp).toBe('append') // The logged event matches the returned event. expect((s.snapshotEvents()[2]! as SurfaceEvent).sourceEventSeqs).toEqual([0, 1]) expect((s.snapshotEvents()[2]! as SurfaceEvent).surfaceOp).toBe('append') }) it('deriveMessages skips a surface node that derives to null (empty assistant/message)', () => { // An empty-content assistant/message is surface-eligible (it can host usage) // but _deriveOneMessage returns null for it, so the surface derivation path's // null-check is exercised — the node is on the surface yet produces no message. const seed: SessionEvent[] = [ { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } }, { type: 'step/start', seq: SessionSeq(1), time: 2, data: { turn: 1, step: 1 } }, { type: 'assistant/message', seq: SessionSeq(2), time: 3, data: { stream: [], turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, }, }), }, surfaceOp: 'append' }, { type: 'step/end', seq: SessionSeq(3), time: 4, data: { turn: 1, step: 1 } }, { type: 'turn/end', seq: SessionSeq(4), time: 5, data: { turn: 1, reason: { kind: 'completed' } } }, ] const s = Session.create(SessionId('nomessage'), seed) // The empty assistant/message is on the surface but _deriveOneMessage returns null for it. expect(s.deriveMessages()).toHaveLength(0) }) it('a non-surface event carries no surface fields', () => { const s = Session.create(SessionId('noopts')) s.append('turn/start', { turn: 1 }) expect((s.snapshotEvents()[0] as SessionEvent).sourceEventSeqs).toBeUndefined() expect((s.snapshotEvents()[0] as SessionEvent).surfaceOp).toBeUndefined() }) it('surfaceOp primitives are not cloned (they are immutable)', () => { const s = Session.create(SessionId('prim')) const event = s.append('assistant/message', { stream: [], turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, }, }), }, { surfaceOp: 'append' }) // The string 'append' is a primitive — identity-preserving is fine. expect(event.surfaceOp).toBe('append') }) it('isSurfaceEvent rejects a surface-eligible type missing its surfaceOp marker', () => { // A raw event (not built via append, which mandates the marker) of a // surface-eligible type but with no surfaceOp must NOT narrow to a // SurfaceEvent — it would otherwise be silently dropped from the surface. const noMarker: SessionEvent = { type: 'user/message', seq: SessionSeq(0), time: 1, data: createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' }, }), } expect(isSurfaceEvent(noMarker)).toBe(false) // A non-surface type is rejected too (the type gate). const boundary: SessionEvent = { type: 'turn/start', seq: SessionSeq(1), time: 1, data: { turn: 1 } } expect(isSurfaceEvent(boundary)).toBe(false) // A properly-marked surface event narrows. const marked = { ...noMarker, surfaceOp: 'append' } as SurfaceEvent expect(isSurfaceEvent(marked)).toBe(true) }) }) describe('surface type guards', () => { it('isSurfaceEligibleType is true only for message-producing types', () => { expect(isSurfaceEligibleType('user/message')).toBe(true) expect(isSurfaceEligibleType('assistant/message')).toBe(true) expect(isSurfaceEligibleType('tool/result')).toBe(true) expect(isSurfaceEligibleType('turn/start')).toBe(false) expect(isSurfaceEligibleType('assistant/attempt')).toBe(false) }) it('isSurfaceEvent narrows a fully-formed surface event', () => { const s = surfaceSession() const userMessage = s.snapshotEvents().find(e => e.type === 'user/message')! expect(isSurfaceEvent(userMessage)).toBe(true) }) it('isSurfaceEvent rejects a non-surface-eligible type', () => { const s = surfaceSession() const turnStart = s.snapshotEvents().find(e => e.type === 'turn/start')! expect(isSurfaceEvent(turnStart)).toBe(false) }) it('isSurfaceEvent rejects a surface-eligible type missing its surfaceOp marker', () => { // A surface-eligible type whose mandatory surfaceOp is absent — the state a // seed/load log can carry before the marker is validated. surfaceOp is // optional on SessionEvent, so this is a representable runtime value. const markerless: SessionEvent = { type: 'user/message', seq: SessionSeq(0), time: 0, data: createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' }, }), } expect(isSurfaceEligibleType(markerless.type)).toBe(true) expect(isSurfaceEvent(markerless)).toBe(false) }) it('splits surface events into append-origin and replacement by their marker', () => { const s = surfaceSession() s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'checkpoint' }], source: { kind: 'plugin', plugin: 'compact' }, }), { surfaceOp: surfaceOp({ op: 'replace', start: 1, end: 2 }), sourceEventSeqs: sourceSeqs(1, 2) }) const appended = s.snapshotEvents().find(e => e.type === 'user/message')! const replacement = s.snapshotEvents().at(-1)! expect(isAppendSurfaceEvent(appended)).toBe(true) expect(isReplacementSurfaceEvent(appended)).toBe(false) expect(isAppendSurfaceEvent(replacement)).toBe(false) expect(isReplacementSurfaceEvent(replacement)).toBe(true) }) it('rejects log-only and markerless events from both marker guards', () => { const s = surfaceSession() const turnStart = s.snapshotEvents().find(e => e.type === 'turn/start')! // A surface-eligible type whose mandatory marker is absent has no origin at // all: it never entered the surface. const markerless: SessionEvent = { type: 'user/message', seq: SessionSeq(0), time: 0, data: createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' }, }), } expect(isAppendSurfaceEvent(turnStart)).toBe(false) expect(isReplacementSurfaceEvent(turnStart)).toBe(false) expect(isAppendSurfaceEvent(markerless)).toBe(false) expect(isReplacementSurfaceEvent(markerless)).toBe(false) }) }) describe('SurfaceManager.replaceGeneration', () => { it('folds the pending log delta on access and counts replaces', () => { const s = Session.create(SessionId('gen')) s.append('turn/start', { turn: 1 }) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'one' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'two' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) // Read the generation FIRST — before nodes — so the getter itself folds // the pending delta rather than piggybacking on a nodes read. expect(s.surface.replaceGeneration).toBe(0) const nodes = s.surface.nodes s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'summary' }], source: { kind: 'plugin', plugin: 'compact' }, }), { surfaceOp: { op: 'replace', start: nodes[0]!, end: nodes[1]! }, sourceEventSeqs: [nodes[0]!, nodes[1]!] }) expect(s.surface.replaceGeneration).toBe(1) }) }) describe('system/message surface node', () => { function systemEvent(seq: number, text: string, op: TestSurfaceOp = 'append', sources?: number[]): SessionEvent { return { type: 'system/message', seq: SessionSeq(seq), time: seq, data: { turn: 1, step: 1, message: createSystemMessage(text, 'test-plugin') }, surfaceOp: surfaceOp(op), ...sources === undefined ? {} : { sourceEventSeqs: sourceSeqs(...sources) }, } } function userEvent(seq: number, op: TestSurfaceOp = 'append', sources?: number[]): SessionEvent { return { type: 'user/message', seq: SessionSeq(seq), time: seq, data: createUserMessage({ content: [{ type: 'text', text: `u${seq}` }], source: { kind: 'user' } }), surfaceOp: surfaceOp(op), ...sources === undefined ? {} : { sourceEventSeqs: sourceSeqs(...sources) }, } } it('projects a system node as the leading system-role message and an empty one as no message', () => { const s = Session.create(SessionId('sys')) s.append('turn/start', { turn: 1 }) s.append('step/start', { turn: 1, step: 1 }) s.append('system/message', { turn: 1, step: 1, message: createSystemMessage('be brief', 'p') }, { surfaceOp: 'append' }) s.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }), { surfaceOp: 'append' }) expect(s.deriveMessages().map(message => message.role)).toEqual(['system', 'user']) expect(s.deriveMessages()[0]?.content).toEqual([{ type: 'text', text: 'be brief' }]) const head = s.surface.nodes[0] as SessionSeq s.append('system/message', { turn: 1, step: 1, message: createSystemMessage('', 'p') }, { surfaceOp: { op: 'replace', start: head, end: head }, sourceEventSeqs: [head], }) expect(s.deriveMessages().map(message => message.role)).toEqual(['user']) expect(s.surface.nodes).toHaveLength(2) }) it('replaces node 0 with a new system node and rejects other rewrites of the head', () => { const replaced = foldSurface([ systemEvent(0, 'v1'), userEvent(1), systemEvent(2, 'v2', { op: 'replace', start: 0, end: 0 }, [0]), ]) expect(replaced.nodes).toEqual(sourceSeqs(2, 1)) expect(() => foldSurface([ systemEvent(0, 'v1'), userEvent(1), userEvent(2, { op: 'replace', start: 0, end: 1 }, [0, 1]), ])).toThrow(/node 0 holds the system prompt/) expect(() => foldSurface([ systemEvent(0, 'v1'), userEvent(1), systemEvent(2, 'v2', { op: 'replace', start: 0, end: 1 }, [0, 1]), ])).toThrow(/node 0 holds the system prompt/) }) it('leaves later system nodes and a non-system head unprotected', () => { const later = foldSurface([ systemEvent(0, 'v1'), userEvent(1), systemEvent(2, 'v2'), userEvent(3), userEvent(4, { op: 'replace', start: 1, end: 3 }, [1, 2, 3]), ]) expect(later.nodes).toEqual(sourceSeqs(0, 4)) const plainHead = foldSurface([ userEvent(0), userEvent(1), userEvent(2, { op: 'replace', start: 0, end: 1 }, [0, 1]), ]) expect(plainHead.nodes).toEqual(sourceSeqs(2)) }) it('rejects a seeded system/message with a non-system role or non-plugin source', () => { const good = systemEvent(0, 'v1') const badRole = { ...good, data: { ...good.data, message: { ...(good.data as { message: object }).message, role: 'user' } } } expect(() => Session.create(SessionId('bad-role'), [badRole as SessionEvent])).toThrow(/role "system"/) const badSource = { ...good, data: { ...good.data, message: { ...(good.data as { message: object }).message, source: { kind: 'user' } } } } expect(() => Session.create(SessionId('bad-source'), [badSource as SessionEvent])).toThrow(/plugin source/) }) })