Files
deepseek-harness/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts
T
_KermanandTianyi Cui fb79a944f5 refactor(llm): separate durable producer sources from request inputs
Give durable user context its producer-owned source kind instead of a generic plugin wrapper. The V3-to-V4 conversion preserves the frozen rename and collision rules, all own JSON metadata, event coordinates, and historical request-only kinds. Native catalog and JSONL admission retain source validation so unknown attribution can survive without its producer while malformed owned sources are refused.

Use an explicit user-only RequestUserInput for auxiliary LLM prompts that do not own durable message identity. Auto Review and compaction remove their request-only source registrations while retaining prompt content and freezing; provider serializers and projections keep the same wire data, assistant replay isolation, image ordering, and tool correlation. Session writes, Agent delivery, and recorded title requests continue to require identified durable messages.

Bind the saved compatibility policy only to user sources and qualify tmux location attribution without weakening fixed system, model, or tool semantics. Rebuild the current inventory and unpublished V4 acknowledgement on the actual tool-role parent, preserving its rationale and all accepted historical records. The catalog format version records tooling metadata; Session remains V4. Developer-role policy and activation belong to their own feature.

The latest-master Messages adapter, serialization, request-input typing, EpochHeader reservation, and attribution checks pass 103 tests across five files. The retained V3 Messages input-history snapshot passes through the source profile; Host/Client typechecks and all 41 documentation gates pass. The generated comparison records four changed roots and 447 unchanged type fingerprints. Latest-master ARIA changes are retained, with producer source labels applied independently; new Messages fixtures retain their producer names as declared source kinds and use the fixed system-message helper; recorded Session generations are unchanged by this regrouping.

Co-authored-by: Tianyi Cui <[email protected]>
2026-09-18 19:07:12 +08:00

481 lines
19 KiB
TypeScript

import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, Inbox, ModelSelectionRef } from '@deepseek-ai/dsh-agent'
import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment'
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import { createAssistantMessage, createUserMessage, MessageId } from '@deepseek-ai/dsh-llm'
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
import SessionStore, {
SESSION_FORMAT_VERSION, Session, SessionId, SessionLogOffset, SessionSeq,
} from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader, UserMessage } from '@deepseek-ai/dsh-session'
import { snapshotSubagentDescriptor, SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent'
import { subagentIdentityProjectionDefinition } from '@deepseek-ai/dsh-subagent/src/projection.ts'
import { describe, expect, it, vi } from 'vitest'
import { ApiSessionAgentController } from '../src/agent.ts'
import { SessionCommandController } from '../src/commands.ts'
import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
'test': { kind: 'test' } & ContextFormed
}
}
async function commandHarness(
childMode?: 'continuable' | 'seeded-continuable' | 'seed-only' | 'one-shot' | 'unknown' | 'corrupt',
): Promise<{
ctx: Context
controller: SessionCommandController
agent: Agent
inbox: Inbox
steer: ReturnType<typeof vi.fn>
cancel: ReturnType<typeof vi.fn>
}> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
installSessionReadTestServices(ctx)
ctx.sessionProjections.register(subagentIdentityProjectionDefinition)
const sessionId = SessionId('commands-session')
const ancestor = Session.create(SessionId('ancestor'))
ancestor.append('subagent/descriptor', snapshotSubagentDescriptor({
mode: 'continuable', provider: 'test', label: 'ancestor',
}))
// A seeded child inherits exactly the ancestor prefix; its own descriptor
// is appended after creation, as the continuation manager does. `seed-only`
// never appends one: the identity folds as continuable, but from the
// inherited prefix rather than this Session's own suffix.
const lineage = childMode === 'seeded-continuable' || childMode === 'seed-only'
? ancestor.snapshotEvents()
: undefined
const session = ctx.sessions.create(sessionId, {
...lineage === undefined ? {} : { seed: lineage, inheritedEventCount: SessionLogOffset(lineage.length) },
meta: {
cwd: '/workspace',
...(childMode === undefined ? {} : {
origin: 'subagent' as const,
parentSession: SessionId('offline-parent'),
}),
...lineage === undefined ? {} : { isSeeded: true },
},
})
if (childMode === 'continuable' || childMode === 'seeded-continuable') {
session.append('subagent/descriptor', snapshotSubagentDescriptor({
mode: 'continuable', provider: 'test', label: 'child',
}))
} else if (childMode === 'one-shot') {
session.append('subagent/descriptor', snapshotSubagentDescriptor({
mode: 'one-shot', provider: 'test', label: 'child',
}))
} else if (childMode === 'corrupt') {
session.append('subagent/descriptor', {
version: SUBAGENT_DESCRIPTOR_VERSION,
mode: 'continuable',
provider: 1,
} as never)
}
const inbox = createInboxStub()
const steer = vi.fn((message: UserMessage) => { inbox.append('next-step', message) })
const cancel = vi.fn()
const agent = {
id: session.id,
session,
inbox,
status: 'running',
ctx,
steer,
followup: vi.fn(),
cancel,
} as unknown as Agent
await ctx.agents.register(agent)
ctx.provide('workspaceRegistry', { get: () => undefined, list: () => [] } as never)
ctx.provide('agentDefaultModel', {
currentSelection: () => ({ provider: 'fixture', model: 'fixture-model' }),
saveSelection: () => Promise.resolve(),
} as never)
const selection: ModelSelectionRef = {
current: { provider: 'fixture', model: 'fixture-model' },
assembled: undefined,
}
const agents = {
resolveAgent: (id: SessionId) => Promise.resolve(id === agent.id
? { agent }
: { error: new RemoteError('session/not-found', 'missing', { sessionId: id }) }),
selectionFor: () => selection,
serializeImageAdmission: <Value>(_agent: Agent, operation: () => Promise<Value>) => operation(),
composeAgent: () => Promise.resolve({ setup: () => {} }),
} as unknown as ApiSessionAgentController
return {
ctx,
controller: new SessionCommandController(ctx, agents, '/workspace'),
agent,
inbox,
steer,
cancel,
}
}
async function expectFailure(operation: Promise<unknown>, code: string): Promise<void> {
await expect(operation).rejects.toMatchObject({ code })
}
describe('Session queue commands', () => {
it('preserves the cold Agent resolver rejection', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
const error = new RemoteError('session/agent-busy', 'owned by a child', { reason: 'subagent-owned' })
const controller = new SessionCommandController(ctx, {
resolveAgent: () => Promise.resolve({ error }),
} as unknown as ApiSessionAgentController, '/workspace')
try {
await expect(controller.updateQueue({
sessionId: SessionId('cold-child'), itemId: MessageId('pending'), action: { kind: 'remove' },
})).rejects.toBe(error)
} finally {
await ctx.fiber.dispose()
}
})
it('edits, removes, steers, and rejects stale queue occurrences', async () => {
const { ctx, controller, agent, inbox, steer, cancel } = await commandHarness()
const queued = createUserMessage({ content: [{ type: 'text', text: 'queued' }], source: { kind: 'user' } })
const nextStep = createUserMessage({ content: [{ type: 'text', text: 'step' }], source: { kind: 'user' } })
inbox.append('next-turn', queued)
inbox.append('next-step', nextStep)
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: agent.id,
itemId: queued.id,
action: {
kind: 'edit',
content: [{
type: 'image',
attachment: {
attachmentId: AttachmentId('att-edit'), mediaType: 'image/png', bytes: 1, width: 1, height: 1,
},
}],
},
})), 'session/attachment-invalid')
for (const content of [[], [{ type: 'text' as const, text: ' \t\n' }]]) {
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: agent.id,
itemId: queued.id,
action: { kind: 'edit', content },
})), 'gateway/bad-request')
}
expect(inbox.nextTurn[0]?.content).toEqual([{ type: 'text', text: 'queued' }])
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: SessionId('missing'), itemId: queued.id, action: { kind: 'remove' },
})), 'session/queue-item-not-found')
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: agent.id, itemId: MessageId('missing'), action: { kind: 'remove' },
})), 'session/queue-item-not-found')
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: agent.id, itemId: nextStep.id, action: { kind: 'steer' },
})), 'session/steer-unavailable')
Object.assign(agent, { status: 'idle' })
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: agent.id, itemId: queued.id, action: { kind: 'steer' },
})), 'session/steer-unavailable')
expect(await controller.updateQueue({
sessionId: agent.id,
itemId: queued.id,
action: { kind: 'edit', content: [{ type: 'text', text: 'edited' }] },
})).toEqual({ accepted: true })
expect(inbox.nextTurn[0]?.content).toEqual([{ type: 'text', text: 'edited' }])
// An edit rewrites content in place, so the occurrence a client addressed
// by id stays addressable.
expect(inbox.nextTurn[0]?.id).toBe(queued.id)
expect(await controller.updateQueue({
sessionId: agent.id, itemId: nextStep.id, action: { kind: 'remove' },
})).toEqual({ accepted: true })
Object.assign(agent, { status: 'running' })
const steered = inbox.nextTurn[0]
if (steered === undefined) throw new Error('missing edited queue item')
expect(await controller.updateQueue({
sessionId: agent.id, itemId: steered.id, action: { kind: 'steer' },
})).toEqual({ accepted: true })
expect(steer).toHaveBeenCalledWith(steered)
const queuedFile = createUserMessage({
content: [{
type: 'file',
attachment: { attachmentId: AttachmentId('file-queued'), name: 'queued.txt', bytes: 6 },
}],
source: { kind: 'user', rpcId: 'file-rpc' as never },
})
inbox.append('next-turn', queuedFile)
expect(await controller.updateQueue({
sessionId: agent.id, itemId: queuedFile.id, action: { kind: 'steer' },
})).toEqual({ accepted: true })
expect(steer).toHaveBeenLastCalledWith(queuedFile)
expect(queuedFile).toMatchObject({
source: { kind: 'user', rpcId: 'file-rpc' },
content: [{ type: 'file', attachment: { name: 'queued.txt', bytes: 6 } }],
})
await expectFailure(Promise.resolve().then(() => controller.cancel({
sessionId: SessionId('missing'),
})), 'session/not-found')
expect(controller.cancel({ sessionId: agent.id })).toEqual({ accepted: true })
expect(cancel).toHaveBeenCalledWith({ kind: 'user' }, { keepInbox: true })
await ctx.fiber.dispose()
})
it.each(['continuable', 'seeded-continuable'] as const)(
'mutates both inbox destinations of a live %s child while its parent is offline',
async (childMode) => {
const { ctx, controller, agent, inbox, steer } = await commandHarness(childMode)
const queued = createUserMessage({
content: [{ type: 'text', text: 'queued' }], source: { kind: 'user' },
})
const context = createUserMessage({
content: [{ type: 'text', text: 'context' }], source: { kind: 'test' },
})
inbox.append('next-turn', queued)
inbox.append('next-step', context)
expect(await controller.updateQueue({
sessionId: agent.id,
itemId: context.id,
action: { kind: 'edit', content: [{ type: 'text', text: 'edited context' }] },
})).toEqual({ accepted: true })
const editedContext = inbox.nextStep[0]
expect(editedContext).toMatchObject({
content: [{ type: 'text', text: 'edited context' }],
source: context.source,
})
expect(editedContext?.id).toBe(context.id)
if (editedContext === undefined) throw new Error('missing edited context')
expect(await controller.updateQueue({
sessionId: agent.id, itemId: editedContext.id, action: { kind: 'remove' },
})).toEqual({ accepted: true })
expect(await controller.updateQueue({
sessionId: agent.id, itemId: queued.id, action: { kind: 'steer' },
})).toEqual({ accepted: true })
expect(steer).toHaveBeenCalledWith(queued)
await ctx.fiber.dispose()
},
)
it('removes the selected message before handing it to Agent steering', async () => {
const { ctx, controller, agent, inbox, steer } = await commandHarness('continuable')
const first = createUserMessage({
content: [{ type: 'text', text: 'first' }], source: { kind: 'user' },
})
const second = createUserMessage({
content: [{ type: 'text', text: 'second' }], source: { kind: 'user' },
})
inbox.append('next-turn', first)
inbox.append('next-turn', second)
// Stand in for the Agent's cancellation-convergence destination; the
// command must accept whichever boundary `Agent.steer()` selects.
steer.mockImplementation((message: UserMessage) => { inbox.append('next-turn', message) })
expect(await controller.updateQueue({
sessionId: agent.id, itemId: first.id, action: { kind: 'steer' },
})).toEqual({ accepted: true })
expect(steer).toHaveBeenCalledWith(first)
// Ordering proves the removal happened before delivery rather than after.
expect(inbox.nextTurn).toEqual([second, first])
expect(inbox.nextStep).toEqual([])
await ctx.fiber.dispose()
})
it('keeps one-shot, seed-only, missing, and malformed child descriptors behind the ownership fence', async () => {
for (const mode of ['one-shot', 'seed-only', 'unknown', 'corrupt'] as const) {
const { ctx, controller, agent, inbox } = await commandHarness(mode)
const queued = createUserMessage({
content: [{ type: 'text', text: mode }], source: { kind: 'user' },
})
inbox.append('next-turn', queued)
await expectFailure(Promise.resolve().then(() => controller.updateQueue({
sessionId: agent.id, itemId: queued.id, action: { kind: 'remove' },
})), 'session/agent-busy')
expect(inbox.nextTurn).toEqual([queued])
await ctx.fiber.dispose()
}
})
})
function imageRef(id: string): ImageAttachmentRef {
return {
attachmentId: AttachmentId(id),
mediaType: 'image/png',
bytes: 1,
width: 1,
height: 1,
}
}
function event(type: string, seq: SessionSeq, data: unknown): SessionEvent {
return { type, seq, time: seq + 1, data } as SessionEvent
}
async function persistedController(
events: SessionEvent[],
readImage: (ref: ImageAttachmentRef) => Promise<{ ref: ImageAttachmentRef; data: Uint8Array }>,
): Promise<{ ctx: Context; controller: SessionCommandController; sessionId: SessionId }> {
const ctx = new Context()
await ctx.plugin(SessionStore)
const sessionId = SessionId('cold-attachment')
const meta: SessionHeader = {
version: SESSION_FORMAT_VERSION,
id: sessionId,
createdAt: 1,
cwd: '/workspace',
isSeeded: false,
}
ctx.provide('sessionPersistence', testSessionPersistence(ctx, {
list: () => Promise.resolve([meta]),
inspect: () => Promise.resolve({
meta,
inheritedEventCount: SessionLogOffset(0),
events,
}),
}) as never)
installSessionReadTestServices(ctx)
ctx.provide('attachments', { readImage } as never)
const agents = { resolveAgent: vi.fn() } as unknown as ApiSessionAgentController
return { ctx, controller: new SessionCommandController(ctx, agents, '/workspace'), sessionId }
}
describe('Session attachment authorization', () => {
it('finds references in direct, message, inserted, nested, and streamed content', async () => {
const nested = imageRef('nested')
const message = imageRef('message')
const inserted = imageRef('inserted')
const streamed = imageRef('streamed')
const events: SessionEvent[] = [
{ ...event('fixture/direct', SessionSeq(0), {
content: [null, [], { type: 'tool-result', content: [{ type: 'text', text: 'none' }] }, {
type: 'tool-result', content: [{ type: 'image', attachment: nested }],
}],
}), ignorable: true as const },
{
type: 'assistant/message', seq: SessionSeq(1), time: 2, surfaceOp: 'append',
data: {
turn: 1,
step: 1,
stream: [],
message: createAssistantMessage({
content: [{ type: 'image', attachment: message }],
source: { provider: 'fixture', model: 'fixture' },
}),
},
},
event('agent/inbox/spliced', SessionSeq(2), {
target: 'next-turn',
start: 0,
inserted: [createUserMessage({
content: [{ type: 'image', attachment: inserted }],
source: { kind: 'user' },
})],
}),
event('assistant/attempt', SessionSeq(3), {
turn: 1,
step: 1,
stream: [
{
type: 'chunk',
time: 3,
chunk: { type: 'block-start', index: 0, blockType: 'text' },
},
{
type: 'chunk',
time: 3,
chunk: { type: 'block-end', index: 0, block: { type: 'text', text: '' } },
},
],
}),
event('assistant/attempt', SessionSeq(4), {
turn: 1,
step: 1,
stream: [{
type: 'chunk',
time: 4,
chunk: { type: 'block-end', index: 0, block: { type: 'image', attachment: streamed } },
}],
}),
]
const readImage = vi.fn((ref: ImageAttachmentRef) => Promise.resolve({ ref, data: Uint8Array.of(1) }))
const { ctx, controller, sessionId } = await persistedController(events, readImage)
for (const ref of [nested, message, inserted, streamed]) {
await expect(controller.attachment({ sessionId, attachmentId: ref.attachmentId }))
.resolves.toEqual({ attachment: ref, data: 'AQ==' })
}
expect(readImage).toHaveBeenCalledTimes(4)
await ctx.fiber.dispose()
})
it('maps missing persistence identities and attachment backend failures', async () => {
const noPersistence = new Context()
await noPersistence.plugin(SessionStore)
installSessionReadTestServices(noPersistence)
const noPersistenceController = new SessionCommandController(
noPersistence,
{ resolveAgent: vi.fn() } as unknown as ApiSessionAgentController,
'/workspace',
)
await expectFailure(noPersistenceController.attachment({
sessionId: SessionId('missing'), attachmentId: AttachmentId('att'),
}), 'session/not-found')
const missing = new Context()
await missing.plugin(SessionStore)
missing.provide('sessionPersistence', testSessionPersistence(missing, {
list: () => Promise.resolve([]),
inspect: vi.fn(),
}) as never)
installSessionReadTestServices(missing)
const missingController = new SessionCommandController(
missing,
{ resolveAgent: vi.fn() } as unknown as ApiSessionAgentController,
'/workspace',
)
await expectFailure(missingController.attachment({
sessionId: SessionId('missing'), attachmentId: 'att' as never,
}), 'session/not-found')
for (const thrown of [
new AttachmentError('stored image is unavailable', 'ATTACHMENT_NOT_FOUND'),
new Error('backend offline'),
]) {
const ref = imageRef(`failure-${thrown.name}`)
const fixture = await persistedController(
[event('fixture/content', SessionSeq(0), { content: [{ type: 'image', attachment: ref }] })],
() => Promise.reject(thrown),
)
await expectFailure(fixture.controller.attachment({
sessionId: fixture.sessionId,
attachmentId: ref.attachmentId,
}), thrown instanceof AttachmentError ? 'session/attachment-invalid' : 'gateway/internal')
await fixture.ctx.fiber.dispose()
}
})
it('maps a cold observation failure to an internal authorization error', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
installSessionReadTestServices(ctx)
vi.spyOn(ctx.sessionQuery, 'observeSession').mockRejectedValue(new Error('storage offline'))
const controller = new SessionCommandController(
ctx,
{ resolveAgent: vi.fn() } as unknown as ApiSessionAgentController,
'/workspace',
)
await expectFailure(controller.attachment({
sessionId: SessionId('unreadable'), attachmentId: AttachmentId('att'),
}), 'gateway/internal')
await ctx.fiber.dispose()
})
})