test(agent-presets): cover the Remote migration

This commit is contained in:
imccyu
2026-08-26 11:18:28 +08:00
parent ef1c812d93
commit c5be99838c
13 changed files with 838 additions and 522 deletions
@@ -237,19 +237,8 @@ export class FakeApiClient implements IApiClient {
readonly agentPresets: IApiClient['agentPresets'] = {
list: (payload: unknown) => this.record('agentPreset.list', payload, Promise.resolve(ok({ presets: [], authorable: false, hasDocument: false }))),
select: (payload: { agentPreset: string }) =>
this.record('agentPreset.select', payload, Promise.resolve(ok({ agentPreset: payload.agentPreset }))),
read: (payload: { agentPreset: string }) =>
this.record('agentPreset.read', payload, Promise.resolve(ok({
agentPreset: payload.agentPreset, trust: 'user' as const, content: '',
}))),
copy: (payload: { agentPreset: string }) =>
this.record('agentPreset.copy', payload, Promise.resolve(ok({ agentPreset: payload.agentPreset }))),
openDocument: (payload: { agentPreset: string }) =>
this.record('agentPreset.openDocument', payload, Promise.resolve(ok({ opened: true as const }))),
remove: (payload: { agentPreset: string }) =>
this.record('agentPreset.remove', payload, Promise.resolve(ok({}))),
}
readonly skills: IApiClient['skills'] = {
@@ -2273,6 +2273,82 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
return { ok: true, value: goalView(projection) }
}
/** Canonical fixture implementation of the generated AgentPresets Remote contract. */
const presetRemotes = {
// Both trusts appear, because a surface must present a locally authored
// preset differently from one the deployment vetted.
list(): RpcResult<{ presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[]; authorable: boolean }> {
return {
ok: true,
value: {
presets: [...fixturePresets].map(([id, preset]) => ({
id,
trust: preset.trust,
isDefault: id === fixtureDefaultPreset,
})),
authorable: true,
},
}
},
select(_id: SessionId, agentPreset: string): RpcResult<string> {
fixtureDefaultPreset = agentPreset
return { ok: true, value: agentPreset }
},
read(agentPreset: string): RpcResult<{ agentPreset: string; trust: 'system' | 'user'; content: string }> {
const preset = fixturePresets.get(agentPreset)
if (preset === undefined) {
return {
ok: false,
error: {
code: 'agent-preset-not-found',
message: `unknown agent preset "${agentPreset}"`,
details: { agentPreset, available: [...fixturePresets.keys()] },
},
}
}
return { ok: true, value: { agentPreset, trust: preset.trust, content: preset.content } }
},
copy(from: string, id: string): RpcResult<void> {
const source = fixturePresets.get(from)
if (source === undefined) {
return {
ok: false,
error: {
code: 'agent-preset-not-found',
message: `unknown agent preset "${from}"`,
details: { agentPreset: from, available: [...fixturePresets.keys()] },
},
}
}
if (fixturePresets.has(id)) {
return {
ok: false,
error: {
code: 'agent-preset-invalid',
message: `agent preset "${id}" already exists`,
details: { agentPreset: id, reason: 'already exists' },
},
}
}
fixturePresets.set(id, { trust: 'user', content: source.content })
return { ok: true, value: undefined }
},
deletePreset(id: string): RpcResult<void> {
if (fixturePresets.get(id)?.trust === 'system') {
return {
ok: false,
error: {
code: 'agent-preset-read-only',
message: `agent preset "${id}" ships with the deployment`,
details: { agentPreset: id, reason: 'it ships with the deployment' },
},
}
}
fixturePresets.delete(id)
return { ok: true, value: undefined }
},
}
/** At most one in-flight replay per session; cancel clears it. */
const replays = new Map<SessionId, { timer: ReturnType<typeof setTimeout>; finish(aborted: boolean): void }>()
@@ -3234,57 +3310,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
openPath: request => ok(request, { opened: true as const }),
},
agentPresets: {
// Both trusts appear, because a surface must present a locally authored
// preset differently from one the deployment vetted.
list: request => ok(request, {
presets: [...fixturePresets].map(([id, preset]) => ({
id,
trust: preset.trust,
isDefault: id === fixtureDefaultPreset,
})),
authorable: true,
hasDocument: true,
}),
select: (request) => {
fixtureDefaultPreset = request.payload.agentPreset
return ok(request, { agentPreset: request.payload.agentPreset })
},
read: (request) => {
const { agentPreset } = request.payload
const preset = fixturePresets.get(agentPreset)
if (preset === undefined) {
return err(request, {
code: 'agent-preset-not-found',
message: `unknown agent preset "${agentPreset}"`,
details: { agentPreset, available: [...fixturePresets.keys()] },
})
}
return ok(request, {
agentPreset,
trust: preset.trust,
content: preset.content,
})
},
copy: (request) => {
const { from, agentPreset } = request.payload
const source = fixturePresets.get(from)
if (source === undefined) {
return err(request, {
code: 'agent-preset-not-found',
message: `unknown agent preset "${from}"`,
details: { agentPreset: from, available: [...fixturePresets.keys()] },
})
}
if (fixturePresets.has(agentPreset)) {
return err(request, {
code: 'agent-preset-invalid',
message: `agent preset "${agentPreset}" already exists`,
details: { agentPreset, reason: 'already exists' },
})
}
fixturePresets.set(agentPreset, { trust: 'user', content: source.content })
return ok(request, { agentPreset })
},
// Native opens are deterministic no-op successes in this fixture, so the
// open-directory affordance renders and the path-text fallback stays a
// component-test concern.
@@ -3300,19 +3325,6 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
}
return ok(request, { opened: true as const })
},
remove: (request) => {
const { agentPreset } = request.payload
const existing = fixturePresets.get(agentPreset)
if (existing?.trust === 'system') {
return err(request, {
code: 'agent-preset-read-only',
message: `agent preset "${agentPreset}" ships with the deployment`,
details: { agentPreset, reason: 'it ships with the deployment' },
})
}
fixturePresets.delete(agentPreset)
return ok(request, {})
},
},
skills: {
@@ -3422,6 +3434,9 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
query?: string
images?: readonly unknown[]
ref?: { id: string; revision: number }
agentPreset?: string
from?: string
id?: string
request?: unknown
_request?: unknown
}>
@@ -3449,6 +3464,11 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
case 'goals/resume': return Promise.resolve(goalRemotes.resume(sessionId, args.ref as FxGoalRef))
case 'goals/complete': return Promise.resolve(goalRemotes.complete(sessionId, args.ref as FxGoalRef))
case 'goals/clear': return Promise.resolve(goalRemotes.clear(sessionId, args.ref as FxGoalRef))
case 'agentPresets/list': return Promise.resolve(presetRemotes.list())
case 'agentPresets/select': return Promise.resolve(presetRemotes.select(sessionId, args.agentPreset as string))
case 'agentPresets/read': return Promise.resolve(presetRemotes.read(args.agentPreset as string))
case 'agentPresets/copy': return Promise.resolve(presetRemotes.copy(args.from as string, args.id as string))
case 'agentPresets/deletePreset': return Promise.resolve(presetRemotes.deletePreset(args.id as string))
case 'session/list': return sessionApi.list(
args._request as Parameters<FixtureSessionApi['list']>[0],
)
@@ -3580,12 +3600,7 @@ export class FixtureApiClient extends AbstractApiClient {
case 'host.createDirectory': return this.api.host.createDirectory(request)
case 'host.openPath': return this.api.host.openPath(request, new AbortController().signal)
case 'skill.list': return this.api.skills.list(request)
case 'agentPreset.list': return this.api.agentPresets.list(request)
case 'agentPreset.select': return this.api.agentPresets.select(request)
case 'agentPreset.read': return this.api.agentPresets.read(request)
case 'agentPreset.copy': return this.api.agentPresets.copy(request)
case 'agentPreset.openDocument': return this.api.agentPresets.openDocument(request, new AbortController().signal)
case 'agentPreset.remove': return this.api.agentPresets.remove(request)
case 'settings.describe': return this.api.settings.describe(request)
case 'settings.openDocument': return this.api.settings.openDocument(request, signal)
case 'settings.update': return this.api.settings.update(request)
@@ -97,19 +97,8 @@ export class FakeApiClient implements IApiClient {
readonly agentPresets: IApiClient['agentPresets'] = {
list: (payload: unknown) => this.record('agentPreset.list', payload, Promise.resolve(ok({ presets: [], authorable: false, hasDocument: false }))),
select: (payload: { agentPreset: string }) =>
this.record('agentPreset.select', payload, Promise.resolve(ok({ agentPreset: payload.agentPreset }))),
read: (payload: { agentPreset: string }) =>
this.record('agentPreset.read', payload, Promise.resolve(ok({
agentPreset: payload.agentPreset, trust: 'user' as const, content: '',
}))),
copy: (payload: { agentPreset: string }) =>
this.record('agentPreset.copy', payload, Promise.resolve(ok({ agentPreset: payload.agentPreset }))),
openDocument: (payload: { agentPreset: string }) =>
this.record('agentPreset.openDocument', payload, Promise.resolve(ok({ opened: true as const }))),
remove: (payload: { agentPreset: string }) =>
this.record('agentPreset.remove', payload, Promise.resolve(ok({}))),
}
readonly skills: IApiClient['skills'] = {
@@ -176,7 +176,7 @@ describe('connection node half', () => {
const methods = [
'host.pickDirectory', 'host.openPath',
'settings.describe', 'settings.update', 'credentials.describe', 'credentials.set',
'llm.discoverModels', 'llm.models', 'agentPreset.read', 'agentPreset.list',
'llm.discoverModels', 'llm.models', 'agentPreset.openDocument',
]
for (const method of methods) {
const denied = fakeResponse()
@@ -505,8 +505,8 @@ describe('connection node half over a real HTTP server', () => {
'credentials.describe', 'credentials.set', 'credentials.unset',
'host.pickDirectory', 'host.openPath',
'llm.discoverModels',
'agentPreset.read', 'agentPreset.copy', 'agentPreset.openDocument', 'agentPreset.remove',
'llm.providers', 'llm.models', 'agentPreset.list', 'agentPreset.select',
'agentPreset.openDocument',
'llm.providers', 'llm.models',
]
for (const method of methods) {
expect([method, await call(port, method, 'localhost')]).toEqual([method, 401])
@@ -29,46 +29,34 @@ import { AgentPresetSeatController } from '../src/client/seat-store.ts'
// FALLBACK_LOCALE (en); each bench stages zh explicitly on the locale instead.
const ROSTER_ONE = {
rpcId: 'r',
result: {
ok: true as const,
value: {
presets: [{ id: 'standard', trust: 'system', isDefault: true }],
authorable: true,
hasDocument: true,
},
ok: true as const,
value: {
presets: [{ id: 'standard', trust: 'system', isDefault: true }],
authorable: true,
},
}
/** The roster after this browser copied one preset of its own. */
const ROSTER_AUTHORED = {
rpcId: 'r',
result: {
ok: true as const,
value: {
presets: [
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'mine', trust: 'user', isDefault: false },
],
authorable: true,
hasDocument: true,
},
ok: true as const,
value: {
presets: [
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'mine', trust: 'user', isDefault: false },
],
authorable: true,
},
}
/** The same roster with a second preset carrying the default. */
const ROSTER_MOVED = {
rpcId: 'r',
result: {
ok: true as const,
value: {
presets: [
{ id: 'standard', trust: 'system', isDefault: false },
{ id: 'minimal', trust: 'system', isDefault: true },
],
authorable: true,
hasDocument: true,
},
ok: true as const,
value: {
presets: [
{ id: 'standard', trust: 'system', isDefault: false },
{ id: 'minimal', trust: 'system', isDefault: true },
],
authorable: true,
},
}
@@ -84,30 +72,45 @@ async function bench() {
ctx.provide('locale', locale)
const remote = new TestRemote(ctx)
const calls: string[] = []
// The roster and the switch are the AgentPresets Remote namespace; the
// shared double carries no generated namespaces, so this spec stages its
// own. Registered twice on purpose: the nested key satisfies the plugin's
// `inject`, and the property is what `ctx.remote.agentPresets` reads,
// because the double is a plain provided object rather than a Service.
const agentPresets = {
list: () => { calls.push('list'); return Promise.resolve(ROSTER) },
read: () => Promise.resolve({
ok: true as const,
value: { agentPreset: 'standard', trust: 'system', content: '' },
}),
copy: (_from: string, id: string) => {
calls.push(`copy:${id}`)
// The host's roster now contains it, which is the whole point of the
// copy and what every surface must converge on.
ROSTER = ROSTER_AUTHORED
return Promise.resolve({ ok: true as const, value: undefined })
},
deletePreset: () => Promise.resolve({ ok: true as const, value: undefined }),
select: (_agentId: SessionId, agentPreset: string) => {
calls.push(`select:${agentPreset}`)
return Promise.resolve({ ok: true as const, value: agentPreset })
},
}
ctx.provide('remote.agentPresets', agentPresets as never)
Object.assign(remote, { agentPresets })
ctx.provide('connection', {
api: {
agentPresets: {
list: () => { calls.push('list'); return Promise.resolve(ROSTER) },
read: () => Promise.resolve({
host: {
describe: () => Promise.resolve({
rpcId: 'r',
result: { ok: true as const, value: { agentPreset: 'standard', trust: 'system', content: '' } },
result: { ok: true as const, value: { canOpenPath: true } },
}),
copy: (payload: { from: string; agentPreset: string }) => {
calls.push(`copy:${payload.agentPreset}`)
// The host's roster now contains it, which is the whole point of the
// copy and what every surface must converge on.
ROSTER = ROSTER_AUTHORED
return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { agentPreset: payload.agentPreset } } })
},
},
agentPresets: {
openDocument: (payload: { agentPreset: string }) => {
calls.push(`openDocument:${payload.agentPreset}`)
return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { opened: true as const } } })
},
remove: () => Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: {} } }),
select: (payload: { agentPreset: string }) => {
calls.push(`select:${payload.agentPreset}`)
return Promise.resolve({ rpcId: 'r', result: { ok: true as const, value: { agentPreset: payload.agentPreset } } })
},
},
settings: {
// The row reads this to learn whether this browser may write at all.
@@ -179,7 +182,7 @@ function sessionsDouble(state: {
describe('ui-agent-preset apply', () => {
it('declares the services it uses', () => {
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope'])
expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'remote.agentPresets', 'settingsScope'])
})
it('registers the General row and the settings section', async () => {
@@ -600,8 +603,7 @@ describe('AgentPresetSeatController reconciliation', () => {
{
name: 'RPC rejection',
select: () => Promise.resolve({
rpcId: 'r',
result: { ok: false as const, error: { code: 'failed', message: 'selection rejected', details: {} } },
ok: false as const, error: { code: 'failed', message: 'selection rejected', details: {} },
}),
message: 'selection rejected',
},
@@ -7,7 +7,7 @@
*/
import { describe, expect, it } from 'vitest'
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
import { AgentPresetSectionController, draftBlocker } from '../src/client/section-store.ts'
import type { CopyDraft, PresetRow } from '../src/client/section-store.ts'
@@ -41,6 +41,8 @@ interface FakeOptions {
authorable?: boolean
/** Whether the host can open a preset directory on a desktop. */
hasDocument?: boolean
/** Reject `host.describe`, as a dead transport does. */
throwDescribe?: boolean
/** Hold `remove` until this resolves, to observe the in-flight state. */
holdRemove?: Promise<void>
}
@@ -49,63 +51,29 @@ const ok = (value: unknown) => Promise.resolve({ rpcId: 'r', result: { ok: true
const fail = (message: string) =>
Promise.resolve({ rpcId: 'r', result: { ok: false as const, error: { code: 'internal', message, details: {} } } })
const remoteOk = (value: unknown) => Promise.resolve({ ok: true as const, value })
const remoteFail = (message: string) =>
Promise.resolve({ ok: false as const, error: { code: 'internal', message, details: {} } })
/**
* A wire face over an in-memory preset store: copies land, so the roster the
* controller re-reads after a copy is the one the copy produced.
* @param presets - the starting compositions by id.
* The carried wire face: the desktop opener, the default write, and the opener
* capability the page joins onto the roster.
* @param defaultId - the preset a session with no choice gets.
* @param options - failure injection and call recording.
* @returns the fake client.
*/
function fakeApi(
presets: Map<string, FakePreset>,
defaultId: { id: string },
options: FakeOptions = {},
): Pick<IApiClient, 'agentPresets' | 'settings'> {
): Pick<IApiClient, 'agentPresets' | 'settings' | 'host'> {
const record = (method: string, payload: unknown): void => { options.calls?.push({ method, payload }) }
return {
host: {
describe: () => (options.throwDescribe === true
? Promise.reject(new Error('socket closed'))
: ok({ canOpenPath: options.hasDocument ?? true })),
},
agentPresets: {
list: () => {
record('list', {})
if (options.throwList === true) return Promise.reject(new Error('socket closed'))
if (options.failList !== undefined) return fail(options.failList)
return ok({
presets: [...presets].map(([id, preset]) => ({
id, trust: preset.trust, isDefault: id === defaultId.id,
...preset.name === undefined ? {} : { name: preset.name },
})),
authorable: options.authorable ?? true,
hasDocument: options.hasDocument ?? true,
})
},
read: (payload: { agentPreset: string }) => {
record('read', payload)
if (options.throwRead === true) return Promise.reject(new Error('socket closed'))
if (options.failRead !== undefined) return fail(options.failRead)
const preset = presets.get(payload.agentPreset)
/* v8 ignore next -- every test reads an id the fake store holds */
if (preset === undefined) return fail(`unknown preset ${payload.agentPreset}`)
return ok({
agentPreset: payload.agentPreset,
trust: preset.trust,
content: preset.content,
...preset.name === undefined ? {} : { name: preset.name },
})
},
copy: (payload: { from: string; agentPreset: string; name?: string }) => {
record('copy', payload)
if (options.throwCopy === true) return Promise.reject(new Error('socket closed'))
if (options.failCopy !== undefined) return fail(options.failCopy)
const source = presets.get(payload.from)
/* v8 ignore next -- every test copies a source the fake store holds */
if (source === undefined) return fail(`unknown preset ${payload.from}`)
presets.set(payload.agentPreset, {
trust: 'user',
content: source.content,
...payload.name === undefined ? {} : { name: payload.name },
})
return ok({ agentPreset: payload.agentPreset })
},
openDocument: (payload: { agentPreset: string }) => {
record('openDocument', payload)
if (options.throwOpen === true) return Promise.reject(new Error('socket closed'))
@@ -114,13 +82,6 @@ function fakeApi(
? ok({ opened: true })
: ok({ opened: false, path: `/presets/${payload.agentPreset}` })
},
remove: async (payload: { agentPreset: string }) => {
record('remove', payload)
await options.holdRemove
if (options.failRemove !== undefined) return await fail(options.failRemove)
presets.delete(payload.agentPreset)
return await ok({})
},
},
settings: {
update: (payload: { ns: string; patch: { default?: string } }) => {
@@ -131,7 +92,81 @@ function fakeApi(
return ok({})
},
},
} as unknown as Pick<IApiClient, 'agentPresets' | 'settings'>
} as unknown as Pick<IApiClient, 'agentPresets' | 'settings' | 'host'>
}
/**
* The Remote namespace over an in-memory preset store: copies land, so the
* roster the controller re-reads after a copy is the one the copy produced.
* @param presets - the starting compositions by id.
* @param defaultId - the preset a session with no choice gets.
* @param options - failure injection and call recording.
* @returns the fake Remote namespace.
*/
function fakeRemote(
presets: Map<string, FakePreset>,
defaultId: { id: string },
options: FakeOptions = {},
): Pick<ClientRemote, 'agentPresets'> {
const record = (method: string, payload: unknown): void => { options.calls?.push({ method, payload }) }
return {
agentPresets: {
list: () => {
record('list', {})
if (options.throwList === true) return Promise.reject(new Error('socket closed'))
if (options.failList !== undefined) return remoteFail(options.failList)
return remoteOk({
presets: [...presets].map(([id, preset]) => ({
id, trust: preset.trust, isDefault: id === defaultId.id,
...preset.name === undefined ? {} : { name: preset.name },
})),
authorable: options.authorable ?? true,
})
},
read: (agentPreset: string) => {
record('read', { agentPreset })
if (options.throwRead === true) return Promise.reject(new Error('socket closed'))
if (options.failRead !== undefined) return remoteFail(options.failRead)
const preset = presets.get(agentPreset)
/* v8 ignore next -- every test reads an id the fake store holds */
if (preset === undefined) return remoteFail(`unknown preset ${agentPreset}`)
return remoteOk({
agentPreset,
trust: preset.trust,
content: preset.content,
...preset.name === undefined ? {} : { name: preset.name },
})
},
// Arity is checked against the declaration, not against which arguments
// carry a value, so a short call rejects instead of answering. Reject
// one here too: the real face would, and a lenient double hid it once.
copy: (...args: [from: string, id: string, name?: string]) => {
if (args.length !== 3) {
return Promise.reject(new Error(`client api: agentPresets/copy expected 3 argument(s), got ${String(args.length)}`))
}
const [from, id, name] = args
record('copy', { from, id, ...name === undefined ? {} : { name } })
if (options.throwCopy === true) return Promise.reject(new Error('socket closed'))
if (options.failCopy !== undefined) return remoteFail(options.failCopy)
const source = presets.get(from)
/* v8 ignore next -- every test copies a source the fake store holds */
if (source === undefined) return remoteFail(`unknown preset ${from}`)
presets.set(id, {
trust: 'user',
content: source.content,
...name === undefined ? {} : { name },
})
return remoteOk(undefined)
},
deletePreset: async (id: string) => {
record('deletePreset', { id })
await options.holdRemove
if (options.failRemove !== undefined) return await remoteFail(options.failRemove)
presets.delete(id)
return await remoteOk(undefined)
},
},
} as unknown as Pick<ClientRemote, 'agentPresets'>
}
function seed(): Map<string, FakePreset> {
@@ -146,8 +181,10 @@ function harness(options: FakeOptions = {}) {
const defaultId = { id: 'standard' }
const calls: Recorded[] = []
let rosterChanges = 0
const wired = { ...options, calls: options.calls ?? calls }
const controller = new AgentPresetSectionController(
fakeApi(presets, defaultId, { ...options, calls: options.calls ?? calls }),
fakeApi(defaultId, wired),
fakeRemote(presets, defaultId, wired),
() => { rosterChanges += 1 },
)
return { controller, presets, defaultId, calls, rosterChanges: () => rosterChanges }
@@ -160,6 +197,19 @@ function copyOf(controller: AgentPresetSectionController): CopyDraft {
}
describe('loading the roster', () => {
it('still lists the roster when the opener capability cannot be read', async () => {
const { controller } = harness({ throwDescribe: true })
await controller.load()
// The two reads are independent: a refused `host.describe` costs the
// open-directory affordance, not the page.
const state = controller.store.getSnapshot()
expect(state.status).toBe('ready')
expect(state.hasDocument).toBe(false)
expect(state.rows.map((row: PresetRow) => row.id)).toEqual(['standard', 'mine'])
})
it('maps the roster onto rows with the capability flags', async () => {
const { controller } = harness({ authorable: true, hasDocument: false })
@@ -352,7 +402,7 @@ describe('submitting a copy', () => {
expect(state.rows.map(row => row.id)).toContain('my-copy')
expect(rosterChanges()).toBe(1)
expect(calls.find(call => call.method === 'copy')?.payload)
.toEqual({ from: 'standard', agentPreset: 'my-copy', name: '我的模式' })
.toEqual({ from: 'standard', id: 'my-copy', name: '我的模式' })
// A preset is its files from here on, so landing in them completes the
// copy rather than following it.
expect(calls.find(call => call.method === 'openDocument')?.payload)
@@ -369,7 +419,7 @@ describe('submitting a copy', () => {
await controller.confirmCopy()
expect(calls.find(call => call.method === 'copy')?.payload)
.toEqual({ from: 'standard', agentPreset: 'my-copy' })
.toEqual({ from: 'standard', id: 'my-copy' })
})
it('reveals the new directory as text where the host has no desktop', async () => {
@@ -492,7 +542,7 @@ describe('deleting', () => {
await controller.remove()
expect(controller.store.getSnapshot().rows.map(row => row.id)).toContain('mine')
expect(calls.some(call => call.method === 'remove')).toBe(false)
expect(calls.some(call => call.method === 'deletePreset')).toBe(false)
})
it('ignores a second confirmation while one delete is in flight', async () => {
@@ -508,7 +558,7 @@ describe('deleting', () => {
release()
await removal
expect(calls.filter(call => call.method === 'remove')).toHaveLength(1)
expect(calls.filter(call => call.method === 'deletePreset')).toHaveLength(1)
})
it('surfaces a refusal and clears the confirmation', async () => {
@@ -528,13 +578,15 @@ describe('deleting', () => {
const { controller, presets } = harness()
await controller.load()
presets.clear()
const broken = new AgentPresetSectionController({
agentPresets: {
list: () => Promise.reject(new Error('gone')),
remove: () => Promise.reject(new Error('socket closed')),
},
settings: {},
} as unknown as Pick<IApiClient, 'agentPresets' | 'settings'>)
const broken = new AgentPresetSectionController(
{ agentPresets: {}, settings: {}, host: {} } as unknown as Pick<IApiClient, 'agentPresets' | 'settings' | 'host'>,
{
agentPresets: {
list: () => Promise.reject(new Error('gone')),
deletePreset: () => Promise.reject(new Error('socket closed')),
},
} as unknown as Pick<ClientRemote, 'agentPresets'>,
)
broken.confirmDelete('mine')
await broken.remove()
@@ -548,7 +600,9 @@ describe('a controller with no roster listener', () => {
// The rosterChanged callback is optional wiring, not a requirement: a
// page composed without sibling surfaces still deletes cleanly.
const presets = seed()
const alone = new AgentPresetSectionController(fakeApi(presets, { id: 'standard' }))
const defaultId = { id: 'standard' }
const alone = new AgentPresetSectionController(
fakeApi(defaultId), fakeRemote(presets, defaultId))
await alone.load()
alone.confirmDelete('mine')
@@ -6,7 +6,7 @@
*/
import { describe, expect, it } from 'vitest'
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
import type { ClientRemote, IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
import type { SessionSummary } from '@deepseek-ai/dsh-api-session-controller/client'
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-mirror.ts'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
@@ -14,9 +14,15 @@ import {
AGENT_PRESET_SETTINGS_NS, AgentPresetSettingsController, messageOf,
} from '../src/client/settings-store.ts'
/** The two faces the row reads: the roster Remote and the settings wire. */
interface FakeWire {
api: IApiClient
remote: Pick<ClientRemote, 'agentPresets'>
}
/** Controller over a real mirror derived from the same fake wire. */
function derivedController(api: IApiClient) {
return new AgentPresetSettingsController(api, new SettingsDescribeMirror(api))
function derivedController(wire: FakeWire) {
return new AgentPresetSettingsController(wire.api, wire.remote, new SettingsDescribeMirror(wire.api))
}
import { AgentPresetSeatController } from '../src/client/seat-store.ts'
@@ -24,7 +30,27 @@ type SeatSession = Pick<SessionSummary, 'id' | 'blank' | 'projectionValues'>
interface Recorded { ns: string; patch: unknown }
/** A client whose roster and write outcome the test controls. */
/** A roster Remote answering a fixed set of rows, or refusing. */
function fakeRoster(
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
options: { failList?: string; failListCode?: string; throwOnList?: boolean } = {},
): Pick<ClientRemote, 'agentPresets'> {
return {
agentPresets: {
list: () => {
if (options.throwOnList === true) return Promise.reject(new Error('socket closed'))
return Promise.resolve(options.failList === undefined
? { ok: true as const, value: { presets, authorable: true } }
: {
ok: false as const,
error: { code: options.failListCode ?? 'internal', message: options.failList, details: {} },
})
},
},
} as unknown as Pick<ClientRemote, 'agentPresets'>
}
/** A wire whose roster and write outcome the test controls. */
function fakeApi(
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
options: {
@@ -34,13 +60,8 @@ function fakeApi(
failWriteWith?: Error
readOnly?: boolean
} = {},
): 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: {} } } }),
},
): FakeWire {
const api = {
settings: {
// Host persistence is enabled in production only on the selected client path; a read-only provider answers writable:false
// and the row disables its control instead of offering a refused write.
@@ -65,6 +86,10 @@ function fakeApi(
},
},
} as unknown as IApiClient
return {
api,
remote: fakeRoster(presets, options.failList === undefined ? {} : { failList: options.failList }),
}
}
describe('the agent-preset settings controller', () => {
@@ -138,6 +163,20 @@ describe('the agent-preset settings controller', () => {
expect(controller.store.getSnapshot().error).toBeNull()
})
it('treats an unavailable optional namespace as an empty roster', async () => {
const controller = derivedController({
api: {} as IApiClient,
remote: fakeRoster([], {
failList: 'no active Remote method exports this endpoint',
failListCode: 'invocation-unavailable',
}),
})
await controller.load()
expect(controller.store.getSnapshot()).toMatchObject({ status: 'unavailable', error: null, options: [] })
})
it('writes only the default field, into the agent-presets namespace', async () => {
const writes: Recorded[] = []
const controller = derivedController(fakeApi([
@@ -221,8 +260,9 @@ describe('the agent-preset settings controller', () => {
it('reports a transport that rejects rather than answering', async () => {
const controller = derivedController({
agentPresets: { list: () => Promise.reject(new Error('socket closed')) },
} as unknown as IApiClient)
api: {} as IApiClient,
remote: fakeRoster([], { throwOnList: true }),
})
await controller.load()
@@ -249,27 +289,43 @@ describe('the new-session chip controller', () => {
function chip(
presets: { id: string; trust: 'system' | 'user'; isDefault: boolean }[],
current: SeatSession | undefined | (() => SeatSession | undefined),
options: { writes?: Recorded[]; failSelect?: string; failList?: string; throwOn?: 'list' | 'select' } = {},
options: {
writes?: Recorded[]
failSelect?: string
failList?: string
failListCode?: string
throwOn?: 'list' | 'select'
} = {},
): AgentPresetSeatController {
const api = {
const remote = {
agentPresets: {
list: () => {
if (options.throwOn === 'list') return Promise.reject(new Error('socket closed'))
return 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: {} } } })
? { ok: true as const, value: { presets, authorable: true } }
: {
ok: false as const,
error: { code: options.failListCode ?? 'internal', message: options.failList, details: {} },
})
},
select: (payload: { agentPreset: string }) => {
select: (agentId: SessionId, agentPreset: string) => {
if (options.throwOn === 'select') return Promise.reject(new Error('socket closed'))
options.writes?.push({ ns: 'select', patch: payload.agentPreset })
options.writes?.push({ ns: 'select', patch: 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: {} } } })
? { ok: true as const, value: agentPreset }
: {
ok: false as const,
error: {
code: 'agent-preset-locked',
message: options.failSelect,
details: { sessionId: agentId, agentPreset },
},
})
},
},
} as unknown as IApiClient
} as unknown as Pick<ClientRemote, 'agentPresets'>
return new AgentPresetSeatController(
api,
remote,
typeof current === 'function' ? current : () => current,
)
}
@@ -325,6 +381,17 @@ describe('the new-session chip controller', () => {
expect(controller.store.getSnapshot().current).toBe('')
})
it('opens on nothing when the optional namespace is unavailable', async () => {
const controller = chip([], undefined, {
failList: 'no active Remote method exports this endpoint',
failListCode: 'invocation-unavailable',
})
await controller.load()
expect(controller.store.getSnapshot()).toMatchObject({ current: '', error: null, options: [] })
})
it('stages a pick made before any session exists', async () => {
const writes: Recorded[] = []
const controller = chip(ROSTER, undefined, { writes })
@@ -500,18 +567,12 @@ describe('the new-session chip controller', () => {
})
it('degrades to a read-only row while the mirror holds no answer', async () => {
const api = {
agentPresets: {
list: () => Promise.resolve({
rpcId: 'r',
result: { ok: true as const, value: { presets: [{ id: 'standard', trust: 'system', isDefault: true }], authorable: true } },
}),
},
const controller = derivedController({
// The roster answered; the mirror's read is what failed, so the row
// shows the current default without offering a write it never confirmed.
settings: { describe: () => Promise.reject(new Error('socket closed')) },
} as unknown as IApiClient
const controller = derivedController(api)
api: { settings: { describe: () => Promise.reject(new Error('socket closed')) } } as unknown as IApiClient,
remote: fakeRoster([{ id: 'standard', trust: 'system', isDefault: true }]),
})
await controller.load()
@@ -244,205 +244,6 @@ describe('a capability the session\'s preset mounts', () => {
})
})
describe('agentPreset.list', () => {
it('marks the default and carries each preset\'s trust', async () => {
const { api } = await harness(['standard', 'minimal'])
const response = await api.agentPresets.list(request({}))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.presets).toEqual([
{ id: 'standard', trust: 'system', isDefault: true },
{ id: 'minimal', trust: 'system', isDefault: false },
])
expect(response.result.value.authorable).toBe(true)
})
it('answers with an empty roster when the deployment composes no presets', async () => {
const { api } = await harness()
const response = await api.agentPresets.list(request({}))
// Composing no presets is a valid deployment, not an error: every session
// then shares the host composition and the browser offers no choice.
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.presets).toEqual([])
// Nothing to write to either, so a surface offering "new preset" knows to
// stay hidden rather than offering a button whose save always fails.
expect(response.result.value.authorable).toBe(false)
})
})
describe('agentPreset.select', () => {
it('recomposes a blank session', async () => {
const { api } = await harness(['standard', 'minimal'])
await createSession(api, { sessionId: SessionId('sel-1'), agentPreset: 'standard' })
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-1'), agentPreset: 'minimal' }))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.agentPreset).toBe('minimal')
})
it('records the switch in the log', async () => {
const { api, ctx } = await harness(['standard', 'minimal'])
await createSession(api, { sessionId: SessionId('sel-log'), agentPreset: 'standard' })
await api.agentPresets.select(
request({ sessionId: SessionId('sel-log'), agentPreset: 'minimal' }))
// The header is written once at creation, so the switch lives in the log —
// this is what a restart replays and what every projection resolves from.
// Asserting only the RPC's echo would miss a switch that never persisted.
const session = ctx.sessions.get(SessionId('sel-log'))
if (session === undefined) throw new Error('unreachable')
expect(session.header.agentPreset).toBe('standard')
expect(session.events.findLast(event => event.type === 'agent-preset/selected')?.data)
.toEqual({ agentPreset: 'minimal' })
})
it('serializes two concurrent selects on one session', async () => {
const { api, ctx } = await harness(['standard', 'minimal'])
await createSession(api, { sessionId: SessionId('sel-race'), agentPreset: 'standard' })
// Both pass the blank check; unserialized, the second unmount finds no
// record because the first already removed it, and two compositions end up
// in one agent layer. The client's busy flag is not enforcement.
const [first, second] = await Promise.all([
api.agentPresets.select(request({ sessionId: SessionId('sel-race'), agentPreset: 'minimal' })),
api.agentPresets.select(request({ sessionId: SessionId('sel-race'), agentPreset: 'standard' })),
])
expect(first.result.ok).toBe(true)
expect(second.result.ok).toBe(true)
const session = ctx.sessions.get(SessionId('sel-race'))
if (session === undefined) throw new Error('unreachable')
// One winner, and the log agrees with it: the last committed switch.
expect(session.events.findLast(event => event.type === 'agent-preset/selected')?.data)
.toEqual({ agentPreset: 'standard' })
})
it('refuses once the conversation has started', async () => {
const { api, ctx } = await harness(['standard', 'minimal'])
await createSession(api, { sessionId: SessionId('sel-2'), agentPreset: 'standard' })
// One turn is enough: the history from here on was produced under
// `standard`'s tools, and a swap would strand those tool calls.
ctx.sessions.get(SessionId('sel-2'))?.append('turn/start', { turn: 0 })
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-2'), agentPreset: 'minimal' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-locked')
})
it('reports an unknown preset without disturbing the session', async () => {
const { api } = await harness(['standard'])
await createSession(api, { sessionId: SessionId('sel-3') })
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-3'), agentPreset: 'nope' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
it('reports a deployment that composes no presets', async () => {
const { api } = await harness()
await createSession(api, { sessionId: SessionId('sel-4') })
const response = await api.agentPresets.select(
request({ sessionId: SessionId('sel-4'), agentPreset: 'anything' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
})
describe('authoring over the wire', () => {
it('reads a composition with its trust', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.read(request({ agentPreset: 'standard' }))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
// The shipped set is readable: it is the known-good composition a copy
// starts from, and trust is what tells a surface to say so.
expect(response.result.value.trust).toBe('system')
expect(response.result.value.content).toContain('- id: x')
})
it('copies a preset under a new id', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.copy(
request({ from: 'standard', agentPreset: 'mine', name: '我的模式' }))
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
expect(response.result.value.agentPreset).toBe('mine')
})
it('rejects a copy target that could escape the preset root', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.copy(request({ from: 'standard', agentPreset: '../escape' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-invalid')
})
it('rejects a copy target the roster already supplies', async () => {
const { api } = await harness(['standard', 'minimal'])
const response = await api.agentPresets.copy(request({ from: 'standard', agentPreset: 'minimal' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-invalid')
expect(response.result.error.message).toMatch(/already exists/)
})
it('rejects a copy whose source is unknown', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.copy(request({ from: 'never-existed', agentPreset: 'mine' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
it('reports a deployment that composes no presets', async () => {
const { api } = await harness()
const response = await api.agentPresets.read(request({ agentPreset: 'anything' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
it('reports an unknown id on delete rather than succeeding silently', async () => {
const { api } = await harness(['standard'])
const response = await api.agentPresets.remove(request({ agentPreset: 'never-existed' }))
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
expect(response.result.error.code).toBe('agent-preset-not-found')
})
})
describe('opening a preset directory', () => {
it('hands the resolved directory to the native opener', async () => {
const opened: string[] = []
@@ -492,7 +293,7 @@ describe('opening a preset directory', () => {
expect(opened).toEqual([])
})
it('reports the roster capability on list', async () => {
it('reports the opener capability on host.describe', async () => {
const openable = await harness(['standard'], {
defaults: { canOpenPath: () => true },
})
@@ -500,11 +301,13 @@ describe('opening a preset directory', () => {
defaults: { canOpenPath: () => false },
})
const yes = await openable.api.agentPresets.list(request({}))
const no = await headless.api.agentPresets.list(request({}))
// The capability a surface joins onto the roster to decide between opening
// a preset directory and showing its path as text.
const yes = await openable.api.host.describe(request({}))
const no = await headless.api.host.describe(request({}))
expect(yes.result.ok && yes.result.value.hasDocument).toBe(true)
expect(no.result.ok && no.result.value.hasDocument).toBe(false)
expect(yes.result.ok && yes.result.value.canOpenPath).toBe(true)
expect(no.result.ok && no.result.value.canOpenPath).toBe(false)
})
it('counts an injected opener as openable', async () => {
@@ -512,9 +315,9 @@ describe('opening a preset directory', () => {
defaults: { openPath: () => Promise.resolve() },
})
const response = await api.agentPresets.list(request({}))
const response = await api.host.describe(request({}))
expect(response.result.ok && response.result.value.hasDocument).toBe(true)
expect(response.result.ok && response.result.value.canOpenPath).toBe(true)
})
})
@@ -47,12 +47,7 @@ function scriptedApi(overrides: {
},
skills: { list: r => ok(r, { skills: [] }), ...overrides.skills },
agentPresets: {
list: r => ok(r, { presets: [], authorable: false, hasDocument: false }),
select: r => ok(r, { agentPreset: r.payload.agentPreset }),
read: r => ok(r, { agentPreset: r.payload.agentPreset, trust: 'user' as const, content: '' }),
copy: r => ok(r, { agentPreset: r.payload.agentPreset }),
openDocument: r => ok(r, { opened: true as const }),
remove: r => ok(r, {}),
...overrides.agentPresets,
},
settings: {
@@ -115,16 +110,9 @@ describe('unary round trip', () => {
expect(response.result).toMatchObject({ ok: true, value: { version: '0-test' } })
})
it('routes the agent-preset roster and switch through the wire', async () => {
const c = client(scriptedApi())
const listed = await c.agentPresets.list({})
expect(listed.result).toEqual({ ok: true, value: { presets: [], authorable: false, hasDocument: false } })
// The switch carries the session it is about: the host refuses one whose
// conversation has started, and it can only know which by id.
const selected = await c.agentPresets.select({ sessionId: sid('s1'), agentPreset: 'standard' })
expect(selected.result).toEqual({ ok: true, value: { agentPreset: 'standard' } })
it('routes the agent-preset document opener through the wire', async () => {
const opened = await client(scriptedApi()).agentPresets.openDocument({ agentPreset: 'mine' })
expect(opened.result).toEqual({ ok: true, value: { opened: true } })
})
it('passes business errors through as 200 + err result, not a throw', async () => {
@@ -57,30 +57,9 @@ function fakeApi(overrides: Partial<{ crashOn: string }> = {}): ApiProxy {
},
},
agentPresets: {
list(request: RpcRequest<{}>) {
return Promise.resolve({
rpcId: request.rpcId,
result: { ok: true as const, value: { presets: [], authorable: false, hasDocument: false } },
})
},
select(request: RpcRequest<{ agentPreset: string }>) {
const value = { agentPreset: request.payload.agentPreset }
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value } })
},
read(request: RpcRequest<{ agentPreset: string }>) {
const value = { agentPreset: request.payload.agentPreset, trust: 'user' as const, content: '' }
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value } })
},
copy(request: RpcRequest<{ from: string; agentPreset: string }>) {
const value = { agentPreset: request.payload.agentPreset }
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value } })
},
openDocument(request: RpcRequest<{ agentPreset: string }>) {
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value: { opened: true as const } } })
},
remove(request: RpcRequest<{ agentPreset: string }>) {
return Promise.resolve({ rpcId: request.rpcId, result: { ok: true as const, value: {} } })
},
},
skills: {
async list(request) {
@@ -162,26 +141,12 @@ describe('unary round trip (handler ⇄ client, no network)', () => {
if (!response.result.ok) expect(response.result.error.code).toBe('settings-rejected')
})
it('round-trips every agent-preset method, authoring included', async () => {
const c = client()
// The whole domain crosses the carrier: the roster a picker reads, the
// per-session switch, and the authoring calls the settings page makes.
// Each has its own request schema, so a registration missing from either
// half fails here rather than in the browser.
expect((await c.agentPresets.list({})).result).toEqual({
ok: true, value: { presets: [], authorable: false, hasDocument: false },
})
expect((await c.agentPresets.select({ sessionId: 's' as never, agentPreset: 'minimal' })).result)
.toEqual({ ok: true, value: { agentPreset: 'minimal' } })
expect((await c.agentPresets.read({ agentPreset: 'mine' })).result).toEqual({
ok: true, value: { agentPreset: 'mine', trust: 'user', content: '' },
})
expect((await c.agentPresets.copy({ from: 'standard', agentPreset: 'mine' })).result)
.toEqual({ ok: true, value: { agentPreset: 'mine' } })
expect((await c.agentPresets.openDocument({ agentPreset: 'mine' })).result)
it('round-trips the agent-preset document opener', async () => {
// The opener is the domain's whole carried surface: its request schema is
// registered in both halves, so a missing registration fails here rather
// than in the browser.
expect((await client().agentPresets.openDocument({ agentPreset: 'mine' })).result)
.toEqual({ ok: true, value: { opened: true } })
expect((await c.agentPresets.remove({ agentPreset: 'mine' })).result).toEqual({ ok: true, value: {} })
})
it('round-trips the native picker without the default unary timeout', async () => {
@@ -318,4 +318,10 @@ describe('canOpenNativePath', () => {
expect(canOpenNativePath({ platform: 'linux', osRelease: '6.8.0-generic' })).toBe(expected)
})
it('samples the ambient platform when none is named', () => {
// The internals are a test seam; a deployment calls this with nothing and
// must get the answer for the host it is actually running on.
expect(canOpenNativePath()).toBe(canOpenNativePath({ platform: process.platform }))
})
})
@@ -11,9 +11,7 @@ import {
hostListDirectoryRequestSchema, hostListDirectoryValueSchema,
} from '../src/api/host.schema.ts'
import { skillEntrySchema, skillListRequestSchema, skillListValueSchema } from '../src/api/skills.schema.ts'
import {
agentPresetEntrySchema, agentPresetListValueSchema, agentPresetOpenDocumentValueSchema,
} from '../src/api/agent-presets.schema.ts'
import { agentPresetOpenDocumentValueSchema } from '../src/api/agent-presets.schema.ts'
import { subagentPromptRequestSchema } from '../src/api/subagents.schema.ts'
describe('RpcId', () => {
@@ -178,20 +176,6 @@ describe('skills domain schemas', () => {
})
describe('agent-preset schemas', () => {
it('accepts a roster row and rejects an unknown trust', () => {
expect(agentPresetEntrySchema.parse({ id: 'standard', trust: 'system', isDefault: true }))
.toEqual({ id: 'standard', trust: 'system', isDefault: true })
expect(() => agentPresetEntrySchema.parse({ id: 'x', trust: 'root', isDefault: false })).toThrow()
expect(() => agentPresetEntrySchema.parse({ id: '', trust: 'user', isDefault: false })).toThrow()
})
it('accepts an empty roster', () => {
// A deployment composing no presets still reports its authoring and
// native-open capabilities, so a surface knows what to offer.
expect(agentPresetListValueSchema.parse({ presets: [], authorable: false, hasDocument: false }))
.toEqual({ presets: [], authorable: false, hasDocument: false })
})
it('answers the open-document union by its discriminant', () => {
expect(agentPresetOpenDocumentValueSchema.parse({ opened: true })).toEqual({ opened: true })
expect(agentPresetOpenDocumentValueSchema.parse({ opened: false, path: '/presets/mine' }))
@@ -0,0 +1,460 @@
/**
* The agent-preset Remote namespace: the path-free roster a client reads, the
* composition view behind the read-only viewer, and the per-session switch
* which is the only one of the three that mutates an agent.
*/
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import Include from '@deepseek-ai/cordis-plugin-include'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { TypertRemoteFailure, type RemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
import { afterEach, describe, expect, it, vi } from 'vitest'
import AgentPresets, { COMPOSITION_FILE, METADATA_FILE } from '@deepseek-ai/dsh-agent-presets'
import type { Config } from '@deepseek-ai/dsh-agent-presets'
import type {} from '@deepseek-ai/dsh-agent-presets/types'
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
const ROOTS = [
{ path: join(FIXTURES, 'system'), trust: 'system' as const },
{ path: join(FIXTURES, 'user'), trust: 'user' as const },
]
const VALID = '- id: tool-alpha\n name: ../../plugins/contribute.js\n config:\n tool: alpha\n'
afterEach(() => vi.restoreAllMocks())
async function remoteFailure(operation: Promise<unknown>): Promise<RemoteFailure> {
try {
await operation
} catch (error: unknown) {
expect(error).toBeInstanceOf(TypertRemoteFailure)
if (error instanceof TypertRemoteFailure) return error.failure
throw error
}
throw new Error('expected the Remote operation to fail')
}
function availableOf(failure: RemoteFailure): readonly string[] {
if (!('available' in failure.details)) throw new Error('expected available preset ids')
const available: unknown = failure.details.available
if (!Array.isArray(available)
|| !available.every((value: unknown): value is string => typeof value === 'string')) {
throw new Error('expected available preset ids')
}
return available
}
function reasonOf(failure: RemoteFailure): string {
if (!('reason' in failure.details)) throw new Error('expected a preset failure reason')
const reason: unknown = failure.details.reason
if (typeof reason !== 'string') throw new Error('expected a preset failure reason')
return reason
}
async function harness(
roster: Config = { default: 'standard', roots: ROOTS, includeShippedRoot: false, includeUserRoot: false },
): Promise<Context> {
const ctx = new Context()
ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRuntime)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(AgentPresets, roster)
return ctx
}
async function agentOn(ctx: Context, id: string, presetId?: string): Promise<Agent> {
const handle = await ctx.agents.create({
sessionId: SessionId(id),
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, presetId),
})
return handle.agent
}
/** The recorded preset a restart replays, which is what a switch must move. */
const recordedPreset = (agent: Agent): unknown =>
agent.session.events.findLast(event => event.type === 'agent-preset/selected')?.data
describe('the roster a client reads', () => {
it('projects path-free rows, marking the default and carrying published metadata', async () => {
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
await mkdir(join(userRoot, 'documented'), { recursive: true })
await writeFile(join(userRoot, 'documented', COMPOSITION_FILE), VALID)
await writeFile(join(userRoot, 'documented', METADATA_FILE), 'name: 我的模式\ndescription: 只做检索。\n')
const ctx = await harness({
default: 'minimal',
roots: [{ path: join(FIXTURES, 'system'), trust: 'system' }, { path: userRoot, trust: 'user' }],
includeShippedRoot: false,
includeUserRoot: false,
})
const roster = await ctx.agentPresets.remoteExportList()
expect(roster.authorable).toBe(true)
expect(roster.presets).toEqual([
{ id: 'minimal', trust: 'system', isDefault: true },
{ id: 'standard', trust: 'system', isDefault: false },
{ id: 'documented', trust: 'user', isDefault: false, name: '我的模式', description: '只做检索。' },
])
// No row carries the composition's location: a preset is addressed by id
// everywhere off the Host.
expect(roster.presets.every(row => !('path' in row))).toBe(true)
})
it('keeps a broken preset on the roster with its reason', async () => {
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
await mkdir(join(userRoot, 'damaged'), { recursive: true })
const ctx = await harness({
default: 'standard',
roots: [{ path: join(FIXTURES, 'system'), trust: 'system' }, { path: userRoot, trust: 'user' }],
includeShippedRoot: false,
includeUserRoot: false,
})
const roster = await ctx.agentPresets.remoteExportList()
// The directory still occupies the id, so a surface must be able to show
// and delete it; offering it for selection is what the reason prevents.
expect(roster.presets.find(row => row.id === 'damaged')?.broken).toEqual(expect.any(String))
})
it('answers an empty roster with nothing authorable', async () => {
const ctx = await harness({ default: 'standard', roots: [], includeShippedRoot: false, includeUserRoot: false })
const roster = await ctx.agentPresets.remoteExportList()
// Composing no presets is a valid deployment: every session then shares
// the host composition, and nothing can be written either.
expect(roster).toEqual({ presets: [], authorable: false })
})
})
describe('reading one composition', () => {
it('rejects an empty id before resolving it', async () => {
const ctx = await harness()
const resolve = vi.spyOn(ctx.agentPresets, 'resolve')
await expect(ctx.agentPresets.readDocument(''))
.rejects.toMatchObject({ failure: { code: 'bad-request' } })
expect(resolve).not.toHaveBeenCalled()
})
it('answers the stored text with the row it belongs to', async () => {
const ctx = await harness()
const document = await ctx.agentPresets.readDocument('standard')
// The shipped set is readable: it is the known-good composition a copy
// starts from, and trust is what tells a surface to say so.
expect(document).toEqual({
agentPreset: 'standard',
trust: 'system',
content: await ctx.agentPresets.read('standard'),
})
})
it('carries the display metadata a preset published', async () => {
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
await mkdir(join(userRoot, 'documented'), { recursive: true })
await writeFile(join(userRoot, 'documented', COMPOSITION_FILE), VALID)
await writeFile(join(userRoot, 'documented', METADATA_FILE), 'name: 我的模式\ndescription: 只做检索。\n')
const ctx = await harness({
default: 'documented',
roots: [{ path: userRoot, trust: 'user' }],
includeShippedRoot: false,
includeUserRoot: false,
})
const document = await ctx.agentPresets.readDocument('documented')
// The viewer titles the dialog from the published name, so both optional
// fields have to survive the projection rather than only the id.
expect(document).toEqual({
agentPreset: 'documented',
trust: 'user',
content: VALID,
name: '我的模式',
description: '只做检索。',
})
})
it('refuses an id no root supplies', async () => {
const ctx = await harness()
const failure = await remoteFailure(ctx.agentPresets.readDocument('never-existed'))
expect(failure).toMatchObject({
code: 'agent-preset-not-found',
details: {
agentPreset: 'never-existed',
},
})
expect(failure.message)
.toMatch(/^agent-presets: preset "never-existed" not found \(available: .+\)$/)
expect(availableOf(failure)).toEqual(expect.arrayContaining(['minimal', 'standard']))
})
it('keeps the legacy internal diagnostic for an unrelated read failure', async () => {
const ctx = await harness()
vi.spyOn(ctx.agentPresets, 'read').mockRejectedValueOnce(new Error('disk failed'))
const failure = await remoteFailure(ctx.agentPresets.readDocument('standard'))
expect(failure).toEqual({
code: 'internal',
message: 'agent preset "standard": Error: disk failed',
details: {},
})
})
})
describe('authoring over Remote', () => {
it('rejects empty source, target, and delete ids before authoring', async () => {
const ctx = await harness()
const copy = vi.spyOn(ctx.agentPresets, 'copy')
const remove = vi.spyOn(ctx.agentPresets, 'remove')
for (const operation of [
() => ctx.agentPresets.remoteExportCopy('', 'mine'),
() => ctx.agentPresets.remoteExportCopy('standard', ''),
() => ctx.agentPresets.remoteExportDelete(''),
]) {
await expect(operation()).rejects.toMatchObject({ failure: { code: 'bad-request' } })
}
expect(copy).not.toHaveBeenCalled()
expect(remove).not.toHaveBeenCalled()
})
it('copies and deletes through the Remote adapters', async () => {
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
const ctx = await harness({
default: 'standard',
roots: [{ path: join(FIXTURES, 'system'), trust: 'system' }, { path: userRoot, trust: 'user' }],
includeShippedRoot: false,
includeUserRoot: false,
})
await ctx.agentPresets.remoteExportCopy('standard', 'mine', '我的模式')
expect((await ctx.agentPresets.resolve('mine')).name).toBe('我的模式')
await ctx.agentPresets.remoteExportDelete('mine')
await expect(ctx.agentPresets.resolve('mine')).rejects.toThrow(/not found/)
})
it('preserves not-found details for an unknown copy source', async () => {
const ctx = await harness()
const failure = await remoteFailure(ctx.agentPresets.remoteExportCopy('never-existed', 'mine'))
expect(failure).toMatchObject({
code: 'agent-preset-not-found',
details: {
agentPreset: 'never-existed',
},
})
expect(failure.message)
.toMatch(/^agent-presets: preset "never-existed" not found \(available: .+\)$/)
expect(availableOf(failure)).toEqual(expect.arrayContaining(['minimal', 'standard']))
})
it('preserves invalid-id and occupied-id failures', async () => {
const ctx = await harness()
const invalid = await remoteFailure(ctx.agentPresets.remoteExportCopy('standard', '../escape'))
expect(invalid).toMatchObject({
code: 'agent-preset-invalid',
details: { agentPreset: '../escape' },
})
expect(reasonOf(invalid)).toContain('must match')
const occupied = await remoteFailure(ctx.agentPresets.remoteExportCopy('standard', 'minimal'))
expect(occupied).toMatchObject({
code: 'agent-preset-invalid',
details: { agentPreset: 'minimal' },
})
expect(reasonOf(occupied)).toContain('already exists')
})
it('keeps the requested id when no writable root exists', async () => {
const ctx = await harness({
default: 'standard',
roots: [{ path: join(FIXTURES, 'system'), trust: 'system' }],
includeShippedRoot: false,
includeUserRoot: false,
})
const failure = await remoteFailure(ctx.agentPresets.remoteExportCopy('standard', 'mine'))
expect(failure).toMatchObject({
code: 'agent-preset-read-only',
details: { agentPreset: 'mine' },
})
expect(reasonOf(failure)).toContain('no user-writable preset root')
})
it('preserves read-only and not-found delete failures', async () => {
const ctx = await harness()
const readOnly = await remoteFailure(ctx.agentPresets.remoteExportDelete('standard'))
expect(readOnly).toMatchObject({
code: 'agent-preset-read-only',
details: { agentPreset: 'standard' },
})
expect(reasonOf(readOnly)).toContain('ships with the deployment')
const missing = await remoteFailure(ctx.agentPresets.remoteExportDelete('never-existed'))
expect(missing).toMatchObject({
code: 'agent-preset-not-found',
details: { agentPreset: 'never-existed' },
})
expect(availableOf(missing)).toEqual(expect.arrayContaining(['minimal', 'standard']))
})
it('keeps the legacy internal diagnostic for an unrelated authoring failure', async () => {
const ctx = await harness()
vi.spyOn(ctx.agentPresets, 'copy').mockRejectedValueOnce(new Error('copy failed'))
const failure = await remoteFailure(ctx.agentPresets.remoteExportCopy('standard', 'mine'))
expect(failure).toEqual({
code: 'internal',
message: 'agent preset "mine": Error: copy failed',
details: {},
})
})
})
describe('switching one session\'s composition', () => {
it('rejects an empty preset id before queuing a switch', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-empty', 'standard')
const recompose = vi.spyOn(ctx.agentPresets, 'recompose')
await expect(ctx.agentPresets.select(agent, ''))
.rejects.toMatchObject({ failure: { code: 'bad-request' } })
expect(recompose).not.toHaveBeenCalled()
})
it('recomposes a blank session and records what it now runs', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-1', 'standard')
expect(await ctx.agentPresets.select(agent, 'minimal')).toBe('minimal')
// The header is written once at creation, so the switch lives in the log:
// that is what a restart replays and what every projection resolves from.
expect(ctx.agentPresets.composedPreset(agent.ctx)).toBe('minimal')
expect(recordedPreset(agent)).toEqual({ agentPreset: 'minimal' })
})
it('serializes two concurrent switches on one session', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-race', 'standard')
// Both pass the blank check; unserialized, the second re-link finds the
// record the first already replaced and two compositions end up in one
// agent layer. A client's busy flag is not enforcement.
await Promise.all([
ctx.agentPresets.select(agent, 'minimal'),
ctx.agentPresets.select(agent, 'standard'),
])
// One winner, and the log agrees with it: the last committed switch.
expect(recordedPreset(agent)).toEqual({ agentPreset: 'standard' })
expect(ctx.agentPresets.composedPreset(agent.ctx)).toBe('standard')
})
it('refuses once the conversation has started', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-locked', 'standard')
// One turn is enough: the history from here on was produced under
// `standard`'s tools, and a swap would strand those tool calls.
agent.session.append('turn/start', { turn: 0 })
const failure = await remoteFailure(ctx.agentPresets.select(agent, 'minimal'))
expect(failure).toEqual({
code: 'agent-preset-locked',
message: 'session "sel-locked" has already started; its agent preset is fixed',
details: { sessionId: SessionId('sel-locked'), agentPreset: 'minimal' },
})
expect(ctx.agentPresets.composedPreset(agent.ctx)).toBe('standard')
expect(recordedPreset(agent)).toBeUndefined()
})
it('leaves the session on its composition when the named preset is unknown', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-unknown', 'standard')
const failure = await remoteFailure(ctx.agentPresets.select(agent, 'nope'))
expect(failure).toMatchObject({
code: 'agent-preset-not-found',
details: { agentPreset: 'nope' },
})
expect(availableOf(failure)).toEqual(expect.arrayContaining(['minimal', 'standard']))
// Resolution happens before any re-link, and nothing is recorded until the
// swap commits.
expect(ctx.agentPresets.composedPreset(agent.ctx)).toBe('standard')
expect(recordedPreset(agent)).toBeUndefined()
})
it('serves a later switch after one was refused', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-after-failure', 'standard')
await expect(ctx.agentPresets.select(agent, 'nope')).rejects.toThrow(/not found/)
// The queue holds a failure-swallowing guard, so a refused switch does not
// reject the next caller's chain.
expect(await ctx.agentPresets.select(agent, 'minimal')).toBe('minimal')
})
it('reports an unusable composition with its discovery reason', async () => {
const userRoot = await mkdtemp(join(tmpdir(), 'dsh-preset-remote-'))
await mkdir(join(userRoot, 'damaged'), { recursive: true })
const ctx = await harness({
default: 'standard',
roots: [{ path: join(FIXTURES, 'system'), trust: 'system' }, { path: userRoot, trust: 'user' }],
includeShippedRoot: false,
includeUserRoot: false,
})
const agent = await agentOn(ctx, 'sel-broken', 'standard')
const failure = await remoteFailure(ctx.agentPresets.select(agent, 'damaged'))
expect(failure).toMatchObject({
code: 'agent-preset-invalid',
details: { agentPreset: 'damaged' },
})
expect(reasonOf(failure)).not.toBe('')
})
it('keeps the legacy internal diagnostic for an unrelated switch failure', async () => {
const ctx = await harness()
const agent = await agentOn(ctx, 'sel-internal', 'standard')
vi.spyOn(ctx.agentPresets, 'recompose').mockRejectedValueOnce(new Error('mount failed'))
const failure = await remoteFailure(ctx.agentPresets.select(agent, 'minimal'))
expect(failure).toEqual({
code: 'internal',
message: 'failed to select agent preset "minimal": Error: mount failed',
details: {},
})
})
})