import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit' import type { Agent, ModelSelectionRef } from '@deepseek-ai/dsh-agent' import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment' import type { FileAttachmentRef, ImageAttachmentRef, SaveFileAttachment, SaveFileStreamAttachment, } from '@deepseek-ai/dsh-attachment' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { UserMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import CommandRuntime from '@deepseek-ai/dsh-commands' import { createScope } from '@deepseek-ai/dsh-scope' import FileUploads from '@deepseek-ai/dsh-client-file-upload' import type { FileUploadReceiptId } from '@deepseek-ai/dsh-client-file-upload/types' import { describe, expect, it, vi } from 'vitest' import type { ApiSessionAgentController } from '../src/agent.ts' import { SessionCommandController } from '../src/commands.ts' import type { SessionRequestId } from '../src/types.ts' const SESSION = SessionId('upload-session') async function uploadHarness(origin?: 'subagent'): Promise<{ ctx: Context controller: SessionCommandController uploads: FileUploads agent: Agent followup: ReturnType saveFile: ReturnType saveFileStream: ReturnType saveImages: ReturnType disposeAgent: () => void uploadRoute: (request: Request) => Promise }> { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) await ctx.plugin(CommandRuntime) const session = ctx.sessions.create(SESSION, { meta: { cwd: '/workspace', ...(origin === undefined ? {} : { origin }) }, }) const inbox = createInboxStub() const followup = vi.fn() const agent = { id: session.id, session, inbox, status: 'idle', ctx: undefined, steer: vi.fn(), followup, cancel: vi.fn(), } as unknown as Agent ;(agent as { ctx: Context }).ctx = createScope(ctx, agent).ctx const disposeAgent = ctx.agents.register(agent) const saveFile = vi.fn((input: SaveFileAttachment): Promise => Promise.resolve({ attachmentId: AttachmentId(`sha256:${'cd'.repeat(32)}`), name: input.name ?? 'file', bytes: input.data.byteLength, })) const saveFileStream = vi.fn(async (input: SaveFileStreamAttachment): Promise => { let bytes = 0 for await (const chunk of input.data) bytes += chunk.byteLength return { attachmentId: AttachmentId(`sha256:${'ef'.repeat(32)}`), name: input.name ?? 'file', bytes, } }) const saveImages = vi.fn((): Promise => Promise.reject(new Error('fixture did not expect image persistence'))) ctx.provide('attachments', Object.setPrototypeOf( { saveFile, saveFileStream, saveImages }, AttachmentStore.prototype, ) as never) let uploadRoute: ((request: Request) => Promise) | undefined ctx.provide('connection', { fetch: { register: (route: { readonly fetch: (request: Request) => Promise }) => { uploadRoute = route.fetch return () => {} }, }, } as never) ctx.provide('llm', { listProviders: () => [{ id: 'fixture', name: 'Fixture' }], resolveModelInfo: () => Promise.resolve({ provider: 'fixture', id: 'fixture-model', name: 'Fixture' }), } as never) const selection: ModelSelectionRef = { current: { provider: 'fixture', model: 'fixture-model' }, assembled: undefined, } const agents = { resolveAgent: () => Promise.resolve({ agent }), selectionFor: () => selection, serializeImageAdmission: (_agent: Agent, operation: () => Promise) => operation(), } as unknown as ApiSessionAgentController const uploads = new FileUploads(ctx) if (uploadRoute === undefined) throw new Error('file upload route was not registered') return { ctx, controller: new SessionCommandController(ctx, agents, '/workspace'), uploads, agent, followup, saveFile, saveFileStream, saveImages, disposeAgent, uploadRoute, } } function promptRequest(content: Parameters[0]['content']) { return { requestId: 'req-1' as SessionRequestId, sessionId: SESSION, mode: 'queue' as const, content, } } describe('Session file uploads', () => { it('registers an HTTP route bound to the upload service', async () => { const { uploadRoute } = await uploadHarness() await expect(uploadRoute(new Request('http://host/upload'))) .resolves.toMatchObject({ status: 405 }) }) it('stages one verbatim upload and preserves its order with an admitted image', async () => { const { ctx, controller, uploads, agent, followup, saveFile, saveImages } = await uploadHarness() const receipt = await uploads.upload(agent, { data: 'AAAA', name: 'notes.pdf' }, new AbortController().signal) expect(saveFile).toHaveBeenCalledTimes(1) expect(receipt.file.name).toBe('notes.pdf') expect(receipt.file.bytes).toBe(3) expect(uploads.resolve(agent, receipt.receiptId)).toEqual(receipt.file) expect(uploads.resolve(agent, 'missing' as FileUploadReceiptId)).toBeUndefined() const commandHandler = vi.fn((_invocation: unknown) => ({ kind: 'success' as const })) ctx.commands.register({ name: 'files', description: 'Use staged files', input: { hint: '', attachments: true }, handler: commandHandler, }) await ctx.commands.execute( agent, '/files inspect', [{ type: 'file', receiptId: receipt.receiptId }], new AbortController().signal, ) expect(commandHandler.mock.calls[0]?.[0]).toMatchObject({ attachments: [{ type: 'file', attachment: receipt.file }], }) const image: ImageAttachmentRef = { attachmentId: AttachmentId(`sha256:${'ab'.repeat(32)}`), mediaType: 'image/png', bytes: 3, width: 1, height: 1, } saveImages.mockResolvedValueOnce([image]) await controller.prompt(promptRequest([ { type: 'file', receiptId: receipt.receiptId }, { type: 'image', mediaType: 'image/png', data: 'AAAA' }, { type: 'text', text: 'read it' }, ])) expect(followup).toHaveBeenCalledTimes(1) const message = followup.mock.calls[0]?.[0] as UserMessage expect(message.content).toEqual([ { type: 'file', attachment: receipt.file }, { type: 'image', attachment: image }, { type: 'text', text: 'read it' }, ]) }) it('stages a bounded byte stream and forwards cancellation to storage', async () => { const { controller, uploads, followup, saveFileStream } = await uploadHarness() const abort = new AbortController() const receipt = await uploads.uploadStream({ sessionId: SESSION, data: (async function* (): AsyncIterable { yield Uint8Array.of(1, 2) yield Uint8Array.of(3, 4, 5) })(), signal: abort.signal, name: 'huge.bin', }) expect(saveFileStream).toHaveBeenCalledWith(expect.objectContaining({ signal: abort.signal, name: 'huge.bin', })) expect(receipt.file).toMatchObject({ name: 'huge.bin', bytes: 5 }) await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }])) expect((followup.mock.calls[0]?.[0] as UserMessage).content).toEqual([ { type: 'file', attachment: receipt.file }, ]) }) it('keeps the stream name optional and maps storage failures through the same error vocabulary', async () => { const { uploads, saveFileStream } = await uploadHarness() const stream = (async function* (): AsyncIterable {})() await expect(uploads.uploadStream({ sessionId: SESSION, data: stream })) .resolves.toMatchObject({ file: { name: 'file', bytes: 0 } }) expect(saveFileStream).toHaveBeenLastCalledWith({ data: stream }) saveFileStream.mockRejectedValueOnce('disk offline') await expect(uploads.uploadStream({ sessionId: SESSION, data: (async function* (): AsyncIterable { yield Uint8Array.of(1) })(), })) .rejects.toMatchObject({ code: 'gateway/internal', message: 'failed to store file upload: disk offline', }) }) it('rejects a prompt citing a file that was never staged for the session', async () => { const { controller, followup, saveImages } = await uploadHarness() await expect(controller.prompt(promptRequest([ { type: 'image', mediaType: 'image/png', data: 'AAAA' }, { type: 'file', receiptId: 'missing-receipt' as FileUploadReceiptId }, ]))).rejects.toMatchObject({ code: 'session/attachment-invalid', details: { reason: 'FILE_NOT_STAGED' } }) expect(followup).not.toHaveBeenCalled() expect(saveImages).not.toHaveBeenCalled() }) it('publishes no receipt when its exact Agent is disposed during storage', async () => { const { uploads, agent, saveFile, disposeAgent } = await uploadHarness() const saved = Promise.withResolvers() saveFile.mockReturnValueOnce(saved.promise) const uploading = uploads.upload(agent, { data: 'AAAA', name: 'late.bin' }, new AbortController().signal) await vi.waitFor(() => { expect(saveFile).toHaveBeenCalledOnce() }) disposeAgent() saved.resolve({ attachmentId: AttachmentId(`sha256:${'ab'.repeat(32)}`), name: 'late.bin', bytes: 3, }) await expect(uploading).rejects.toMatchObject({ code: 'session/not-found' }) }) it('resolves a cold ordinary Agent and releases the resolver registration', async () => { const { ctx, uploads, agent, disposeAgent } = await uploadHarness() disposeAgent() const resolveAgent = vi.fn(async () => { ctx.agents.register(agent) return agent }) const disposeResolver = uploads.registerAgentResolver(resolveAgent) expect(() => { uploads.registerAgentResolver(resolveAgent) }).toThrow('already registered') await expect(uploads.uploadStream({ sessionId: SESSION, data: (async function* (): AsyncIterable { yield Uint8Array.of(1) })(), })).resolves.toMatchObject({ file: { bytes: 1 } }) expect(resolveAgent).toHaveBeenCalledWith(SESSION) disposeResolver() const replacement = vi.fn(async () => agent) const disposeReplacement = uploads.registerAgentResolver(replacement) disposeResolver() expect(() => { uploads.registerAgentResolver(replacement) }).toThrow('already registered') expect(disposeReplacement).toBeTypeOf('function') disposeReplacement() }) it('rejects a cold upload when no Agent resolver is registered', async () => { const { uploads, disposeAgent } = await uploadHarness() disposeAgent() await expect(uploads.uploadStream({ sessionId: SESSION, data: (async function* (): AsyncIterable {})(), })).rejects.toMatchObject({ code: 'session/not-found' }) }) it('rejects subagent uploads and access outside the receiving Agent scope', async () => { const child = await uploadHarness('subagent') await expect(child.uploads.upload( child.agent, { data: 'AAAA' }, new AbortController().signal, )).rejects.toMatchObject({ code: 'subagent/attachment-invalid', details: { reason: 'SUBAGENT_FILE_UNSUPPORTED' }, }) expect(child.saveFile).not.toHaveBeenCalled() const ordinary = await uploadHarness() const receipt = await ordinary.uploads.upload( ordinary.agent, { data: 'AAAA' }, new AbortController().signal, ) const foreignScope = { ...ordinary.agent, ctx: ordinary.ctx } as Agent expect(() => ordinary.uploads.resolve(foreignScope, receipt.receiptId)) .toThrow('operation requires the Agent\'s own scope') }) it('retires accepted receipts after their rpcId becomes observable', async () => { const { controller, uploads, agent } = await uploadHarness() uploads.retirePrompt(agent, 'not-staged') const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal) await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }])) expect(uploads.resolve(agent, receipt.receiptId)).toEqual(receipt.file) uploads.retirePrompt(agent, 'other-request') expect(uploads.resolve(agent, receipt.receiptId)).toEqual(receipt.file) uploads.retirePrompt(agent, 'req-1') expect(uploads.resolve(agent, receipt.receiptId)).toBeUndefined() }) it('retires observed prompt receipts and drops staged state with the Session', async () => { const first = await uploadHarness() const observed = await first.uploads.upload( first.agent, { data: 'AAAA' }, new AbortController().signal, ) await first.controller.prompt(promptRequest([{ type: 'file', receiptId: observed.receiptId }])) first.ctx.emit('session/event', first.agent.session, { type: 'user/message', data: createUserMessage({ content: [{ type: 'text', text: 'extension event' }], source: { kind: 'user', rpcId: 1 } as never, }), } as never) expect(first.uploads.resolve(first.agent, observed.receiptId)).toEqual(observed.file) first.agent.session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'observed' }], source: { kind: 'user', rpcId: 'req-1' as SessionRequestId }, }), { surfaceOp: 'append' }) expect(first.uploads.resolve(first.agent, observed.receiptId)).toBeUndefined() const second = await uploadHarness() const abandoned = await second.uploads.upload( second.agent, { data: 'AAAA' }, new AbortController().signal, ) second.ctx.emit('session/disposed', second.agent.session) expect(second.uploads.resolve(second.agent, abandoned.receiptId)).toBeUndefined() }) it('deduplicates a retried rpcId already present in the Agent inbox', async () => { const { controller, agent, followup } = await uploadHarness() const request = promptRequest([{ type: 'text', text: 'once' }]) await controller.prompt(request) agent.inbox.append('next-turn', followup.mock.calls[0]?.[0] as UserMessage) await expect(controller.prompt(request)).resolves.toEqual({ accepted: true }) expect(followup).toHaveBeenCalledOnce() }) it('deduplicates a retried rpcId already present in the durable log', async () => { const { controller, agent, followup } = await uploadHarness() const request = promptRequest([{ type: 'text', text: 'once' }]) agent.session.append('turn/start', { turn: 1 }) agent.session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'unidentified' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) agent.session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'accepted' }], source: { kind: 'user', rpcId: request.requestId }, }), { surfaceOp: 'append' }) await expect(controller.prompt(request)).resolves.toEqual({ accepted: true }) expect(followup).not.toHaveBeenCalled() }) it('rejects when the Agent disappears during prompt admission', async () => { const { controller, saveImages, disposeAgent } = await uploadHarness() const admitted = Promise.withResolvers() saveImages.mockReturnValueOnce(admitted.promise) const prompting = controller.prompt(promptRequest([ { type: 'image', mediaType: 'image/png', data: 'AAAA' }, ])) await vi.waitFor(() => { expect(saveImages).toHaveBeenCalledOnce() }) disposeAgent() admitted.resolve([{ attachmentId: AttachmentId('admitted-image'), mediaType: 'image/png', bytes: 3, width: 1, height: 1, }]) await expect(prompting).rejects.toMatchObject({ code: 'session/not-found' }) }) it('rejects when a previously bound receipt retires during image admission', async () => { const { controller, uploads, agent, saveImages } = await uploadHarness() const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal) await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }])) const admitted = Promise.withResolvers() saveImages.mockReturnValueOnce(admitted.promise) const prompting = controller.prompt({ ...promptRequest([ { type: 'file', receiptId: receipt.receiptId }, { type: 'image', mediaType: 'image/png', data: 'AAAA' }, ]), requestId: 'req-2' as SessionRequestId, }) await vi.waitFor(() => { expect(saveImages).toHaveBeenCalledOnce() }) uploads.retirePrompt(agent, 'req-1') admitted.resolve([{ attachmentId: AttachmentId('admitted-image'), mediaType: 'image/png', bytes: 3, width: 1, height: 1, }]) await expect(prompting).rejects.toMatchObject({ code: 'session/attachment-invalid', details: { reason: 'FILE_NOT_STAGED' }, }) }) it('keeps the prior receipt binding when a later prompt attempt fails', async () => { const { controller, uploads, agent, followup } = await uploadHarness() const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal) await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }])) followup.mockImplementationOnce(() => { throw new Error('busy') }) await expect(controller.prompt({ ...promptRequest([{ type: 'file', receiptId: receipt.receiptId }]), requestId: 'req-2' as SessionRequestId, })).rejects.toMatchObject({ code: 'session/agent-busy' }) uploads.retirePrompt(agent, 'req-1') expect(uploads.resolve(agent, receipt.receiptId)).toBeUndefined() }) it('restores an unbound receipt after prompt delivery fails', async () => { const { controller, uploads, agent, followup } = await uploadHarness() const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal) followup.mockImplementationOnce(() => { throw new Error('busy') }) await expect(controller.prompt(promptRequest([ { type: 'file', receiptId: receipt.receiptId }, ]))).rejects.toMatchObject({ code: 'session/agent-busy' }) uploads.retirePrompt(agent, 'req-1') expect(uploads.resolve(agent, receipt.receiptId)).toEqual(receipt.file) }) it('retires a prompt-bound receipt when its queued occurrence is removed', async () => { const { controller, uploads, agent, followup } = await uploadHarness() const receipt = await uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal) await controller.prompt(promptRequest([{ type: 'file', receiptId: receipt.receiptId }])) const queued = followup.mock.calls[0]?.[0] as UserMessage agent.inbox.append('next-turn', queued) expect(controller.updateQueue({ sessionId: SESSION, itemId: queued.id, action: { kind: 'remove' }, })).toEqual({ accepted: true }) expect(uploads.resolve(agent, receipt.receiptId)).toBeUndefined() }) it('keeps separate names for identical bytes uploaded more than once', async () => { const { controller, uploads, agent, followup } = await uploadHarness() const first = await uploads.upload(agent, { data: 'AAAA', name: 'first.txt' }, new AbortController().signal) const second = await uploads.upload(agent, { data: 'AAAA', name: 'second.txt' }, new AbortController().signal) expect(first.file.attachmentId).toBe(second.file.attachmentId) expect(first.receiptId).not.toBe(second.receiptId) await controller.prompt(promptRequest([ { type: 'file', receiptId: first.receiptId }, { type: 'file', receiptId: second.receiptId }, ])) const message = followup.mock.calls[0]?.[0] as UserMessage expect(message.content).toEqual([ { type: 'file', attachment: first.file }, { type: 'file', attachment: second.file }, ]) }) it('maps a non-canonical payload to the wire attachment error', async () => { const { uploads, agent, saveFile } = await uploadHarness() await expect(uploads.upload(agent, { data: 'not base64!!' }, new AbortController().signal)) .rejects.toMatchObject({ code: 'session/attachment-invalid', details: { reason: 'INVALID_FILE_BASE64' } }) expect(saveFile).not.toHaveBeenCalled() }) it('maps an unexpected storage failure to the internal wire error', async () => { const { uploads, agent, saveFile } = await uploadHarness() saveFile.mockRejectedValueOnce(new Error('disk unavailable')) await expect(uploads.upload(agent, { data: 'AAAA' }, new AbortController().signal)) .rejects.toMatchObject({ code: 'gateway/internal', message: 'failed to store file upload: Error: disk unavailable', }) }) })