Bladeren bron

test(agent-presets): cover the Remote migration

imccyu 1 maand geleden
bovenliggende
commit
c5be99838c

+ 0 - 11
packages/api/session-controller/tests/fake-api.client.ts

@@ -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'] = {

+ 84 - 69
packages/client/connection/src/client/fixture.ts

@@ -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)

+ 0 - 11
packages/client/connection/tests/fake-api.client.ts

@@ -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'] = {

+ 3 - 3
packages/client/connection/tests/node-half.host.spec.ts

@@ -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])

+ 51 - 49
packages/client/ui-agent-preset/tests/apply.client.spec.ts

@@ -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',
     },

+ 113 - 59
packages/client/ui-agent-preset/tests/section-store.client.spec.ts

@@ -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,89 +51,122 @@ 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(
+  defaultId: { id: string },
+  options: FakeOptions = {},
+): 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: {
+      openDocument: (payload: { agentPreset: string }) => {
+        record('openDocument', payload)
+        if (options.throwOpen === true) return Promise.reject(new Error('socket closed'))
+        if (options.failOpen !== undefined) return fail(options.failOpen)
+        return (options.hasDocument ?? true)
+          ? ok({ opened: true })
+          : ok({ opened: false, path: `/presets/${payload.agentPreset}` })
+      },
+    },
+    settings: {
+      update: (payload: { ns: string; patch: { default?: string } }) => {
+        record('settings.update', payload)
+        if (options.failSettings !== undefined) return fail(options.failSettings)
+        /* v8 ignore next -- the controller only ever patches `default` */
+        defaultId.id = payload.patch.default ?? defaultId.id
+        return ok({})
+      },
+    },
+  } 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<IApiClient, 'agentPresets' | 'settings'> {
+): 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 fail(options.failList)
-        return ok({
+        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,
-          hasDocument: options.hasDocument ?? true,
         })
       },
-      read: (payload: { agentPreset: string }) => {
-        record('read', payload)
+      read: (agentPreset: string) => {
+        record('read', { agentPreset })
         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)
+        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 fail(`unknown preset ${payload.agentPreset}`)
-        return ok({
-          agentPreset: payload.agentPreset,
+        if (preset === undefined) return remoteFail(`unknown preset ${agentPreset}`)
+        return remoteOk({
+          agentPreset,
           trust: preset.trust,
           content: preset.content,
           ...preset.name === undefined ? {} : { name: preset.name },
         })
       },
-      copy: (payload: { from: string; agentPreset: string; name?: string }) => {
-        record('copy', payload)
+      // 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 fail(options.failCopy)
-        const source = presets.get(payload.from)
+        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 fail(`unknown preset ${payload.from}`)
-        presets.set(payload.agentPreset, {
+        if (source === undefined) return remoteFail(`unknown preset ${from}`)
+        presets.set(id, {
           trust: 'user',
           content: source.content,
-          ...payload.name === undefined ? {} : { name: payload.name },
+          ...name === undefined ? {} : { name },
         })
-        return ok({ agentPreset: payload.agentPreset })
+        return remoteOk(undefined)
       },
-      openDocument: (payload: { agentPreset: string }) => {
-        record('openDocument', payload)
-        if (options.throwOpen === true) return Promise.reject(new Error('socket closed'))
-        if (options.failOpen !== undefined) return fail(options.failOpen)
-        return (options.hasDocument ?? true)
-          ? ok({ opened: true })
-          : ok({ opened: false, path: `/presets/${payload.agentPreset}` })
-      },
-      remove: async (payload: { agentPreset: string }) => {
-        record('remove', payload)
+      deletePreset: async (id: string) => {
+        record('deletePreset', { id })
         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 } }) => {
-        record('settings.update', payload)
-        if (options.failSettings !== undefined) return fail(options.failSettings)
-        /* v8 ignore next -- the controller only ever patches `default` */
-        defaultId.id = payload.patch.default ?? defaultId.id
-        return ok({})
+        if (options.failRemove !== undefined) return await remoteFail(options.failRemove)
+        presets.delete(id)
+        return await remoteOk(undefined)
       },
     },
-  } as unknown as Pick<IApiClient, 'agentPresets' | 'settings'>
+  } 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')
 

+ 94 - 33
packages/client/ui-agent-preset/tests/settings-store.client.spec.ts

@@ -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()
 

+ 9 - 206
packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts

@@ -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)
   })
 })
 

+ 3 - 15
packages/host/apiproxy/tests/client-handler.spec.ts

@@ -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 () => {

+ 5 - 40
packages/host/apiproxy/tests/fetch-carrier.spec.ts

@@ -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 () => {

+ 6 - 0
packages/host/apiproxy/tests/native-path-opener.spec.ts

@@ -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 }))
+  })
 })

+ 1 - 17
packages/host/apiproxy/tests/rpc-schemas.spec.ts

@@ -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' }))

+ 460 - 0
packages/preset/agent-presets/tests/remote.spec.ts

@@ -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: {},
+    })
+  })
+})