/**
* Session Controller model-directory and selection behavior: dynamic provider grouping,
* provider-local catalog failures, logged-selection restoration without stale
* catalog injection, advisory pass-through models, and the prompt-assembly
* boundary for a running selection change.
*/
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AttachmentStore from '@deepseek-ai/dsh-attachment'
import LlmRuntime, { LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type {
GenerateOptions, LlmCallConfig, LlmCallConfigAdapterDefaults, LlmModelInfo,
LlmModelReasoningInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk,
UserMessage,
} from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts'
import { ApiSessionAgentController } from '../src/agent.ts'
import { buildModelCatalog } from '../src/catalog.ts'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
import { createSessionTestRemote } from './test-remote.ts'
function request
(payload: P): P {
return payload
}
let nextRequestId = 1
function promptRequest(
payload: Omit,
): SessionPromptRequest {
return {
...payload,
requestId: `models-${String(nextRequestId++)}` as SessionRequestId,
}
}
class CatalogAdapter extends LlmAdapter {
constructor(
private readonly name: string,
private readonly models: readonly LlmModelInfo[] | Error,
private readonly reasoning?: LlmModelReasoningInfo,
private readonly exactError?: Error,
) {
super()
}
override providerInfo(provider: string): LlmProviderInfo {
return { id: provider, name: this.name }
}
override listModels(): Promise {
return this.models instanceof Error
? Promise.reject(this.models)
: Promise.resolve(this.models)
}
override resolveModel(provider: string, model: string): Promise {
if (this.exactError !== undefined) return Promise.reject(this.exactError)
return Promise.resolve({
provider,
id: model,
name: model,
...this.reasoning === undefined ? {} : { reasoning: this.reasoning },
})
}
override async *stream(_options: GenerateOptions): AsyncIterable {
// Catalog tests never enter provider streaming.
}
}
const REASONING: LlmModelReasoningInfo = {
efforts: [
{ id: ReasoningEffortId('off'), name: 'Off' },
{ id: ReasoningEffortId('high'), name: 'High' },
{ id: ReasoningEffortId('max'), name: 'Max' },
],
defaultEffort: ReasoningEffortId('high'),
}
async function harness(logged?: {
provider: string
model: string
reasoningEffort?: ReasoningEffortId
adapterDefaults?: LlmCallConfigAdapterDefaults
}): Promise<{
ctx: Context
agent: Agent
sessionId: SessionId
}> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(LlmRuntime)
await ctx.plugin(AgentRegistry)
ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', [
{ provider: 'deepseek-official', id: 'deepseek-chat', name: 'DeepSeek Chat' },
{ provider: 'deepseek-official', id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', description: 'Reasoning model' },
], REASONING))
ctx.llm.registerAdapter(['broken'], new CatalogAdapter('Broken Provider', new Error('catalog offline')))
ctx.llm.registerAdapter(['metadata-broken'], new CatalogAdapter('Metadata Broken', [
{ provider: 'metadata-broken', id: 'listed', name: 'Listed' },
], undefined, new Error('reasoning metadata offline')))
ctx.llm.registerAdapter(['remote-rejected'], new CatalogAdapter(
'Remote Rejected',
[],
undefined,
new TypertRemoteFailure({
code: 'fixture-rejected',
message: 'fixture rejected the selection',
details: { provider: 'remote-rejected' },
}),
))
ctx.llm.registerAdapter(['empty'], new CatalogAdapter('Empty Provider', []))
ctx.llm.registerAdapter(['duplicate'], new CatalogAdapter('Duplicate Provider', [
{ provider: 'duplicate', id: 'same', name: 'Same' },
{ provider: 'duplicate', id: 'same', name: 'Same Again' },
]))
const session = ctx.sessions.create()
if (logged !== undefined) {
const { adapterDefaults, ...config } = logged
session.append('request/header', {
header: { config, ...adapterDefaults === undefined ? {} : { adapterDefaults } },
reason: 'initial',
})
}
const agent = {
id: session.id,
session,
status: 'running',
ctx,
inbox: { nextTurn: [], nextStep: [] },
} as unknown as Agent
ctx.agents.register(agent)
return { ctx, agent, sessionId: session.id }
}
function expectValue(result: { ok: true; value: T } | { ok: false }): T {
if (!result.ok) throw new Error('expected successful response')
return result.value
}
function registerTextOnly(ctx: Context): void {
ctx.llm.registerAdapter(['text-only'], new class extends CatalogAdapter {
override resolveModel(provider: string, model: string): Promise {
return Promise.resolve({ provider, id: model, name: model, inputModalities: ['text'] })
}
}('Text Only', []))
}
/** Resolve the Client-visible next selection from durable state and the Host default. */
function currentSelection(ctx: Context, sessionId: SessionId) {
const session = ctx.sessions.get(sessionId)
if (session === undefined) throw new Error('expected a live test Session')
return ctx.sessionProjections.snapshot(session).values.modelSelection?.next
?? ctx.agentDefaultModel.currentSelection()
}
describe('Web session model selection', () => {
it('validates an ordered image batch before persisting any member', async () => {
const { ctx, agent, sessionId } = await harness()
const validateImage = vi.fn((_input: { data: Uint8Array }) => Promise.resolve())
const saveImage = vi.fn((input: { data: Uint8Array; mediaType: 'image/png'; name?: string }) => Promise.resolve({
attachmentId: `att-${String(input.data[0])}`,
mediaType: input.mediaType,
bytes: input.data.byteLength,
width: 1,
height: 1,
...input.name === undefined ? {} : { name: input.name },
}))
const attachments = {
imageLimits: {
maxImageBytes: 4,
maxImagesPerMessage: 2,
maxMessageImageBytes: 4,
maxImagePixels: 4,
maxImageDimension: 2000,
mediaTypes: ['image/png'],
},
validateImage,
saveImage,
}
ctx.provide('attachments', Object.setPrototypeOf(attachments, AttachmentStore.prototype) as never)
const followup = vi.fn()
Object.assign(agent, { followup })
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }),
cwd: '/tmp',
})
const result = await remote.prompt(promptRequest({
sessionId,
mode: 'queue' as const,
content: [
{ type: 'image' as const, mediaType: 'image/png' as const, data: 'AQ==', name: 'first.png' },
{ type: 'text' as const, text: 'compare' },
{ type: 'image' as const, mediaType: 'image/png' as const, data: 'Ag==' },
],
}))
expect(result.ok).toBe(true)
expect(validateImage.mock.calls.map(([input]) => [...input.data])).toEqual([[1], [2]])
expect(saveImage.mock.calls.map(([input]) => [...input.data])).toEqual([[1], [2]])
expect((followup.mock.calls[0]?.[0] as UserMessage).content).toEqual([
{
type: 'image',
attachment: {
attachmentId: 'att-1', mediaType: 'image/png', bytes: 1, width: 1, height: 1, name: 'first.png',
},
},
{ type: 'text', text: 'compare' },
{ type: 'image', attachment: { attachmentId: 'att-2', mediaType: 'image/png', bytes: 1, width: 1, height: 1 } },
])
const denied = await remote.prompt(promptRequest({
sessionId,
mode: 'queue' as const,
content: Array.from({ length: 3 }, () => ({
type: 'image' as const, mediaType: 'image/png' as const, data: 'AQ==',
})),
}))
expect(denied).toMatchObject({
ok: false,
error: { code: 'attachment-error', details: { reason: 'TOO_MANY_IMAGES' } },
})
expect(saveImage).toHaveBeenCalledTimes(2)
await ctx.fiber.dispose()
})
it('allows a text-only selection while durable or pending images remain available for later models', async () => {
const { ctx, agent, sessionId } = await harness()
registerTextOnly(ctx)
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }),
cwd: '/tmp',
})
const image = {
type: 'image' as const,
attachment: { attachmentId: 'att-history', mediaType: 'image/png' as const, bytes: 1, width: 1, height: 1 },
}
const imageEvent = agent.session.append('user/message', {
id: 'image-message', role: 'user', source: { kind: 'user' }, content: [image],
} as never, { surfaceOp: 'append' })
expect(expectValue(await remote.selectModel(request({
sessionId, provider: 'text-only', model: 'plain',
}))).selected).toEqual({ provider: 'text-only', model: 'plain' })
agent.session.append('user/message', {
id: 'summary', role: 'user', source: { kind: 'plugin', plugin: 'compact' },
content: [{ type: 'text', text: 'image summarized' }],
} as never, {
surfaceOp: { op: 'replace', start: imageEvent.seq, end: imageEvent.seq },
sourceEventSeqs: [imageEvent.seq],
})
;(agent.inbox.nextTurn as UserMessage[]).push({
id: 'pending-image', role: 'user', source: { kind: 'user' }, content: [image],
} as never)
expect(expectValue(await remote.selectModel(request({
sessionId, provider: 'text-only', model: 'plain',
}))).selected).toEqual({ provider: 'text-only', model: 'plain' })
await ctx.fiber.dispose()
})
it('authorizes attachment bytes only when the session event stream references the id', async () => {
const { ctx, agent, sessionId } = await harness()
const ref = {
attachmentId: 'att-authorized', mediaType: 'image/png' as const, bytes: 2, width: 1, height: 1,
}
const readImage = vi.fn(() => Promise.resolve({ ref, data: Uint8Array.of(1, 2) }))
ctx.provide('attachments', { readImage } as never)
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }),
cwd: '/tmp',
})
agent.session.append('agent/inbox/spliced', {
target: 'next-turn',
start: 0,
inserted: [{
id: 'queued-image', role: 'user', source: { kind: 'user' },
content: [{ type: 'image', attachment: ref }],
}],
} as never)
const allowed = await remote.attachment(request({
sessionId, attachmentId: 'att-authorized' as never,
}))
expect(allowed).toMatchObject({ ok: true, value: { attachment: ref, data: 'AQI=' } })
const denied = await remote.attachment(request({
sessionId, attachmentId: 'att-other' as never,
}))
expect(denied).toMatchObject({
ok: false,
error: { code: 'attachment-error', details: { reason: 'ATTACHMENT_NOT_REFERENCED' } },
})
expect(readImage).toHaveBeenCalledOnce()
await ctx.fiber.dispose()
})
it('groups successful providers and leaves an unlisted current selection out of the catalog', async () => {
const { ctx, sessionId } = await harness({
provider: 'deepseek-official',
model: 'private-preview',
reasoningEffort: ReasoningEffortId('max'),
})
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' })
const catalog = expectValue(await remote.modelCatalog())
expect(currentSelection(ctx, sessionId)).toEqual({
provider: 'deepseek-official',
model: 'private-preview',
reasoningEffort: 'max',
})
expect(catalog.groups).toEqual([{
id: 'deepseek-official',
name: 'DeepSeek',
models: [
{ id: 'deepseek-chat', name: 'DeepSeek Chat', reasoning: REASONING },
{
id: 'deepseek-reasoner',
name: 'DeepSeek Reasoner',
description: 'Reasoning model',
reasoning: REASONING,
},
],
}])
expect(catalog.failures).toEqual([
{ id: 'broken', name: 'Broken Provider', message: 'catalog offline' },
{ id: 'metadata-broken', name: 'Metadata Broken', message: 'reasoning metadata offline' },
{
id: 'duplicate',
name: 'Duplicate Provider',
message: 'adapter returned invalid or duplicate model metadata for provider "duplicate"',
},
])
await ctx.fiber.dispose()
})
it('preserves optional catalog metadata and string provider failures', async () => {
const { ctx } = await harness()
ctx.llm.registerAdapter(['plain'], new CatalogAdapter('Plain', [
{ provider: 'plain', id: 'plain-model', name: 'Plain Model' },
]))
ctx.llm.registerAdapter(['described-reasoning'], new CatalogAdapter('Described Reasoning', [
{ provider: 'described-reasoning', id: 'reasoning-model', name: 'Reasoning Model' },
], {
efforts: [{ id: ReasoningEffortId('high'), name: 'High', description: 'More thinking' }],
}))
ctx.llm.registerAdapter(['string-failure'], new class extends CatalogAdapter {
override listModels(): Promise {
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- non-Error provider normalization is the scenario.
return Promise.reject('string catalog failure')
}
}('String Failure', []))
createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }),
cwd: '/tmp',
})
const catalog = await buildModelCatalog(ctx)
expect(catalog.groups).toEqual(expect.arrayContaining([
{ id: 'plain', name: 'Plain', models: [{ id: 'plain-model', name: 'Plain Model' }] },
{
id: 'described-reasoning',
name: 'Described Reasoning',
models: [{
id: 'reasoning-model',
name: 'Reasoning Model',
reasoning: {
efforts: [{ id: 'high', name: 'High', description: 'More thinking' }],
},
}],
},
]))
expect(catalog.failures).toContainEqual({
id: 'string-failure', name: 'String Failure', message: 'string catalog failure',
})
await ctx.fiber.dispose()
})
it('accepts an advisory-unlisted model, rejects an unavailable provider, and switches only after the next assembly', async () => {
const { ctx, agent, sessionId } = await harness()
const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' })
const seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 }
const signal = new AbortController().signal
expect(currentSelection(ctx, sessionId))
.toEqual({ provider: 'deepseek-official', model: 'deepseek-chat' })
const selected = expectValue(await remote.selectModel(request({
sessionId,
provider: 'deepseek-official',
model: 'private-preview',
reasoningEffort: 'max',
})))
expect(selected.selected).toEqual({
provider: 'deepseek-official',
model: 'private-preview',
reasoningEffort: 'max',
})
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', { turn: 1, step: 0, signal }, () => Promise.resolve(seed),
)).resolves.toEqual(seed)
expect((await ctx.systemPrompt.assemble()).variables)
.toMatchObject({ provider: 'deepseek-official', model: 'private-preview' })
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', { turn: 1, step: 1, signal }, () => Promise.resolve(seed),
)).resolves.toMatchObject({
provider: 'deepseek-official',
model: 'private-preview',
reasoningEffort: 'max',
})
const unsupported = await remote.selectModel(request({
sessionId,
provider: 'deepseek-official',
model: 'private-preview',
reasoningEffort: 'medium',
}))
expect(unsupported).toMatchObject({
ok: false,
error: {
code: 'model-unavailable',
message: 'provider "deepseek-official" model "private-preview" does not support reasoning effort "medium"',
},
})
const rejected = await remote.selectModel(request({
sessionId,
provider: 'missing',
model: 'model',
}))
expect(rejected).toEqual({
ok: false,
error: {
code: 'model-unavailable',
message: 'no adapter registered for provider "missing"',
details: { provider: 'missing', model: 'model' },
},
})
expect(await remote.selectModel(request({
sessionId,
provider: 'remote-rejected',
model: 'model',
}))).toEqual({
ok: false,
error: {
code: 'fixture-rejected',
message: 'fixture rejected the selection',
details: { provider: 'remote-rejected' },
},
})
expect(currentSelection(ctx, sessionId))
.toEqual({ provider: 'deepseek-official', model: 'private-preview', reasoningEffort: 'max' })
await ctx.fiber.dispose()
})
it('reads the Agent default live for a session whose log names no selection', async () => {
const { ctx, sessionId } = await harness()
let stored = { provider: 'deepseek-official', model: 'deepseek-chat' }
createSessionTestRemote(ctx, {
defaultModelSelection: () => stored,
cwd: '/tmp',
})
expect(currentSelection(ctx, sessionId))
.toEqual({ provider: 'deepseek-official', model: 'deepseek-chat' })
// The default moving after the session exists still reaches it: New
// Session reuses a blank session rather than minting another, so a seed
// captured at creation would show the superseded model there.
stored = { provider: 'deepseek-official', model: 'deepseek-reasoner' }
expect(currentSelection(ctx, sessionId))
.toEqual({ provider: 'deepseek-official', model: 'deepseek-reasoner' })
await ctx.fiber.dispose()
})
it('keeps a session on its logged selection when the Agent default differs', async () => {
const { ctx, sessionId } = await harness({
provider: 'deepseek-official',
model: 'deepseek-chat',
})
let stored = { provider: 'deepseek-official', model: 'deepseek-chat' }
createSessionTestRemote(ctx, {
defaultModelSelection: () => stored,
cwd: '/tmp',
})
stored = { provider: 'duplicate', model: 'same' }
expect(currentSelection(ctx, sessionId))
.toEqual({ provider: 'deepseek-official', model: 'deepseek-chat' })
await ctx.fiber.dispose()
})
it('does not reinterpret an adapter-owned reasoning default as an explicit Web selection', async () => {
const { ctx, agent } = await harness({
provider: 'deepseek-official',
model: 'deepseek-chat',
reasoningEffort: ReasoningEffortId('high'),
adapterDefaults: { reasoningEffort: true },
})
createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'duplicate', model: 'same' }),
cwd: '/tmp',
})
expect(new ApiSessionAgentController(ctx).selectionFor(agent).current)
.toEqual({ provider: 'deepseek-official', model: 'deepseek-chat' })
await ctx.fiber.dispose()
})
it('saves an accepted selection as the default and survives a storage failure', async () => {
const { ctx, sessionId } = await harness()
const saved: unknown[] = []
let reject = false
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }),
saveDefaultModelSelection: (selection) => {
saved.push(selection)
return reject ? Promise.reject(new Error('read-only document')) : Promise.resolve()
},
cwd: '/tmp',
})
expectValue(await remote.selectModel(request({
sessionId, provider: 'deepseek-official', model: 'deepseek-reasoner', reasoningEffort: 'max',
})))
expect(saved).toEqual([
{ provider: 'deepseek-official', model: 'deepseek-reasoner', reasoningEffort: 'max' },
])
// A refused selection never becomes anyone's default.
await remote.selectModel(request({ sessionId, provider: 'missing', model: 'model' }))
expect(saved).toHaveLength(1)
// Storage failing is not the selection failing: the switch already applies
// to this session, so the call still succeeds.
reject = true
const stillAccepted = expectValue(await remote.selectModel(request({
sessionId, provider: 'deepseek-official', model: 'deepseek-chat',
})))
expect(stillAccepted.selected).toEqual({ provider: 'deepseek-official', model: 'deepseek-chat', reasoningEffort: 'high' })
expect(currentSelection(ctx, sessionId))
.toEqual({ provider: 'deepseek-official', model: 'deepseek-chat', reasoningEffort: 'high' })
await ctx.fiber.dispose()
})
it('refuses a prompt no adapter can route, and reports it on the directory', async () => {
const { ctx, sessionId } = await harness()
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deleted-gateway', model: 'deleted-model' }),
cwd: '/tmp',
})
// The client disabling its input is an affordance; this method stays
// callable, so the refusal has to live here.
const refused = await remote.prompt(promptRequest({
sessionId, mode: 'queue' as const, content: [{ type: 'text' as const, text: 'hi' }],
}))
expect(refused).toMatchObject({
ok: false,
error: { code: 'model-unavailable', details: { provider: 'deleted-gateway', model: 'deleted-model' } },
})
const unavailableCatalog = await buildModelCatalog(ctx)
expect(unavailableCatalog.routableProviders.includes(currentSelection(ctx, sessionId).provider)).toBe(false)
// An advisory-unlisted model on a live route is NOT this: the route
// serves it, so the prompt goes through and nothing blocks.
expectValue(await remote.selectModel(request({
sessionId, provider: 'deepseek-official', model: 'unlisted-but-served',
})))
const catalog = await buildModelCatalog(ctx)
expect(catalog.routableProviders.includes(currentSelection(ctx, sessionId).provider)).toBe(true)
expect(catalog.groups.flatMap(group => group.models.map(model => model.id)))
.not.toContain('unlisted-but-served')
await ctx.fiber.dispose()
})
it('serves a session and its catalog when the stored default names a route that is gone', async () => {
const { ctx, sessionId } = await harness()
createSessionTestRemote(ctx, {
// What a Models-page removal leaves behind: the settings document still
// names the route the user last picked, and nothing serves it.
defaultModelSelection: () => ({ provider: 'deleted-gateway', model: 'deleted-model' }),
cwd: '/tmp',
})
const catalog = await buildModelCatalog(ctx)
// Passed through rather than repaired: matching no group is precisely what
// makes the composer seat prompt for a selection instead of naming a model
// the deployment cannot reach.
expect(currentSelection(ctx, sessionId)).toEqual({ provider: 'deleted-gateway', model: 'deleted-model' })
expect(catalog.groups.flatMap(group => group.models.map(model => `${group.id}/${model.id}`)))
.not.toContain('deleted-gateway/deleted-model')
await ctx.fiber.dispose()
})
it('maps image admission failures and accepts image-capable selections', async () => {
const { ctx, agent, sessionId } = await harness()
registerTextOnly(ctx)
ctx.llm.registerAdapter(['image-capable'], new class extends CatalogAdapter {
override resolveModel(provider: string, model: string): Promise {
return Promise.resolve({
provider, id: model, name: model, inputModalities: ['text', 'image'],
})
}
}('Image Capable', []))
ctx.llm.registerAdapter(['string-error'], new class extends CatalogAdapter {
override resolveModel(): Promise {
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- non-Error provider normalization is the scenario.
return Promise.reject('string selection failure')
}
}('String Error', []))
let saveMode: 'success' | 'error' | 'remote' = 'success'
const savedRef = {
attachmentId: 'saved-image', mediaType: 'image/png' as const, bytes: 1, width: 1, height: 1,
}
ctx.provide('attachments', {
saveImages: () => {
if (saveMode === 'error') return Promise.reject(new Error('image store offline'))
if (saveMode === 'remote') {
return Promise.reject(new TypertRemoteFailure({
code: 'fixture-rejected', message: 'fixture rejected', details: {},
}))
}
return Promise.resolve([savedRef])
},
} as never)
const followup = vi.fn()
Object.assign(agent, { followup })
const remote = createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }),
cwd: '/tmp',
})
const image = { type: 'image' as const, mediaType: 'image/png' as const, data: 'AQ==' }
expectValue(await remote.selectModel(request({
sessionId, provider: 'text-only', model: 'plain',
})))
expect(await remote.prompt(promptRequest({
sessionId, mode: 'queue', content: [image],
}))).toMatchObject({
ok: false,
error: { code: 'attachment-error', details: { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' } },
})
expectValue(await remote.selectModel(request({
sessionId, provider: 'image-capable', model: 'vision',
})))
expect(await remote.prompt(promptRequest({
sessionId, mode: 'queue', content: [{ ...image, data: '' }],
}))).toMatchObject({
ok: false,
error: { code: 'attachment-error', details: { reason: 'INVALID_IMAGE_BASE64' } },
})
saveMode = 'error'
expect(await remote.prompt(promptRequest({
sessionId, mode: 'queue', content: [image],
}))).toMatchObject({ ok: false, error: { code: 'agent-busy' } })
saveMode = 'remote'
expect(await remote.prompt(promptRequest({
sessionId, mode: 'queue', content: [image],
}))).toMatchObject({ ok: false, error: { code: 'fixture-rejected' } })
saveMode = 'success'
expectValue(await remote.prompt(promptRequest({ sessionId, mode: 'queue', content: [image] })))
expect(followup).toHaveBeenCalledOnce()
;(agent.inbox.nextTurn as UserMessage[]).push({
id: 'pending-image', role: 'user', source: { kind: 'user' },
content: [{ type: 'image', attachment: savedRef }],
} as never)
expectValue(await remote.selectModel(request({
sessionId, provider: 'deepseek-official', model: 'deepseek-chat',
})))
expectValue(await remote.selectModel(request({
sessionId, provider: 'image-capable', model: 'vision',
})))
expect(await remote.selectModel(request({
sessionId, provider: 'metadata-broken', model: 'broken',
}))).toMatchObject({
ok: false, error: { code: 'model-unavailable', message: 'reasoning metadata offline' },
})
expect(await remote.selectModel(request({
sessionId, provider: 'string-error', model: 'broken',
}))).toMatchObject({
ok: false,
error: { code: 'model-unavailable', message: 'string selection failure' },
})
await ctx.fiber.dispose()
})
})