Files
deepseek-harness/packages/support/loader-smoke/tests/agent-turn.spec.ts
T
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

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()
})
})