101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
/** Inbox projection delivery and queue-operation transport. */
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
import type { InboxState } from '@deepseek-ai/dsh-agent/types'
|
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
|
import type { SessionControlFrame } from '@deepseek-ai/dsh-api-session-controller/types'
|
|
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
|
import { SessionManager } from '../src/client/sessions/manager.ts'
|
|
import { FakeApiClient, fakeRemote } from './fake-api.client.ts'
|
|
|
|
const SID = 'fk-q1' as SessionId
|
|
const text = (value: string): ContentBlock[] => [{ type: 'text', text: value }]
|
|
|
|
let nextSeq = 1
|
|
|
|
function message(label: string, body: string) {
|
|
return createUserMessage({
|
|
content: text(body),
|
|
source: { kind: 'user', rpcId: `rpc-${label}` } as never,
|
|
})
|
|
}
|
|
|
|
function inboxFrame(value: InboxState): Extract<SessionControlFrame, { type: 'projection' }> {
|
|
return {
|
|
type: 'projection',
|
|
sessionId: SID,
|
|
key: 'inbox',
|
|
seq: nextSeq++,
|
|
value: value as never,
|
|
}
|
|
}
|
|
|
|
describe('Inbox projection intake', () => {
|
|
it('stores the complete Agent-owned value without adding queue state to the Session snapshot', () => {
|
|
const manager = new SessionManager(fakeRemote(new FakeApiClient()))
|
|
const queued = message('queued', 'later')
|
|
const steering = message('steering', 'now')
|
|
const value = { 'next-turn': [queued], 'next-step': [steering] }
|
|
|
|
manager.handleControlFrame(inboxFrame(value))
|
|
const session = manager.get(SID)
|
|
|
|
expect(session.projections.faceOf('inbox').getSnapshot()).toEqual(value)
|
|
expect(session.getSnapshot()).not.toHaveProperty('queue')
|
|
})
|
|
|
|
it('drops a projection that claims events beyond a reconnect baseline', () => {
|
|
const manager = new SessionManager(fakeRemote(new FakeApiClient()))
|
|
manager.handleControlFrame(inboxFrame({
|
|
'next-turn': [message('stale', 'stale')],
|
|
'next-step': [],
|
|
}))
|
|
|
|
manager.handleControlFrame({
|
|
type: 'baseline',
|
|
value: {
|
|
jobs: {},
|
|
projections: { [SID]: { asOfSeq: 0, values: {} } },
|
|
},
|
|
})
|
|
|
|
expect(manager.get(SID).projections.faceOf('inbox').getSnapshot()).toBeUndefined()
|
|
})
|
|
|
|
it('retains only the highest-seq value received before Session materialization', () => {
|
|
const manager = new SessionManager(fakeRemote(new FakeApiClient()))
|
|
manager.handleControlFrame(inboxFrame({
|
|
'next-turn': [message('old', 'old')],
|
|
'next-step': [],
|
|
}))
|
|
const latest = {
|
|
'next-turn': [message('latest', 'latest')],
|
|
'next-step': [],
|
|
}
|
|
manager.handleControlFrame(inboxFrame(latest))
|
|
|
|
expect(manager.get(SID).projections.faceOf('inbox').getSnapshot()).toEqual(latest)
|
|
})
|
|
})
|
|
|
|
describe('queue operation transport', () => {
|
|
it('does not mutate the Inbox projection before the Host publishes its committed value', async () => {
|
|
const api = new FakeApiClient()
|
|
const manager = new SessionManager(fakeRemote(api))
|
|
const pending = message('pending', 'before')
|
|
const initial = { 'next-turn': [pending], 'next-step': [] }
|
|
manager.handleControlFrame(inboxFrame(initial))
|
|
const session = manager.get(SID)
|
|
|
|
await expect(session.updateQueue(pending.id, { kind: 'edit', content: text('after') }))
|
|
.resolves.toEqual({ ok: true, value: { accepted: true } })
|
|
expect(api.callsOf('session.updateQueue')).toEqual([{
|
|
sessionId: SID,
|
|
itemId: pending.id,
|
|
action: { kind: 'edit', content: text('after') },
|
|
}])
|
|
expect(session.projections.faceOf('inbox').getSnapshot()).toBe(initial)
|
|
})
|
|
})
|