From c375f110a2966f26404c27697b273223ec898314 Mon Sep 17 00:00:00 2001 From: murashit Date: Fri, 10 Jul 2026 12:37:05 +0900 Subject: [PATCH] Use model queries as the sole model metadata source --- docs/design.md | 2 + src/app-server/query/cache.ts | 30 ++----- src/app-server/query/snapshots.ts | 1 - src/domain/server/metadata.ts | 3 +- .../connection/server-metadata-actions.ts | 1 - tests/app-server/query-cache.test.ts | 81 ++++++------------- tests/app-server/shared-queries.test.ts | 14 ++-- .../connection/server-actions.test.ts | 27 +------ .../chat/host/view-connection.test.ts | 13 ++- 9 files changed, 52 insertions(+), 120 deletions(-) diff --git a/docs/design.md b/docs/design.md index f82a9b2c..2c0f0adb 100644 --- a/docs/design.md +++ b/docs/design.md @@ -42,6 +42,8 @@ Obsidian and app-server boundaries stay outside Preact components. External life Chat-visible state belongs in the chat state store and named reducer actions. Signals and components may project that state, but they should not become parallel sources of truth. +Shared app-server resources should likewise have one query record per resource. In particular, model catalog data belongs to the model query; broader server metadata may report the model probe status but must not carry a second model snapshot. + Preact Signals are a chat panel rendering adapter, not a second state system. Panel surfaces may read narrow signal-backed read models, but application workflows, domain code, presentation helpers, and pure UI components must not depend on broad reducer slices or reactive state primitives. Imperative DOM bridges are allowed when an external API, host lifecycle, hit-test, focus/selection operation, or measurement problem requires an `HTMLElement`. They should remain narrow boundary adapters, not a second UI composition system inside Preact-owned surfaces. diff --git a/src/app-server/query/cache.ts b/src/app-server/query/cache.ts index 56c50cbd..52537157 100644 --- a/src/app-server/query/cache.ts +++ b/src/app-server/query/cache.ts @@ -194,11 +194,8 @@ export class AppServerQueryCache { writeAppServerMetadata(context: AppServerQueryContext, metadata: SharedServerMetadata): SharedServerMetadata | null { if (!appServerQueryContextIsComplete(context)) return null; - const next = metadataWithLastKnownGood(metadata, this.appServerMetadataSnapshot(context), this.modelsSnapshot(context)); + const next = metadataWithLastKnownGood(metadata, this.appServerMetadataSnapshot(context)); this.client.setQueryData(appServerMetadataQueryKey(context), cloneSharedServerMetadata(next)); - if (metadata.serverDiagnostics.probes.models.status === "ok") { - this.client.setQueryData(appServerModelsQueryKey(context), cloneModelMetadata(next.availableModels)); - } return cloneSharedServerMetadata(next); } @@ -273,27 +270,25 @@ export class AppServerQueryCache { const previous = this.appServerMetadataSnapshot(refreshContext); return this.runWithClient(refreshContext, async (client) => { const runtimeConfig = runtimeConfigSnapshotFromAppServerConfig(await readEffectiveConfig(client, refreshContext.vaultPath)); - const [models, skills, permissionProfiles, rateLimit] = await Promise.all([ + const [modelProbe, skills, permissionProfiles, rateLimit] = await Promise.all([ this.readModelMetadataProbe(refreshContext, client), readSkillMetadataProbe(client, refreshContext.vaultPath, options.forceSkills ?? false), readPermissionProfileMetadataProbe(client, refreshContext.vaultPath), readRateLimitMetadataProbe(client), ]); - const diagnostics = [models.probe, skills.probe, permissionProfiles.probe, rateLimit.probe].reduce( + const diagnostics = [modelProbe, skills.probe, permissionProfiles.probe, rateLimit.probe].reduce( (current, probe) => diagnosticsWithProbe(current, probe), previous?.serverDiagnostics ?? createServerDiagnostics(), ); return metadataWithLastKnownGood( { runtimeConfig, - availableModels: models.value, availableSkills: skills.value, availablePermissionProfiles: permissionProfiles.value, rateLimit: rateLimit.value, serverDiagnostics: diagnostics, }, previous, - this.modelsSnapshot(refreshContext), ); }); }, @@ -319,18 +314,12 @@ export class AppServerQueryCache { private async readModelMetadataProbe( context: AppServerQueryContext, client: AppServerClient, - ): Promise<{ - value: readonly ModelMetadata[]; - probe: SharedServerMetadata["serverDiagnostics"]["probes"]["models"]; - }> { + ): Promise { try { const models = cloneModelMetadata(await this.client.fetchQuery(this.modelsQueryOptionsWithClient(context, client))); - return { value: models, probe: diagnosticProbeOk("models", `${String(models.length)} models`, Date.now()) }; + return diagnosticProbeOk("models", `${String(models.length)} models`, Date.now()); } catch (error) { - return { - value: this.modelsSnapshot(context) ?? [], - probe: diagnosticProbeError("models", error, Date.now()), - }; + return diagnosticProbeError("models", error, Date.now()); } } @@ -383,15 +372,10 @@ export class AppServerQueryCache { } } -function metadataWithLastKnownGood( - metadata: SharedServerMetadata, - previous: SharedServerMetadata | null, - models: readonly ModelMetadata[] | null, -): SharedServerMetadata { +function metadataWithLastKnownGood(metadata: SharedServerMetadata, previous: SharedServerMetadata | null): SharedServerMetadata { const probes = metadata.serverDiagnostics.probes; return cloneSharedServerMetadata({ ...metadata, - availableModels: probes.models.status === "ok" ? metadata.availableModels : (models ?? previous?.availableModels ?? []), availableSkills: probes.skills.status === "ok" ? metadata.availableSkills : (previous?.availableSkills ?? []), availablePermissionProfiles: probes.permissionProfiles.status === "ok" ? metadata.availablePermissionProfiles : (previous?.availablePermissionProfiles ?? []), diff --git a/src/app-server/query/snapshots.ts b/src/app-server/query/snapshots.ts index 738950c1..cbb5530b 100644 --- a/src/app-server/query/snapshots.ts +++ b/src/app-server/query/snapshots.ts @@ -25,7 +25,6 @@ export function cloneSharedServerMetadata(metadata: SharedServerMetadata): Share ...metadata, runtimeConfig: metadata.runtimeConfig ? cloneRuntimeConfigSnapshot(metadata.runtimeConfig) : null, rateLimit: metadata.rateLimit ? cloneRateLimitSnapshot(metadata.rateLimit) : null, - availableModels: cloneModelMetadata(metadata.availableModels), availableSkills: metadata.availableSkills.map((skill) => ({ ...skill })), availablePermissionProfiles: metadata.availablePermissionProfiles.map((profile) => ({ ...profile })), serverDiagnostics: { diff --git a/src/domain/server/metadata.ts b/src/domain/server/metadata.ts index b20d60ca..44e640a7 100644 --- a/src/domain/server/metadata.ts +++ b/src/domain/server/metadata.ts @@ -1,4 +1,4 @@ -import type { ModelMetadata, SkillMetadata } from "../catalog/metadata"; +import type { SkillMetadata } from "../catalog/metadata"; import type { RuntimeConfigSnapshot } from "../runtime/config"; import type { RateLimitSnapshot } from "../runtime/metrics"; import type { RuntimePermissionProfileSummary } from "../runtime/permissions"; @@ -6,7 +6,6 @@ import type { Diagnostics } from "./diagnostics"; export interface SharedServerMetadata { runtimeConfig: RuntimeConfigSnapshot | null; - availableModels: readonly ModelMetadata[]; availableSkills: readonly SkillMetadata[]; availablePermissionProfiles: readonly RuntimePermissionProfileSummary[]; rateLimit: RateLimitSnapshot | null; diff --git a/src/features/chat/application/connection/server-metadata-actions.ts b/src/features/chat/application/connection/server-metadata-actions.ts index c0a8a67b..00e7aa34 100644 --- a/src/features/chat/application/connection/server-metadata-actions.ts +++ b/src/features/chat/application/connection/server-metadata-actions.ts @@ -66,7 +66,6 @@ function applyAppServerMetadata(host: ServerMetadataActionsHost, metadata: Share host.stateStore.dispatch({ type: "connection/metadata-applied", runtimeConfig: metadata.runtimeConfig, - availableModels: metadata.availableModels, availableSkills: metadata.availableSkills, availablePermissionProfiles: metadata.availablePermissionProfiles, rateLimit: metadata.rateLimit, diff --git a/tests/app-server/query-cache.test.ts b/tests/app-server/query-cache.test.ts index 018389da..1a286b39 100644 --- a/tests/app-server/query-cache.test.ts +++ b/tests/app-server/query-cache.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import type { CatalogModel, CatalogSkillMetadata } from "../../src/app-server/protocol/catalog"; import { AppServerQueryCache } from "../../src/app-server/query/cache"; import type { AppServerQueryContext } from "../../src/app-server/query/keys"; -import type { ModelMetadata, SkillMetadata } from "../../src/domain/catalog/metadata"; +import type { SkillMetadata } from "../../src/domain/catalog/metadata"; import { emptyRuntimeConfigSnapshot, type RuntimeConfigSnapshot } from "../../src/domain/runtime/config"; import type { RateLimitSnapshot } from "../../src/domain/runtime/metrics"; import type { RuntimePermissionProfileSummary } from "../../src/domain/runtime/permissions"; @@ -17,7 +17,7 @@ import { import type { SharedServerMetadata } from "../../src/domain/server/metadata"; describe("AppServerQueryCache", () => { - it("preserves successful resource values when metadata probes fail", () => { + it("preserves successful metadata resource values when probes fail", () => { const cache = new AppServerQueryCache(); const context = cacheContext(); const goodMetadata = metadata({ @@ -27,12 +27,9 @@ describe("AppServerQueryCache", () => { }); cache.writeAppServerMetadata(context, goodMetadata); - expect(cache.appServerMetadataSnapshot(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.5"]); - cache.writeAppServerMetadata( context, metadata({ - availableModels: [modelMetadata("gpt-5.6")], availableSkills: [skillMetadata("failed-skill")], availablePermissionProfiles: [permissionProfile(":failed")], rateLimit: rateLimit(90), @@ -42,15 +39,13 @@ describe("AppServerQueryCache", () => { }), ); - expect(cache.appServerMetadataSnapshot(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.6"]); expect(cache.appServerMetadataSnapshot(context)?.availableSkills.map((skill) => skill.name)).toEqual(["writer"]); expect(cache.appServerMetadataSnapshot(context)?.availablePermissionProfiles.map((profile) => profile.id)).toEqual([":workspace"]); expect(cache.appServerMetadataSnapshot(context)?.rateLimit?.primary?.usedPercent).toBe(42); expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes.skills.status).toBe("failed"); expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes.rateLimits.status).toBe("failed"); - cache.writeAppServerMetadata(context, metadata({ availableModels: [], modelProbeStatus: "failed" })); - expect(cache.appServerMetadataSnapshot(context)?.availableModels.map((model) => model.model)).toEqual(["gpt-5.6"]); + cache.writeAppServerMetadata(context, metadata({ modelProbeStatus: "failed" })); expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes.models.status).toBe("failed"); }); @@ -61,7 +56,6 @@ describe("AppServerQueryCache", () => { cache.writeAppServerMetadata( context, metadata({ - availableModels: [modelMetadata("gpt-previous")], availableSkills: [skillMetadata("previous-skill")], availablePermissionProfiles: [permissionProfile(":previous")], rateLimit: rateLimit(11), @@ -71,7 +65,6 @@ describe("AppServerQueryCache", () => { cache.writeAppServerMetadata( context, metadata({ - availableModels: [modelMetadata("gpt-next")], availableSkills: [skillMetadata("next-skill")], availablePermissionProfiles: [permissionProfile(":next")], rateLimit: rateLimit(22), @@ -84,14 +77,6 @@ describe("AppServerQueryCache", () => { const snapshot = cache.appServerMetadataSnapshot(context); const statusCase = metadataProbeStatusCase(statuses); - expect( - snapshot?.availableModels.map((model) => model.model), - statusCase, - ).toEqual([statuses.models === "ok" ? "gpt-next" : "gpt-previous"]); - expect( - cache.modelsSnapshot(context)?.map((model) => model.model), - statusCase, - ).toEqual([statuses.models === "ok" ? "gpt-next" : "gpt-previous"]); expect( snapshot?.availableSkills.map((skill) => skill.name), statusCase, @@ -111,7 +96,6 @@ describe("AppServerQueryCache", () => { cache.writeAppServerMetadata( context, metadata({ - availableModels: [modelMetadata("failed-model")], availableSkills: [skillMetadata("writer")], availablePermissionProfiles: [permissionProfile(":workspace")], rateLimit: rateLimit(90), @@ -125,7 +109,6 @@ describe("AppServerQueryCache", () => { const cached = cache.appServerMetadataSnapshot(context); expect(cached?.runtimeConfig).not.toBeNull(); expect(cached?.serverDiagnostics.probes.models.status).toBe("failed"); - expect(cached?.availableModels).toEqual([]); expect(cached?.availableSkills).toEqual([]); expect(cached?.availablePermissionProfiles).toEqual([]); expect(cached?.rateLimit).toBeNull(); @@ -186,15 +169,16 @@ describe("AppServerQueryCache", () => { expect(cache.modelsSnapshot(context)).toBeNull(); }); - it("stores successful empty model snapshots as shared cache truth", () => { - const cache = new AppServerQueryCache(); + it("does not let metadata writes overwrite the model query", async () => { + const cache = cacheWithRequestHandlers({ + "model/list": vi.fn().mockResolvedValue({ data: [catalogModel("gpt-model-query")] }), + }); const context = cacheContext(); - cache.writeAppServerMetadata(context, metadata({ availableModels: [modelMetadata("gpt-5.6")] })); - cache.writeAppServerMetadata(context, metadata({ availableModels: [] })); + await cache.fetchModels(context); + cache.writeAppServerMetadata(context, metadata({ modelProbeStatus: "failed" })); - expect(cache.appServerMetadataSnapshot(context)?.availableModels).toEqual([]); - expect(cache.modelsSnapshot(context)).toEqual([]); + expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-model-query"]); }); it("does not reuse metadata or model snapshots across app-server cache contexts", () => { @@ -279,7 +263,7 @@ describe("AppServerQueryCache", () => { expect(cache.activeThreadsSnapshot(context)).toBeNull(); }); - it("fetches app-server metadata through the query cache and syncs model snapshots", async () => { + it("fetches app-server metadata and models through their respective query records", async () => { const context = cacheContext(); const cache = cacheWithRequestHandlers({ "config/read": vi.fn().mockResolvedValue({}), @@ -291,7 +275,6 @@ describe("AppServerQueryCache", () => { const metadata = await cache.refreshAppServerMetadata(context); - expect(metadata?.availableModels.map((model) => model.model)).toEqual(["gpt-meta"]); expect(metadata?.availableSkills.map((skill) => skill.name)).toEqual(["writer"]); expect(metadata?.availablePermissionProfiles.map((profile) => profile.id)).toEqual([":workspace"]); expect(metadata?.rateLimit?.primary?.usedPercent).toBe(64); @@ -321,36 +304,41 @@ describe("AppServerQueryCache", () => { modelRefresh.resolve({ data: [catalogModel("gpt-shared")] }); await expect(modelsPromise).resolves.toMatchObject([{ model: "gpt-shared" }]); - await expect(metadataPromise).resolves.toMatchObject({ - availableModels: [{ model: "gpt-shared" }], - }); + await expect(metadataPromise).resolves.toMatchObject({ serverDiagnostics: { probes: { models: { status: "ok" } } } }); expect(listModels).toHaveBeenCalledOnce(); expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-shared"]); }); it("keeps query-cached models when app-server metadata model refresh fails", async () => { const context = cacheContext(); + const listModels = vi + .fn() + .mockResolvedValueOnce({ data: [catalogModel("gpt-cached")] }) + .mockRejectedValueOnce(new Error("offline")); const cache = cacheWithRequestHandlers({ "config/read": vi.fn().mockResolvedValue({}), - "model/list": vi.fn().mockRejectedValue(new Error("offline")), + "model/list": listModels, "skills/list": vi.fn().mockResolvedValue({ data: [{ skills: [] }] }), "permissionProfile/list": vi.fn().mockResolvedValue({ data: [], nextCursor: null }), "account/rateLimits/read": vi.fn().mockResolvedValue({ rateLimits: appServerRateLimit(0), rateLimitsByLimitId: null }), }); - cache.writeAppServerMetadata(context, metadata({ availableModels: [modelMetadata("gpt-cached")] })); + await cache.fetchModels(context); const metadataSnapshot = await cache.refreshAppServerMetadata(context); - expect(metadataSnapshot?.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]); expect(metadataSnapshot?.serverDiagnostics.probes.models.status).toBe("failed"); expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-cached"]); }); it("keeps every last-known-good resource through the full metadata refresh path", async () => { const context = cacheContext(); + const listModels = vi + .fn() + .mockResolvedValueOnce({ data: [catalogModel("gpt-cached")] }) + .mockRejectedValueOnce(new Error("models offline")); const cache = cacheWithRequestHandlers({ "config/read": vi.fn().mockResolvedValue({}), - "model/list": vi.fn().mockRejectedValue(new Error("models offline")), + "model/list": listModels, "skills/list": vi.fn().mockRejectedValue(new Error("skills offline")), "permissionProfile/list": vi.fn().mockRejectedValue(new Error("profiles offline")), "account/rateLimits/read": vi.fn().mockRejectedValue(new Error("limits offline")), @@ -358,16 +346,16 @@ describe("AppServerQueryCache", () => { cache.writeAppServerMetadata( context, metadata({ - availableModels: [modelMetadata("gpt-cached")], availableSkills: [skillMetadata("cached-skill")], availablePermissionProfiles: [permissionProfile(":cached")], rateLimit: rateLimit(17), }), ); + await cache.fetchModels(context); const refreshed = await cache.refreshAppServerMetadata(context); - expect(refreshed?.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]); + expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-cached"]); expect(refreshed?.availableSkills.map((skill) => skill.name)).toEqual(["cached-skill"]); expect(refreshed?.availablePermissionProfiles.map((profile) => profile.id)).toEqual([":cached"]); expect(refreshed?.rateLimit?.primary?.usedPercent).toBe(17); @@ -455,7 +443,6 @@ function cacheWithRequestHandlers(handlers: Record function metadata( overrides: { - availableModels?: readonly ModelMetadata[]; availableSkills?: readonly SkillMetadata[]; availablePermissionProfiles?: readonly RuntimePermissionProfileSummary[]; rateLimit?: RateLimitSnapshot | null; @@ -493,7 +480,6 @@ function metadata( ); return { runtimeConfig: overrides.runtimeConfig ?? emptyRuntimeConfigSnapshot(), - availableModels: overrides.availableModels ?? [modelMetadata("gpt-5.5")], availableSkills: overrides.availableSkills ?? [], availablePermissionProfiles: overrides.availablePermissionProfiles ?? [], rateLimit: overrides.rateLimit ?? null, @@ -542,23 +528,6 @@ function rateLimit(usedPercent: number): RateLimitSnapshot { }; } -function modelMetadata(model: string): ModelMetadata { - return { - id: model, - model, - displayName: model, - description: "", - hidden: false, - supportedReasoningEfforts: [], - defaultReasoningEffort: "medium", - inputModalities: [], - additionalSpeedTiers: [], - serviceTiers: [], - defaultServiceTier: null, - isDefault: false, - }; -} - function catalogModel(model: string): CatalogModel { return { id: model, diff --git a/tests/app-server/shared-queries.test.ts b/tests/app-server/shared-queries.test.ts index 7f83e507..822f368a 100644 --- a/tests/app-server/shared-queries.test.ts +++ b/tests/app-server/shared-queries.test.ts @@ -39,7 +39,7 @@ describe("AppServerSharedQueries", () => { const refresh = queries.refreshAppServerMetadata(); context.vaultPath = "/other-vault"; - pending.resolve(serverMetadata({ availableModels: [model("stale-model")] })); + pending.resolve(serverMetadata()); await expect(refresh).rejects.toBeInstanceOf(StaleAppServerSharedQueryContextError); }); @@ -106,14 +106,15 @@ describe("AppServerSharedQueries", () => { }); it("publishes metadata and model snapshots to shared query observers", () => { - const metadata = serverMetadata({ availableModels: [model("gpt-test")] }); + const metadata = serverMetadata(); + const models = [model("gpt-test")]; const metadataListener = vi.fn(); const modelListener = vi.fn(); const queries = new AppServerSharedQueries({ cache: cacheWith({ appServerMetadataSnapshot: () => metadata, updateAppServerMetadata: () => metadata, - modelsSnapshot: () => metadata.availableModels, + modelsSnapshot: () => models, observeAppServerMetadataResult: (_context, listener) => { metadataObserver = listener; return () => undefined; @@ -132,12 +133,12 @@ describe("AppServerSharedQueries", () => { queries.observeModelsResult(modelListener); queries.updateAppServerMetadata(() => metadata); metadataObserver(observedResult(metadata)); - modelObserver(observedResult(metadata.availableModels)); + modelObserver(observedResult(models)); expect(queries.appServerMetadataSnapshot()).toEqual(metadata); - expect(queries.modelsSnapshot()).toEqual(metadata.availableModels); + expect(queries.modelsSnapshot()).toEqual(models); expect(metadataListener).toHaveBeenLastCalledWith(expect.objectContaining({ value: metadata })); - expect(modelListener).toHaveBeenCalledWith(expect.objectContaining({ value: metadata.availableModels })); + expect(modelListener).toHaveBeenCalledWith(expect.objectContaining({ value: models })); }); }); @@ -197,7 +198,6 @@ function serverMetadata(overrides: Partial = {}): SharedSe const diagnostics = diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "0 models", 1)); return { runtimeConfig: null, - availableModels: [], availableSkills: [], availablePermissionProfiles: [], rateLimit: null, diff --git a/tests/features/chat/application/connection/server-actions.test.ts b/tests/features/chat/application/connection/server-actions.test.ts index fe6f861c..fc11c102 100644 --- a/tests/features/chat/application/connection/server-actions.test.ts +++ b/tests/features/chat/application/connection/server-actions.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import type { ModelMetadata, SkillMetadata } from "../../../../../src/domain/catalog/metadata"; +import type { SkillMetadata } from "../../../../../src/domain/catalog/metadata"; import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config"; import type { RateLimitSnapshot } from "../../../../../src/domain/runtime/metrics"; import { @@ -24,9 +24,9 @@ import { deferred } from "../../../../support/async"; import { chatStateFixture, chatStateWith } from "../../support/state"; describe("server metadata actions", () => { - it("applies refreshed app-server metadata", async () => { + it("applies refreshed non-model app-server metadata", async () => { const stateStore = createChatStateStore(chatStateFixture()); - const metadata = serverMetadataFixture({ availableModels: [modelFixture("gpt-5.1")] }); + const metadata = serverMetadataFixture({ availableSkills: [skillFixture("writer")] }); const actions = createServerMetadataActions({ stateStore, metadataResourceTransport: metadataResourceTransport(), @@ -37,7 +37,7 @@ describe("server metadata actions", () => { await actions.refreshAppServerMetadata(); - expect(stateStore.getState().connection.availableModels.map((model) => model.model)).toEqual(["gpt-5.1"]); + expect(stateStore.getState().connection.availableSkills.map((skill) => skill.name)).toEqual(["writer"]); }); it("preserves panel-local tool diagnostics when applying shared metadata", async () => { @@ -208,7 +208,6 @@ describe("server diagnostics actions", () => { it("reuses refreshed app-server metadata for deferred diagnostics", async () => { const stateStore = createChatStateStore(chatStateFixture()); const refreshedMetadata = serverMetadataFixture({ - availableModels: [modelFixture("gpt-5.1")], availableSkills: [skillFixture("writer")], serverDiagnostics: diagnosticsWithProbe( diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 models", 1)), @@ -458,23 +457,6 @@ function toolInventory(): ToolInventorySnapshot { }; } -function modelFixture(model: string): ModelMetadata { - return { - id: model, - model, - displayName: model, - description: "", - hidden: false, - supportedReasoningEfforts: [], - defaultReasoningEffort: "medium", - inputModalities: ["text"], - additionalSpeedTiers: [], - serviceTiers: [], - defaultServiceTier: null, - isDefault: false, - }; -} - function skillFixture(name: string): SkillMetadata { return { name, @@ -499,7 +481,6 @@ function rateLimitFixture(overrides: Partial = {}): RateLimit function serverMetadataFixture(overrides: Partial = {}): SharedServerMetadata { return { runtimeConfig: emptyRuntimeConfigSnapshot(), - availableModels: [], availableSkills: [], availablePermissionProfiles: [], rateLimit: null, diff --git a/tests/features/chat/host/view-connection.test.ts b/tests/features/chat/host/view-connection.test.ts index 08a8889c..bf7f831b 100644 --- a/tests/features/chat/host/view-connection.test.ts +++ b/tests/features/chat/host/view-connection.test.ts @@ -456,7 +456,6 @@ describe("CodexChatView connection lifecycle", () => { () => ({ runtimeConfig: { ...emptyRuntimeConfigSnapshot(), model: "gpt-cached" }, - availableModels: [], availableSkills: [{ name: "writer", enabled: true }], availablePermissionProfiles: [], rateLimit: null, @@ -1026,7 +1025,7 @@ interface ChatHostFixtureOverrides { function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost { let activeThreads = overrides.activeSnapshot?.() ?? null; let metadata = overrides.appServerMetadataSnapshot?.() ?? null; - const models = overrides.modelsSnapshot?.() ?? null; + let models = overrides.modelsSnapshot?.() ?? null; const activeThreadResultListeners = new Set<(result: ObservedResult) => void>(); const metadataResultListeners = new Set<(result: ObservedResult) => void>(); const modelResultListeners = new Set<(result: ObservedResult) => void>(); @@ -1040,7 +1039,6 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost { const applyMetadataToCache = (nextMetadata: SharedServerMetadata): SharedServerMetadata => { metadata = nextMetadata; for (const listener of metadataResultListeners) listener(queryResult(nextMetadata)); - for (const listener of modelResultListeners) listener(queryResult(nextMetadata.availableModels)); return nextMetadata; }; const loadAppServerMetadata = async (): Promise => { @@ -1049,16 +1047,18 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost { const connectionStillCurrent = () => connectionMock.state.client === client && connectionMock.state.connected; await client.request("config/read", { cwd: vaultPath, includeLayers: true }); if (!connectionStillCurrent()) return null; - let availableModels: readonly ModelMetadata[]; + let fetchedModels: readonly ModelMetadata[]; if (overrides.fetchModels) { - availableModels = await overrides.fetchModels(); + fetchedModels = await overrides.fetchModels(); } else { const modelsResponse = (await client.request("model/list", { includeHidden: false, limit: 100 })) as { data: Parameters[0]; }; - availableModels = modelMetadataFromCatalogModels(modelsResponse.data); + fetchedModels = modelMetadataFromCatalogModels(modelsResponse.data); } if (!connectionStillCurrent()) return null; + models = fetchedModels; + for (const listener of modelResultListeners) listener(queryResult(fetchedModels)); const skillsResponse = (await client.request("skills/list", { cwds: [vaultPath], forceReload: false })) as { data: { skills: { name: string; description?: string; path?: string; enabled?: boolean }[] }[]; }; @@ -1072,7 +1072,6 @@ function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexChatHost { if (!connectionStillCurrent()) return null; return { runtimeConfig: emptyRuntimeConfigSnapshot(), - availableModels, availableSkills: skillsResponse.data.flatMap( (entry: { skills: { name: string; description?: string; path?: string; enabled?: boolean }[] }) => entry.skills.map((skill) => ({