mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Use model queries as the sole model metadata source
This commit is contained in:
parent
dccd63b75b
commit
c375f110a2
9 changed files with 52 additions and 120 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<SharedServerMetadata["serverDiagnostics"]["probes"]["models"]> {
|
||||
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 ?? []),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, (params: unknown) =>
|
|||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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<SharedServerMetadata> = {}): SharedSe
|
|||
const diagnostics = diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "0 models", 1));
|
||||
return {
|
||||
runtimeConfig: null,
|
||||
availableModels: [],
|
||||
availableSkills: [],
|
||||
availablePermissionProfiles: [],
|
||||
rateLimit: null,
|
||||
|
|
|
|||
|
|
@ -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<RateLimitSnapshot> = {}): RateLimit
|
|||
function serverMetadataFixture(overrides: Partial<SharedServerMetadata> = {}): SharedServerMetadata {
|
||||
return {
|
||||
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
||||
availableModels: [],
|
||||
availableSkills: [],
|
||||
availablePermissionProfiles: [],
|
||||
rateLimit: null,
|
||||
|
|
|
|||
|
|
@ -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<readonly Thread[]>) => void>();
|
||||
const metadataResultListeners = new Set<(result: ObservedResult<SharedServerMetadata>) => void>();
|
||||
const modelResultListeners = new Set<(result: ObservedResult<readonly ModelMetadata[]>) => 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<SharedServerMetadata | null> => {
|
||||
|
|
@ -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<typeof modelMetadataFromCatalogModels>[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) => ({
|
||||
|
|
|
|||
Loading…
Reference in a new issue