A seat in the composer tool row, left of the model select, showing the preset THIS session runs — its own recorded one, not the deployment default, because a resumed session runs what it was created with. `SessionSummary` carries `agentPreset` for that, alongside `cwd` and `origin`. The switch exists only while the conversation has not started. After the first turn the seat becomes a plain label rather than a disabled menu: a greyed control reads as "temporarily unavailable", when in fact the choice is gone for good. The seat never asks in that state, and the host refuses independently with `agent-preset-locked`, so a stale client cannot slip one past it. A rejected switch restores the previous value and surfaces the host's message rather than leaving the seat showing something the session is not running.
221 lines
8.4 KiB
TypeScript
221 lines
8.4 KiB
TypeScript
/**
|
|
* The agent-preset settings controller: it derives both the options and the
|
|
* current default from one roster call, writes only the `default` field, and
|
|
* treats an empty roster as "this deployment composes no presets" rather than
|
|
* as a failure.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { IApiClient } from '@deepseek-ai/dsh-client-connection/client'
|
|
import {
|
|
AGENT_PRESET_SETTINGS_NS, AgentPresetSettingsController,
|
|
} from '../src/client/settings-store.ts'
|
|
import { AgentPresetSeatController } from '../src/client/seat-store.ts'
|
|
|
|
interface Recorded { ns: string; patch: unknown }
|
|
|
|
/** A client whose roster and write outcome the test controls. */
|
|
function fakeApi(
|
|
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
|
|
options: { writes?: Recorded[]; failWrite?: string; failList?: string } = {},
|
|
): IApiClient {
|
|
return {
|
|
agentPresets: {
|
|
list: () => Promise.resolve(options.failList === undefined
|
|
? { rpcId: 'r', result: { ok: true as const, value: { presets } } }
|
|
: { rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message: options.failList, details: {} } } }),
|
|
},
|
|
settings: {
|
|
update: (payload: { ns: string; patch: unknown }) => {
|
|
options.writes?.push({ ns: payload.ns, patch: payload.patch })
|
|
if (options.failWrite !== undefined) {
|
|
return Promise.resolve({ rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message: options.failWrite, details: {} } } })
|
|
}
|
|
// A committed write moves the roster's default, exactly as the host does.
|
|
for (const preset of presets) {
|
|
preset.isDefault = preset.id === (payload.patch as { default?: string }).default
|
|
}
|
|
return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: {} } })
|
|
},
|
|
},
|
|
} as unknown as IApiClient
|
|
}
|
|
|
|
describe('the agent-preset settings controller', () => {
|
|
it('derives options and the current default from one roster call', async () => {
|
|
const controller = new AgentPresetSettingsController(fakeApi([
|
|
{ id: 'standard', trust: 'system', isDefault: true },
|
|
{ id: 'mine', trust: 'user', isDefault: false },
|
|
]))
|
|
|
|
await controller.load()
|
|
|
|
const state = controller.store.getSnapshot()
|
|
expect(state.status).toBe('ready')
|
|
expect(state.currentValue).toBe('standard')
|
|
expect(state.options).toEqual([
|
|
{ id: 'standard', trust: 'system' },
|
|
{ id: 'mine', trust: 'user' },
|
|
])
|
|
})
|
|
|
|
it('reports an empty roster as unavailable, not as an error', async () => {
|
|
const controller = new AgentPresetSettingsController(fakeApi([]))
|
|
|
|
await controller.load()
|
|
|
|
// A deployment composing no presets is valid: every session shares the
|
|
// host composition and the row renders nothing.
|
|
expect(controller.store.getSnapshot().status).toBe('unavailable')
|
|
expect(controller.store.getSnapshot().error).toBeNull()
|
|
})
|
|
|
|
it('writes only the default field, into the agent-presets namespace', async () => {
|
|
const writes: Recorded[] = []
|
|
const controller = new AgentPresetSettingsController(fakeApi([
|
|
{ id: 'standard', trust: 'system', isDefault: true },
|
|
{ id: 'core-web', trust: 'system', isDefault: false },
|
|
], { writes }))
|
|
await controller.load()
|
|
|
|
await controller.select('core-web')
|
|
|
|
expect(writes).toEqual([{ ns: AGENT_PRESET_SETTINGS_NS, patch: { default: 'core-web' } }])
|
|
expect(controller.store.getSnapshot().currentValue).toBe('core-web')
|
|
})
|
|
|
|
it('restores the previous value and surfaces the message when the write fails', async () => {
|
|
const controller = new AgentPresetSettingsController(fakeApi([
|
|
{ id: 'standard', trust: 'system', isDefault: true },
|
|
{ id: 'core-web', trust: 'system', isDefault: false },
|
|
], { failWrite: 'read-only settings' }))
|
|
await controller.load()
|
|
|
|
await controller.select('core-web')
|
|
|
|
const state = controller.store.getSnapshot()
|
|
expect(state.currentValue).toBe('standard')
|
|
expect(state.error).toBe('read-only settings')
|
|
expect(state.status).toBe('ready')
|
|
})
|
|
|
|
it('ignores a pick that is already the default', async () => {
|
|
const writes: Recorded[] = []
|
|
const controller = new AgentPresetSettingsController(fakeApi([
|
|
{ id: 'standard', trust: 'system', isDefault: true },
|
|
], { writes }))
|
|
await controller.load()
|
|
|
|
await controller.select('standard')
|
|
|
|
expect(writes).toEqual([])
|
|
})
|
|
|
|
it('surfaces a roster failure without claiming the deployment has no presets', async () => {
|
|
const controller = new AgentPresetSettingsController(fakeApi([], { failList: 'host down' }))
|
|
|
|
await controller.load()
|
|
|
|
const state = controller.store.getSnapshot()
|
|
expect(state.status).toBe('error')
|
|
expect(state.error).toBe('host down')
|
|
})
|
|
})
|
|
|
|
describe('the composer seat controller', () => {
|
|
/** A seat over a fixed session summary. */
|
|
function seat(
|
|
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
|
|
summary: { blank: boolean; agentPreset?: string } | undefined,
|
|
options: { writes?: Recorded[]; failSelect?: string } = {},
|
|
): AgentPresetSeatController {
|
|
const api = {
|
|
agentPresets: {
|
|
list: () => Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { presets } } }),
|
|
select: (payload: { agentPreset: string }) => {
|
|
options.writes?.push({ ns: 'select', patch: payload.agentPreset })
|
|
return Promise.resolve(options.failSelect === undefined
|
|
? { rpcId: 'r', result: { ok: true as const, value: { agentPreset: payload.agentPreset } } }
|
|
: { rpcId: 'r', result: { ok: false as const, error: { code: 'agent-preset-locked', message: options.failSelect, details: {} } } })
|
|
},
|
|
},
|
|
} as unknown as IApiClient
|
|
return new AgentPresetSeatController(api, 's1' as never, () => summary)
|
|
}
|
|
|
|
const ROSTER: { id: string; trust: 'system' | 'user'; isDefault: boolean }[] = [
|
|
{ id: 'standard', trust: 'system', isDefault: true },
|
|
{ id: 'core-web', trust: 'system', isDefault: false },
|
|
]
|
|
|
|
it('shows what the session runs, not the deployment default', async () => {
|
|
const controller = seat(ROSTER, { blank: true, agentPreset: 'core-web' })
|
|
|
|
await controller.load()
|
|
|
|
// A resumed session runs what it was created with; showing `standard`
|
|
// because it is the current default would be a lie about this session.
|
|
expect(controller.store.getSnapshot().current).toBe('core-web')
|
|
expect(controller.store.getSnapshot().switchable).toBe(true)
|
|
})
|
|
|
|
it('falls back to the roster default when the session records none', async () => {
|
|
const controller = seat(ROSTER, { blank: true })
|
|
|
|
await controller.load()
|
|
|
|
expect(controller.store.getSnapshot().current).toBe('standard')
|
|
})
|
|
|
|
it('is not switchable once the conversation has started', async () => {
|
|
const controller = seat(ROSTER, { blank: false, agentPreset: 'standard' })
|
|
|
|
await controller.load()
|
|
|
|
expect(controller.store.getSnapshot().switchable).toBe(false)
|
|
})
|
|
|
|
it('refuses to switch a session that already started', async () => {
|
|
const writes: Recorded[] = []
|
|
const controller = seat(ROSTER, { blank: false, agentPreset: 'standard' }, { writes })
|
|
await controller.load()
|
|
|
|
await controller.select('core-web')
|
|
|
|
// The host enforces the same rule; the seat simply never asks.
|
|
expect(writes).toEqual([])
|
|
expect(controller.store.getSnapshot().current).toBe('standard')
|
|
})
|
|
|
|
it('switches a blank session and keeps the host\'s answer', async () => {
|
|
const writes: Recorded[] = []
|
|
const controller = seat(ROSTER, { blank: true, agentPreset: 'standard' }, { writes })
|
|
await controller.load()
|
|
|
|
await controller.select('core-web')
|
|
|
|
expect(writes).toEqual([{ ns: 'select', patch: 'core-web' }])
|
|
expect(controller.store.getSnapshot().current).toBe('core-web')
|
|
})
|
|
|
|
it('restores the previous value when the host rejects the switch', async () => {
|
|
const controller = seat(ROSTER, { blank: true, agentPreset: 'standard' }, { failSelect: 'already started' })
|
|
await controller.load()
|
|
|
|
await controller.select('core-web')
|
|
|
|
const state = controller.store.getSnapshot()
|
|
expect(state.current).toBe('standard')
|
|
expect(state.error).toBe('already started')
|
|
})
|
|
|
|
it('reports no options when the session is unknown to the list yet', async () => {
|
|
const controller = seat([], undefined)
|
|
|
|
await controller.load()
|
|
|
|
expect(controller.store.getSnapshot().options).toEqual([])
|
|
expect(controller.store.getSnapshot().switchable).toBe(false)
|
|
})
|
|
})
|