Merge pinned master 84bde6c7 into V3 readiness repair

This commit is contained in:
Tianyi Cui
2026-09-08 13:16:35 +08:00
308 changed files with 5060 additions and 1314 deletions
@@ -133,6 +133,14 @@ describe('Session queue commands', () => {
}],
},
})), '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')
@@ -17,6 +17,7 @@ import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import type { Agent, Inbox } from '@deepseek-ai/dsh-agent'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import AttachmentStore from '@deepseek-ai/dsh-attachment'
import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts'
import {
SessionPersistenceRevision,
@@ -678,6 +679,101 @@ describe('degenerate composition (no persistence, no factory)', () => {
})
describe('sessions.prompt synchronous rejection', () => {
it('rejects content without non-whitespace text or an attachment before delivery or Session events', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-empty-prompt'))
const followup = vi.fn()
const steer = vi.fn()
ctx.agents.register({
id: session.id,
session,
inbox: inboxFor(),
status: 'idle',
ctx,
followup,
steer,
} as unknown as Agent)
const savedImage = {
attachmentId: 'accepted-image',
mediaType: 'image/png' as const,
bytes: 1,
width: 1,
height: 1,
}
const saveImages = vi.fn(() => Promise.resolve([savedImage]))
ctx.provide('attachments', Object.setPrototypeOf(
{ saveImages },
AttachmentStore.prototype,
) as never)
ctx.provide('llm', {
listProviders: () => [{ id: 'p', name: 'Provider' }],
resolveModelInfo: () => Promise.resolve({
provider: 'p', id: 'm', name: 'Model', inputModalities: ['text', 'image'],
}),
} as never)
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
cwd: '/tmp',
})
const initialEvents = session.snapshotEvents()
const rejectedContent: readonly SessionPromptRequest['content'][] = [
[],
[{ type: 'text', text: '' }],
[{ type: 'text', text: ' \t\n' }, { type: 'text', text: '' }],
]
for (const [index, content] of rejectedContent.entries()) {
const response = await remote.prompt(promptRequest({
sessionId: session.id,
mode: index === 1 ? 'steer' : 'queue',
content,
}))
expect(response).toMatchObject({
ok: false,
error: {
code: 'gateway/bad-request',
message: 'prompt content must include non-whitespace text or an attachment',
details: {},
},
})
}
expect(followup).not.toHaveBeenCalled()
expect(steer).not.toHaveBeenCalled()
expect(session.snapshotEvents()).toEqual(initialEvents)
const queued = await remote.prompt(promptRequest({
sessionId: session.id,
mode: 'queue',
content: [{ type: 'text', text: ' queued ' }],
}))
const steered = await remote.prompt(promptRequest({
sessionId: session.id,
mode: 'steer',
content: [{ type: 'text', text: 'steered' }],
}))
const imageQueued = await remote.prompt(promptRequest({
sessionId: session.id,
mode: 'queue',
content: [{ type: 'image', mediaType: 'image/png', data: 'AQ==' }],
}))
expect(queued).toMatchObject({ ok: true, value: { accepted: true } })
expect(steered).toMatchObject({ ok: true, value: { accepted: true } })
expect(imageQueued).toMatchObject({ ok: true, value: { accepted: true } })
expect(followup).toHaveBeenCalledWith(expect.objectContaining({
content: [{ type: 'text', text: ' queued ' }],
}))
expect(steer).toHaveBeenCalledWith(expect.objectContaining({
content: [{ type: 'text', text: 'steered' }],
}))
expect(saveImages).toHaveBeenCalledOnce()
expect(followup).toHaveBeenCalledWith(expect.objectContaining({
content: [{ type: 'image', attachment: savedImage }],
}))
await ctx.fiber.dispose()
})
it('maps a synchronous send throw (disposed/invalid input) to agent-busy with the reason attached', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)