Files
deepseek-harness/packages/api/session-controller/tests/control-queue.host.spec.ts
T
imccyu 945006c984 perf(session): send one projection frame per committed event (#7587)
* perf(session): send one projection frame per committed event

* fix(session): deliver keys changed before a drive failure; address review

* perf(session): carry projection frame changes as key and val entries
2026-10-30 14:14:03 +08:00

215 lines
8.2 KiB
TypeScript

import { Context } from '@deepseek-ai/cordis'
import type { Agent, Inbox, InboxState } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import { afterEach, describe, expect, it } from 'vitest'
import { SessionControlController } from '../src/control.ts'
import type { SessionControlFrame } from '../src/types.ts'
import {
mountAgentLoopTestDependencies,
mountAgentLoopTestHarness,
} from '@deepseek-ai/dsh-agent-loop-testkit'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'fixture': { kind: 'fixture' } & ContextFormed
}
}
const ownedContexts = new Set<Context>()
afterEach(async () => {
await Promise.all([...ownedContexts].map(ctx => ctx.fiber.dispose()))
ownedContexts.clear()
})
async function harness(): Promise<{
ctx: Context
control: SessionControlController
agent: Agent
inbox: Inbox
}> {
const ctx = new Context()
ownedContexts.add(ctx)
await mountAgentLoopTestDependencies(ctx)
const loop = await mountAgentLoopTestHarness(ctx)
const agent = await loop.create(SessionId('queue-session'))
return { ctx, control: new SessionControlController(ctx), agent, inbox: agent.inbox }
}
function message(text: string, source: 'user' | 'plugin' = 'user') {
return createUserMessage({
content: [{ type: 'text', text }],
source: source === 'user' ? { kind: 'user' } : { kind: 'fixture' },
})
}
describe('Session control Inbox projection', () => {
/** Consume frames until the next durable Inbox value. */
async function nextInboxFrame(
iterator: AsyncIterator<SessionControlFrame>,
): Promise<Extract<SessionControlFrame, { type: 'projection' }>> {
for (;;) {
const next = await iterator.next()
if (next.done) throw new Error('stream ended before an Inbox value')
if (next.value.type === 'projection' && next.value.changes.some(change => change.key === 'inbox')) return next.value
}
}
/** The Inbox value one projection frame carries. */
function inboxValue(frame: Extract<SessionControlFrame, { type: 'projection' }>): unknown {
return frame.changes.find(change => change.key === 'inbox')?.val
}
it('projects both pending lists in baselines and live replacement frames', async () => {
const { control, inbox } = await harness()
const queued = message('queued')
const steering = message('steering')
const context = message('context', 'plugin')
inbox.append('next-turn', queued)
inbox.append('next-step', steering)
inbox.append('next-step', context)
const abort = new AbortController()
const iterator = control.control(abort.signal)[Symbol.asyncIterator]()
const opened = await iterator.next()
expect(opened.value).toMatchObject({
type: 'baseline',
value: {
projections: {
'queue-session': { values: { inbox: {
'next-turn': [queued], 'next-step': [steering, context],
} } },
},
},
})
const replacement = message('replacement')
inbox.append('next-turn', replacement)
const replaced = await nextInboxFrame(iterator)
expect(inboxValue(replaced)).toMatchObject({ 'next-turn': [queued, replacement] })
inbox.remove(steering.id)
const removed = await nextInboxFrame(iterator)
expect(inboxValue(removed)).toMatchObject({ 'next-step': [context] })
abort.abort()
await iterator.next()
})
it('derives queue replacements from the completed projection regardless of registration order', async () => {
const ctx = new Context()
ownedContexts.add(ctx)
await mountAgentLoopTestDependencies(ctx)
const loop = await mountAgentLoopTestHarness(ctx)
const control = new SessionControlController(ctx)
const agent = await loop.create(SessionId('late-projection-queue'))
const { inbox } = agent
const abort = new AbortController()
const iterator = control.control(abort.signal)[Symbol.asyncIterator]()
await iterator.next()
const pending = message('late projection')
inbox.append('next-turn', pending)
await expect(iterator.next()).resolves.toMatchObject({
value: {
type: 'projection',
changes: [{ key: 'inbox', val: { 'next-turn': [{ id: pending.id }], 'next-step': [] } }],
},
})
abort.abort()
await iterator.next()
})
it('projects inbox state for live and cold sessions', async () => {
const { ctx, control, inbox } = await harness()
inbox.append('next-turn', message('queued'))
const cold = ctx.sessions.create(SessionId('cold-session'))
const abort = new AbortController()
const iterator = control.control(abort.signal)[Symbol.asyncIterator]()
const opened = await iterator.next()
if (opened.done || opened.value.type !== 'baseline') throw new Error('missing baseline')
expect(opened.value.value.projections[cold.id]?.values.inbox).toEqual({ 'next-turn': [], 'next-step': [] })
expect(opened.value.value.projections['queue-session' as SessionId]?.values.inbox).toMatchObject({
'next-turn': [expect.objectContaining({ content: [{ type: 'text', text: 'queued' }] })],
})
abort.abort()
await iterator.next()
})
it('projects the prompt rpcId from a user-rpc source and omits it elsewhere', async () => {
const { control, inbox } = await harness()
const identified = createUserMessage({
content: [{ type: 'text', text: 'browser prompt' }],
source: { kind: 'user', rpcId: 'req-42' as never },
})
inbox.append('next-turn', identified)
inbox.append('next-step', message('plain steering'))
const abort = new AbortController()
const iterator = control.control(abort.signal)[Symbol.asyncIterator]()
const opened = await iterator.next()
if (opened.done || opened.value.type !== 'baseline') throw new Error('missing baseline')
const inboxValue = opened.value.value.projections['queue-session' as SessionId]?.values.inbox as unknown as InboxState
expect(inboxValue['next-turn'][0]?.source).toMatchObject({ kind: 'user', rpcId: 'req-42' })
expect(inboxValue['next-step'][0]?.source).toEqual({ kind: 'user' })
abort.abort()
await iterator.next()
})
it('publishes Inbox values for sessions without a live Agent', async () => {
const { ctx, control } = await harness()
const abort = new AbortController()
const iterator = control.control(abort.signal)[Symbol.asyncIterator]()
await iterator.next()
const session = ctx.sessions.create(SessionId('unattached-inbox'))
const pending = message('unattached')
session.append('agent/inbox/spliced', {
target: 'next-turn', start: 0, inserted: [pending],
})
expect(ctx.agents.get(session.id)).toBeUndefined()
const frame = await nextInboxFrame(iterator)
expect(frame.sessionId).toBe(session.id)
expect(inboxValue(frame)).toMatchObject({ 'next-turn': [pending], 'next-step': [] })
abort.abort()
await iterator.next()
})
it('drops broadcasts after cancellation has ended its queue', async () => {
const { control, inbox } = await harness()
const abort = new AbortController()
const iterator = control.control(abort.signal)[Symbol.asyncIterator]()
await iterator.next()
const waiting = iterator.next()
await Promise.resolve()
abort.abort()
inbox.append('next-turn', message('late'))
await expect(waiting).resolves.toMatchObject({ done: true })
})
it('ends active streams on context disposal after flushing buffered frames', async () => {
const { ctx, control, inbox } = await harness()
const iterator = control.control(new AbortController().signal)[Symbol.asyncIterator]()
await iterator.next()
const first = message('first')
const second = message('second')
inbox.append('next-turn', first)
inbox.append('next-turn', second)
const values: unknown[] = []
ownedContexts.delete(ctx)
await ctx.fiber.dispose()
for (;;) {
const next = await iterator.next()
if (next.done) break
if (next.value.type === 'projection' && next.value.changes.some(change => change.key === 'inbox')) values.push(inboxValue(next.value))
}
expect(values).toMatchObject([{ 'next-turn': [{ id: first.id }] }, { 'next-turn': [{ id: first.id }, { id: second.id }] }])
})
})