Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
161 lines
5.7 KiB
TypeScript
161 lines
5.7 KiB
TypeScript
import type { Context } from '@deepseek-ai/cordis'
|
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { runFixtureTurn } from '../src/agent-turn.ts'
|
|
|
|
type Listener = (session: unknown, event: SessionEvent) => void
|
|
|
|
const event = (value: object): SessionEvent => value as unknown as SessionEvent
|
|
|
|
function turnHarness(): {
|
|
readonly ctx: Context
|
|
readonly session: { readonly id: string }
|
|
readonly foreignSession: object
|
|
readonly emit: (session: unknown, value: object) => void
|
|
readonly setFollowup: (callback: (message: { readonly id: unknown }) => void) => void
|
|
readonly whenIdle: ReturnType<typeof vi.fn>
|
|
readonly disposeListener: ReturnType<typeof vi.fn>
|
|
readonly flush: ReturnType<typeof vi.fn>
|
|
} {
|
|
const session = { id: 'fixture-session' }
|
|
const foreignSession = {}
|
|
let listener: Listener | undefined
|
|
let followup = (_message: { readonly id: unknown }): void => {}
|
|
const whenIdle = vi.fn(async () => {})
|
|
const disposeListener = vi.fn()
|
|
const flush = vi.fn(async () => {})
|
|
const agent = {
|
|
session,
|
|
whenIdle,
|
|
followup: vi.fn((message: { readonly id: unknown }) => { followup(message) }),
|
|
}
|
|
const ctx = {
|
|
get: (name: string) => name === 'agents' ? { roots: () => [agent] } : undefined,
|
|
on: (_name: string, callback: Listener) => {
|
|
listener = callback
|
|
return disposeListener
|
|
},
|
|
sessions: { flush },
|
|
} as unknown as Context
|
|
return {
|
|
ctx,
|
|
session,
|
|
foreignSession,
|
|
emit: (target, value) => { listener?.(target, event(value)) },
|
|
setFollowup: (callback) => { followup = callback },
|
|
whenIdle,
|
|
disposeListener,
|
|
flush,
|
|
}
|
|
}
|
|
|
|
describe('runFixtureTurn', () => {
|
|
it.each([
|
|
['no agent registry', undefined, 0],
|
|
['multiple roots', { roots: () => [{}, {}] }, 2],
|
|
])('rejects %s', async (_label, registry, count) => {
|
|
const ctx = { get: () => registry } as unknown as Context
|
|
await expect(runFixtureTurn(ctx, { task: 'ignored' }))
|
|
.rejects.toThrow(`fixture turn requires exactly one top-level agent, found ${count}`)
|
|
})
|
|
|
|
it('observes only the owned interval and returns its final text and deduplicated usage', async () => {
|
|
const harness = turnHarness()
|
|
const observed: SessionEvent[] = []
|
|
harness.setFollowup((message) => {
|
|
harness.emit(harness.foreignSession, {
|
|
type: 'assistant/message', seq: 0, time: 0, data: { message: { content: [] } },
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'step/start', seq: 0, time: 0, data: { turn: 1, step: 1 },
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'agent/inbox/spliced', seq: 1, time: 1, data: { inserted: [{ id: 'other' }] },
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'agent/inbox/spliced', seq: 2, time: 2, data: { inserted: [message] },
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'assistant/chunk', seq: 3, time: 3,
|
|
data: { turn: 1, step: 1, chunk: { type: 'text-delta', text: 'partial' } },
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'assistant/chunk', seq: 4, time: 4,
|
|
data: {
|
|
turn: 1,
|
|
step: 1,
|
|
chunk: { type: 'usage', usage: { inputTokens: 2, outputTokens: 3, reasoningTokens: 1 } },
|
|
},
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'assistant/message', seq: 5, time: 5,
|
|
data: {
|
|
turn: 1,
|
|
step: 1,
|
|
message: { content: [{ type: 'text', text: 'final answer' }] },
|
|
usage: { inputTokens: 4, outputTokens: 5, cacheReadTokens: 6 },
|
|
},
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'assistant/chunk', seq: 6, time: 6,
|
|
data: {
|
|
turn: 1,
|
|
step: 2,
|
|
chunk: { type: 'usage', usage: { inputTokens: 1, outputTokens: 2, cacheWriteTokens: 7, reasoningTokens: 2 } },
|
|
},
|
|
})
|
|
harness.emit(harness.session, {
|
|
type: 'assistant/message', seq: 7, time: 7,
|
|
data: { turn: 1, step: 2, message: { content: [{ type: 'tool-call' }] } },
|
|
})
|
|
harness.emit(harness.foreignSession, {
|
|
type: 'assistant/message', seq: 8, time: 8, data: { message: { content: [] } },
|
|
})
|
|
})
|
|
|
|
await expect(runFixtureTurn(harness.ctx, {
|
|
task: 'prove the fixture',
|
|
onEvent: (_sessionId, current) => { observed.push(current) },
|
|
})).resolves.toEqual({
|
|
type: 'result',
|
|
sessionId: 'fixture-session',
|
|
output: 'final answer',
|
|
usage: {
|
|
inputTokens: 5,
|
|
outputTokens: 7,
|
|
cacheReadTokens: 6,
|
|
cacheWriteTokens: 7,
|
|
reasoningTokens: 2,
|
|
},
|
|
})
|
|
expect(observed.map(current => current.seq)).toEqual([2, 3, 4, 5, 6, 7])
|
|
expect(harness.whenIdle).toHaveBeenCalledTimes(2)
|
|
expect(harness.flush).toHaveBeenCalledWith(harness.session)
|
|
expect(harness.disposeListener).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('omits usage when the interval records none', async () => {
|
|
const harness = turnHarness()
|
|
harness.setFollowup((message) => {
|
|
harness.emit(harness.session, {
|
|
type: 'agent/inbox/spliced', seq: 0, time: 0, data: { inserted: [message] },
|
|
})
|
|
})
|
|
|
|
await expect(runFixtureTurn(harness.ctx, { task: 'no model step' })).resolves.toEqual({
|
|
type: 'result',
|
|
sessionId: 'fixture-session',
|
|
output: '',
|
|
})
|
|
})
|
|
|
|
it('always removes its listener when the turn fails', async () => {
|
|
const harness = turnHarness()
|
|
harness.whenIdle.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('turn failed'))
|
|
|
|
await expect(runFixtureTurn(harness.ctx, { task: 'fail' })).rejects.toThrow('turn failed')
|
|
expect(harness.disposeListener).toHaveBeenCalledOnce()
|
|
expect(harness.flush).not.toHaveBeenCalled()
|
|
})
|
|
})
|