fix: address ds-review-bot v7 findings on the merged image-input head
- gate model selection on steering-placement image carriers from enqueue until their steering/message event publishes; release the gate when an admission ends idle without publication (both behaviorally asserted) - reject session.updateQueue edits carrying non-text blocks at the RPC boundary (queue edits cannot bypass image admission) - extend the durable-directory walk past a first-created DSH_HOME to the deepest pre-existing ancestor - strip Windows-style separators from attachment display names on POSIX - verify attachment reads with a header-only probe (digest already proves the bytes decoded fully at admission); document the read path - make SessionInputShell.addImages refusal observable and keep workspace transfers/composer intake from leaking refused drafts - own ONE recursive image walk (dsh-llm contentHasImage) across apiproxy, pi-ai, compact-basic, and the DeepSeek text-only assertion - drop the redundant canonical-base64 regex and the no-op role read - move AttachmentId/AttachmentError out of types.ts (brand.ts/error.ts); document why AttachmentError does not extend HarnessError - document the hard attachments inject in both consumer READMEs
This commit is contained in:
@@ -387,6 +387,70 @@ describe('Web session model selection', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('gates selection on a steering image from enqueue until its event publishes', async () => {
|
||||
const { ctx, sessionId, agent } = await harness()
|
||||
registerTextOnly(ctx)
|
||||
const api = createApiProxy(ctx, { provider: 'deepseek', model: 'deepseek-chat', cwd: '/tmp', workspaceRoot: '/tmp' })
|
||||
const steering = {
|
||||
id: 's-1', role: 'user', source: { kind: 'user' },
|
||||
content: [{ type: 'image', attachment: { attachmentId: 'att-s', mediaType: 'image/png', bytes: 8, width: 1, height: 1 } }],
|
||||
} as never
|
||||
const steeringItem = { id: 'i-s-1', message: steering, placement: 'steering' } as never
|
||||
// A steering carrier never enters the queued mirror, yet the outbox hop
|
||||
// between steer() and its append must not open a text-only switch window.
|
||||
ctx.emit('agent/inbox/enqueue', agent, steeringItem)
|
||||
expect((await api.sessions.selectModel(request({ sessionId, provider: 'text-only', model: 'plain' }))).result.ok).toBe(false)
|
||||
|
||||
ctx.emit('agent/inbox/dequeue', agent, steeringItem)
|
||||
expect((await api.sessions.selectModel(request({ sessionId, provider: 'text-only', model: 'plain' }))).result.ok).toBe(false)
|
||||
|
||||
// Publication hands the gate over to the durable surface.
|
||||
agent.session.append('steering/message', { turn: 1, message: steering }, { surfaceOp: 'append' })
|
||||
expect((await api.sessions.selectModel(request({ sessionId, provider: 'text-only', model: 'plain' }))).result.ok).toBe(false)
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('re-opens selection when an admission ends idle without publication', async () => {
|
||||
const { ctx, sessionId, agent } = await harness()
|
||||
registerTextOnly(ctx)
|
||||
const api = createApiProxy(ctx, { provider: 'deepseek', model: 'deepseek-chat', cwd: '/tmp', workspaceRoot: '/tmp' })
|
||||
const rejected = {
|
||||
id: 'r-1', role: 'user', source: { kind: 'user' },
|
||||
content: [{ type: 'image', attachment: { attachmentId: 'att-r', mediaType: 'image/png', bytes: 8, width: 1, height: 1 } }],
|
||||
} as never
|
||||
const rejectedItem = { id: 'i-r-1', message: rejected, placement: 'queued' } as never
|
||||
ctx.emit('agent/inbox/enqueue', agent, rejectedItem)
|
||||
ctx.emit('agent/inbox/dequeue', agent, rejectedItem)
|
||||
expect((await api.sessions.selectModel(request({ sessionId, provider: 'text-only', model: 'plain' }))).result.ok).toBe(false)
|
||||
|
||||
// Idle proves the admission ended without publication; nothing durable
|
||||
// requires an image route, so the text-only switch must be accepted again.
|
||||
ctx.emit('agent/status', agent, 'idle')
|
||||
expect(expectValue(await api.sessions.selectModel(request({
|
||||
sessionId, provider: 'text-only', model: 'plain',
|
||||
}))).selected).toEqual({ provider: 'text-only', model: 'plain' })
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects a queue edit that injects unadmitted image content', async () => {
|
||||
const { ctx, sessionId, agent } = await harness()
|
||||
const api = createApiProxy(ctx, { provider: 'deepseek', model: 'deepseek-chat', cwd: '/tmp', workspaceRoot: '/tmp' })
|
||||
Object.assign(agent, { updateInbox: () => 'applied' })
|
||||
const denied = await api.sessions.updateQueue(request({
|
||||
sessionId,
|
||||
itemId: 'i-x' as never,
|
||||
action: {
|
||||
kind: 'edit' as const,
|
||||
content: [{ type: 'image', attachment: { attachmentId: 'att-x', mediaType: 'image/png', bytes: 8, width: 1, height: 1 } }] as never,
|
||||
},
|
||||
}))
|
||||
expect(denied.result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'attachment-error', details: { reason: 'QUEUE_EDIT_NON_TEXT' } },
|
||||
})
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('serializes an image save with a concurrent model selection', async () => {
|
||||
const { ctx, sessionId, agent } = await harness()
|
||||
registerTextOnly(ctx)
|
||||
|
||||
Reference in New Issue
Block a user