Selaa lähdekoodia

test(api): complete migrated Remote coverage

imccyu 3 viikkoa sitten
vanhempi
sitoutus
88f2f0aaec

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: e0376d865fac1505cce48f4f3a678a11730ecd0e
-module-graph.zh.md: 72af3b9a52764e0ae8ed2b7cef910eeedefa6c6c
+module-graph.md: aebb6883280d5edc48355e83936cb8793529fc2a
+module-graph.zh.md: 43607e291b632d1df6cd00a59e182027dc548008

+ 2 - 1
docs/module-graph.md

@@ -549,6 +549,7 @@ flowchart TD
   pkg_file_reference --> pkg_invariants
   pkg_time_context --> pkg_agent
   pkg_time_context --> pkg_invariants
+  pkg_time_context --> pkg_llm
   pkg_time_context --> pkg_session
   pkg_message_feedback --> pkg_brand
   pkg_message_feedback --> pkg_invariants
@@ -1789,7 +1790,7 @@ flowchart TD
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
 | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |

+ 2 - 1
docs/module-graph.zh.md

@@ -551,6 +551,7 @@ flowchart TD
   pkg_file_reference --> pkg_invariants
   pkg_time_context --> pkg_agent
   pkg_time_context --> pkg_invariants
+  pkg_time_context --> pkg_llm
   pkg_time_context --> pkg_session
   pkg_message_feedback --> pkg_brand
   pkg_message_feedback --> pkg_invariants
@@ -1791,7 +1792,7 @@ flowchart TD
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
 | [`spill-local`](../packages/spill/spill-local) | `spill` | [`invariants`](../packages/runtime-diagnostics/invariants), [`spill`](../packages/spill/spill) |
 | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
+| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |

+ 2 - 2
packages/api/session-controller/tests/session-models.host.spec.ts

@@ -305,9 +305,9 @@ describe('Web session model selection', () => {
       model: 'private-preview',
       reasoningEffort: ReasoningEffortId('max'),
     })
-    createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' })
+    const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' })
 
-    const catalog = await buildModelCatalog(ctx)
+    const catalog = expectValue(await remote.modelCatalog())
     expect(currentSelection(ctx, sessionId)).toEqual({
       provider: 'deepseek-official',
       model: 'private-preview',

+ 55 - 1
packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts

@@ -2,7 +2,11 @@ import { Context } from '@deepseek-ai/cordis'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
 import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
 import { describe, expect, it, vi } from 'vitest'
-import { createSessionTestRemote, testSessionPersistence } from './test-remote.ts'
+import {
+  createSessionTestController,
+  createSessionTestRemote,
+  testSessionPersistence,
+} from './test-remote.ts'
 
 async function context(): Promise<Context> {
   const ctx = new Context()
@@ -92,4 +96,54 @@ describe('session/openWorkspacePath', () => {
     await expect(remote.openWorkspacePath({ sessionId, path: 'result.html' }, aborted.signal))
       .resolves.toMatchObject({ ok: false, error: { code: 'cancelled' } })
   })
+
+  it('classifies inspection cancellation and non-session failures', async () => {
+    const ctx = await context()
+    const controller = createSessionTestController(ctx, {
+      defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
+      cwd: '/default',
+    })
+    const inspect = vi.spyOn(controller, 'inspect')
+    const aborted = new AbortController()
+    inspect.mockImplementationOnce(async () => {
+      aborted.abort(new Error('cancelled'))
+      throw new Error('inspection stopped')
+    })
+    await expect(controller.openWorkspacePath({
+      sessionId: SessionId('inspection-cancelled'), path: 'result.html',
+    }, aborted.signal)).rejects.toMatchObject({ failure: { code: 'cancelled' } })
+
+    inspect.mockRejectedValueOnce('storage offline')
+    await expect(controller.openWorkspacePath({
+      sessionId: SessionId('inspection-failed'), path: 'result.html',
+    }, new AbortController().signal)).rejects.toMatchObject({
+      failure: { code: 'internal', message: expect.stringContaining('storage offline') },
+    })
+  })
+
+  it('classifies opener cancellation and non-Error failures', async () => {
+    const ctx = await context()
+    const sessionId = SessionId('open-error-kinds')
+    ctx.sessions.create(sessionId, { meta: { cwd: '/workspace/project' } })
+    const aborted = new AbortController()
+    const openPath = vi.fn()
+      .mockImplementationOnce(async () => {
+        aborted.abort(new Error('cancelled'))
+        throw new Error('opening stopped')
+      })
+      .mockRejectedValueOnce('desktop unavailable')
+    const controller = createSessionTestController(ctx, {
+      defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
+      cwd: '/default',
+      openPath,
+    })
+
+    await expect(controller.openWorkspacePath({ sessionId, path: 'first.html' }, aborted.signal))
+      .rejects.toMatchObject({ failure: { code: 'cancelled' } })
+    await expect(controller.openWorkspacePath({
+      sessionId, path: 'second.html',
+    }, new AbortController().signal)).rejects.toMatchObject({
+      failure: { code: 'internal', message: 'path open failed: desktop unavailable' },
+    })
+  })
 })

+ 37 - 0
packages/api/session-controller/tests/session-skills.host.spec.ts

@@ -188,4 +188,41 @@ describe('SessionSkillCatalog', () => {
         failure: { code: 'internal', message: expect.stringContaining('skill registry is absent') },
       })
   })
+
+  it('rejects observations without projections or a project cwd', async () => {
+    const ctx = await context()
+    const sessionId = SessionId('incomplete-skills')
+    const withoutProjections = { ...observation(sessionId, { cwd: '/project' }), projections: undefined }
+    const observeSession = vi.fn()
+      .mockResolvedValueOnce(withoutProjections)
+      .mockResolvedValueOnce(observation(sessionId))
+    ctx.provide('sessionQuery', { observeSession } as never)
+    const catalog = new SessionSkillCatalog(ctx)
+
+    await expect(catalog.list({ sessionId }, new AbortController().signal))
+      .rejects.toMatchObject({
+        failure: { code: 'internal', message: expect.stringContaining('projected Session observation') },
+      })
+    await expect(catalog.list({ sessionId }, new AbortController().signal))
+      .rejects.toMatchObject({
+        failure: { code: 'internal', message: expect.stringContaining('has no project cwd') },
+      })
+  })
+
+  it('classifies a provider listing failure', async () => {
+    const ctx = await context()
+    const sessionId = SessionId('failed-skills')
+    ctx.provide('sessionQuery', {
+      observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/project' })),
+    } as never)
+    ctx.provide('skills', {
+      list: () => Promise.reject(new Error('catalog offline')),
+    } as never)
+    const catalog = new SessionSkillCatalog(ctx)
+
+    await expect(catalog.list({ sessionId }, new AbortController().signal))
+      .rejects.toMatchObject({
+        failure: { code: 'internal', message: 'skill listing failed: Error: catalog offline' },
+      })
+  })
 })

+ 3 - 0
packages/api/session-controller/tests/test-remote.ts

@@ -19,6 +19,7 @@ import {
 } from '@deepseek-ai/dsh-typert-protocol'
 import SessionController from '../src/index.ts'
 import type {
+  ModelCatalog,
   SessionAttachmentRequest,
   SessionAttachmentValue,
   SessionCancelRequest,
@@ -54,6 +55,7 @@ export interface TestSessionRemote {
   search(request: SessionSearchRequest, signal?: AbortSignal): Promise<RemoteResult<SessionSearchValue>>
   create(request: SessionCreateRequest): Promise<RemoteResult<SessionCreateValue>>
   selectModel(request: SessionSelectModelRequest): Promise<RemoteResult<SessionSelectModelValue>>
+  modelCatalog(): Promise<RemoteResult<ModelCatalog>>
   rename(request: SessionRenameRequest): Promise<RemoteResult<SessionRenameValue>>
   fork(request: SessionForkRequest): Promise<RemoteResult<SessionForkValue>>
   prompt(request: SessionPromptRequest, signal?: AbortSignal): Promise<RemoteResult<SessionPromptValue>>
@@ -241,6 +243,7 @@ export function createSessionTestRemote(
     ),
     create: request => remoteResult(() => direct.create(request)),
     selectModel: request => remoteResult(() => direct.selectModel(request)),
+    modelCatalog: () => remoteResult(() => direct.modelCatalog()),
     rename: request => remoteResult(() => direct.rename(request)),
     fork: request => remoteResult(() => direct.fork(request)),
     prompt: (request, signal = new AbortController().signal) => remoteResult(

+ 96 - 0
packages/api/settings-controller/tests/settings-controller.host.spec.ts

@@ -1,6 +1,11 @@
 import { describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
+import {
+  InvalidPresetIdError,
+  PresetExistsError,
+  UnknownPresetError,
+} from '@deepseek-ai/dsh-agent-presets'
 import { settingsNamespace } from '@deepseek-ai/dsh-settings'
 import type { SettingsDescriptor, SettingsNamespace } from '@deepseek-ai/dsh-settings'
 import { TypertRemoteFailure, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
@@ -306,6 +311,32 @@ describe('the settings Remote namespace a configuration page calls', () => {
       })
   })
 
+  it('classifies cancellation while preparing or opening the settings document', async () => {
+    const preparing = new Context()
+    await preparing.plugin(DocumentSettings)
+    const prepareAbort = new AbortController()
+    vi.spyOn(preparing.settings, 'prepareDocument').mockImplementation(async () => {
+      prepareAbort.abort(new Error('cancelled'))
+      throw new Error('preparation stopped')
+    })
+    const preparingController = new SettingsController(preparing)
+    await expect(preparingController.openSettingsDocument(prepareAbort.signal))
+      .rejects.toMatchObject({ failure: { code: 'cancelled' } })
+
+    const opening = new Context()
+    await opening.plugin(DocumentSettings)
+    vi.spyOn(opening.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml')
+    const openAbort = new AbortController()
+    const openingController = new SettingsController(opening, {}, {
+      openTextFile: async () => {
+        openAbort.abort(new Error('cancelled'))
+        throw new Error('opening stopped')
+      },
+    })
+    await expect(openingController.openSettingsDocument(openAbort.signal))
+      .rejects.toMatchObject({ failure: { code: 'cancelled' } })
+  })
+
   it('opens a user Agent preset directory or returns its path without a native opener', async () => {
     const ctx = new Context()
     ctx.provide('agentPresets', {
@@ -331,6 +362,21 @@ describe('the settings Remote namespace a configuration page calls', () => {
       .resolves.toEqual({ opened: false, path: '/presets/mine' })
   })
 
+  it('covers native-open detection defaults and explicit overrides', () => {
+    const fromInjectedOpener = new SettingsController(new Context(), {}, {
+      openPath: () => Promise.resolve(),
+    })
+    expect((fromInjectedOpener as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe(true)
+
+    const detected = new SettingsController(new Context())
+    expect(typeof (detected as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe('boolean')
+
+    const override = vi.fn(() => false)
+    const overridden = new SettingsController(new Context(), {}, { canOpenPath: override })
+    expect((overridden as unknown as { canOpenPath: () => boolean }).canOpenPath()).toBe(false)
+    expect(override).toHaveBeenCalledOnce()
+  })
+
   it('refuses a shipped Agent preset and a missing preset provider', async () => {
     const ctx = new Context()
     ctx.provide('agentPresets', {
@@ -346,4 +392,54 @@ describe('the settings Remote namespace a configuration page calls', () => {
     await expect(missing.openAgentPresetDirectory('mine', new AbortController().signal))
       .rejects.toMatchObject({ failure: { code: 'agent-preset-not-found' } })
   })
+
+  it('rejects an empty Agent preset id before resolving a provider', async () => {
+    const resolve = vi.fn()
+    const ctx = new Context()
+    ctx.provide('agentPresets', { resolve } as never)
+    const controller = new SettingsController(ctx)
+
+    await expect(controller.openAgentPresetDirectory('', new AbortController().signal))
+      .rejects.toMatchObject({ failure: { code: 'bad-request' } })
+    expect(resolve).not.toHaveBeenCalled()
+  })
+
+  it.each([
+    [new UnknownPresetError('missing', ['standard']), 'agent-preset-not-found'],
+    [new InvalidPresetIdError('../bad'), 'agent-preset-invalid'],
+    [new PresetExistsError('taken'), 'agent-preset-invalid'],
+    [new TypertRemoteFailure({ code: 'cancelled', message: 'cancelled', details: {} }), 'cancelled'],
+    ['unexpected preset failure', 'internal'],
+  ] as const)('maps Agent preset resolution failure %#', async (error, code) => {
+    const ctx = new Context()
+    ctx.provide('agentPresets', { resolve: () => Promise.reject(error) } as never)
+    const controller = new SettingsController(ctx)
+
+    await expect(controller.openAgentPresetDirectory('mine', new AbortController().signal))
+      .rejects.toMatchObject({ failure: { code } })
+  })
+
+  it('classifies cancellation and non-Error failures from the preset opener', async () => {
+    const ctx = new Context()
+    ctx.provide('agentPresets', {
+      resolve: (id: string) => Promise.resolve({
+        id, trust: 'user', path: `/presets/${id}/agent.cordis.yml`,
+      }),
+    } as never)
+    const abort = new AbortController()
+    const openPath = vi.fn()
+      .mockImplementationOnce(async () => {
+        abort.abort(new Error('cancelled'))
+        throw new Error('opening stopped')
+      })
+      .mockRejectedValueOnce('desktop unavailable')
+    const controller = new SettingsController(ctx, { nativeOpen: true }, { openPath })
+
+    await expect(controller.openAgentPresetDirectory('first', abort.signal))
+      .rejects.toMatchObject({ failure: { code: 'cancelled' } })
+    await expect(controller.openAgentPresetDirectory('second', new AbortController().signal))
+      .rejects.toMatchObject({
+        failure: { code: 'internal', message: 'path open failed: desktop unavailable' },
+      })
+  })
 })

+ 15 - 0
packages/client/ui-settings-models/tests/store.client.spec.ts

@@ -181,6 +181,21 @@ describe('ModelsSettingsStore', () => {
     expect(store.store.getSnapshot().status).toBe('ready')
   })
 
+  it('surfaces a configurable-provider directory failure', async () => {
+    const { face, mirror } = api()
+    const llm = (face as unknown as {
+      llm: { listConfigurableProviders: () => Promise<RemoteAnswer<never>> }
+    }).llm
+    llm.listConfigurableProviders = () => Promise.resolve(remoteFail<never>('configuration directory down'))
+    const store = new ModelsSettingsStore(face, settingsSchema, mirror)
+
+    await store.load()
+
+    expect(store.store.getSnapshot()).toMatchObject({
+      status: 'error', error: 'configuration directory down',
+    })
+  })
+
   it('lets the newest load win over a stale slow response', async () => {
     let release: (() => void) | undefined
     const gate = new Promise<void>((resolve) => { release = resolve })

+ 5 - 3
packages/client/ui-settings-plugins/tests/apply.client.spec.ts

@@ -29,7 +29,7 @@ async function bench(served?: string[]) {
   ctx.provide('locale', locale)
   const describeCredentials = vi.fn(() => Promise.resolve({ ok: false, error: { code: 'internal', message: 'no provider', details: {} } }))
   const models = vi.fn(() => Promise.resolve({
-    rpcId: 'm', result: { ok: true, value: { groups: [], failures: [] } },
+    ok: true as const, value: { groups: [], failures: [] },
   }))
   const describeSettings = vi.fn(() => Promise.resolve(served === undefined
     ? { ok: false, error: { code: 'internal', message: 'no provider', details: {} } }
@@ -45,11 +45,11 @@ async function bench(served?: string[]) {
     }))
   const remote = new TestRemote(ctx, {
     credentials: { describe: describeCredentials, set: vi.fn() },
+    session: { modelCatalog: models },
     settings: { describe: describeSettings },
   })
   ctx.provide('connection', {
     isLoopback: true,
-    api: { llm: { models } },
   } as never)
   await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
   return {
@@ -66,7 +66,9 @@ function declareRoot(slots: SlotRegistry): () => void {
 
 describe('ui-settings-plugins apply', () => {
   it('declares the services it uses', () => {
-    expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'remote.credentials', 'settingsScope'])
+    expect(inject).toEqual([
+      'slots', 'locale', 'connection', 'remote', 'remote.credentials', 'remote.session', 'settingsScope',
+    ])
   })
 
   it('registers one Plugins section and declares the tab and card slots', async () => {

+ 15 - 21
packages/client/ui-settings-plugins/tests/stores.client.spec.ts

@@ -65,12 +65,11 @@ function modelsApi(options: {
   error?: string
 } = {}) {
   const models = vi.fn(() => Promise.resolve({
-    rpcId: 'm-1' as never,
-    result: options.error === undefined
+    ...(options.error === undefined
       ? { ok: true as const, value: { groups: options.groups ?? [], failures: options.failures ?? [] } }
-      : { ok: false as const, error: { code: 'internal_error' as never, message: options.error } },
+      : { ok: false as const, error: { code: 'internal' as const, message: options.error, details: {} } }),
   }))
-  return { api: { llm: { models } } as never, models }
+  return { api: { modelCatalog: models } as never, models }
 }
 
 function deferred<T>() {
@@ -662,15 +661,14 @@ describe('SubagentModelSelectionCardController', () => {
     const refreshed = deferred<never>()
     const models = vi.fn()
       .mockResolvedValueOnce({
-        rpcId: 'catalog-1',
-        result: { ok: true, value: {
+        ok: true, value: {
           groups: [{ id: 'alpha', name: 'Alpha', models: [{ id: 'fast', name: 'Fast' }] }],
           failures: [],
-        } },
+        },
       })
       .mockImplementationOnce(() => refreshed.promise)
     const controller = new SubagentModelSelectionCardController(
-      host.scope, { llm: { models } } as never,
+      host.scope, { modelCatalog: models } as never,
     )
     const face = controller.inject()
     const state = () => face.hooks.subagentModelSelectionCard.getSnapshot()
@@ -684,8 +682,7 @@ describe('SubagentModelSelectionCardController', () => {
       candidates: [expect.objectContaining({ key: 'alpha\0fast', selected: true })],
     })
     refreshed.resolve({
-      rpcId: 'catalog-2',
-      result: { ok: true, value: { groups: [], failures: [] } },
+      ok: true, value: { groups: [], failures: [] },
     } as never)
     await vi.waitFor(() => { expect(state().catalogStatus).toBe('ready') })
     expect(state().candidates).toEqual([
@@ -738,21 +735,19 @@ describe('SubagentModelSelectionCardController', () => {
     })
     const models = vi.fn()
       .mockResolvedValueOnce({
-        rpcId: 'catalog-1',
-        result: { ok: true, value: {
+        ok: true, value: {
           groups: [{ id: 'alpha', name: 'Alpha', models: [{ id: 'fast', name: 'Fast' }] }],
           failures: [],
-        } },
+        },
       })
       .mockResolvedValueOnce({
-        rpcId: 'catalog-2',
-        result: { ok: true, value: {
+        ok: true, value: {
           groups: [{ id: 'beta', name: 'Beta', models: [{ id: 'new', name: 'New' }] }],
           failures: [],
-        } },
+        },
       })
     const controller = new SubagentModelSelectionCardController(
-      host.scope, { llm: { models } } as never,
+      host.scope, { modelCatalog: models } as never,
     )
     const state = () => controller.inject().hooks.subagentModelSelectionCard.getSnapshot()
     await vi.waitFor(() => { expect(state().candidates[0]?.provider).toBe('alpha') })
@@ -807,7 +802,7 @@ describe('SubagentModelSelectionCardController', () => {
 
     const pending = deferred<never>()
     const models = vi.fn(() => pending.promise)
-    const controller = new SubagentModelSelectionCardController(host.scope, { llm: { models } } as never)
+    const controller = new SubagentModelSelectionCardController(host.scope, { modelCatalog: models } as never)
     const face = controller.inject()
     face.toggleEnabled()
     face.retryCatalog()
@@ -819,14 +814,13 @@ describe('SubagentModelSelectionCardController', () => {
     const pendingResolve = deferred<never>()
     const resolving = new SubagentModelSelectionCardController(
       host.scope,
-      { llm: { models: () => pendingResolve.promise } } as never,
+      { modelCatalog: () => pendingResolve.promise } as never,
     )
     const resolvingFace = resolving.inject()
     resolvingFace.toggleEnabled()
     resolving.dispose()
     pendingResolve.resolve({
-      rpcId: 'late' as never,
-      result: { ok: true, value: { groups: [], failures: [] } },
+      ok: true, value: { groups: [], failures: [] },
     } as never)
     await pendingResolve.promise
   })