Use panel-owned diagnostic probe ids

This commit is contained in:
murashit 2026-07-01 07:43:35 +09:00
parent 7be9a1de24
commit 9842ce38c4
12 changed files with 111 additions and 120 deletions

View file

@ -192,12 +192,12 @@ export class AppServerQueryCache {
const probes = metadata.serverDiagnostics.probes;
const next = cloneSharedServerMetadata({
...metadata,
availableModels: probes["model/list"].status === "ok" ? metadata.availableModels : (this.modelsSnapshot(context) ?? []),
availableSkills: probes["skills/list"].status === "ok" ? metadata.availableSkills : (previous?.availableSkills ?? []),
rateLimit: probes["account/rateLimits/read"].status === "ok" ? metadata.rateLimit : (previous?.rateLimit ?? null),
availableModels: probes.models.status === "ok" ? metadata.availableModels : (this.modelsSnapshot(context) ?? []),
availableSkills: probes.skills.status === "ok" ? metadata.availableSkills : (previous?.availableSkills ?? []),
rateLimit: probes.rateLimits.status === "ok" ? metadata.rateLimit : (previous?.rateLimit ?? null),
});
this.client.setQueryData(appServerMetadataQueryKey(context), cloneSharedServerMetadata(next));
if (probes["model/list"].status === "ok") {
if (probes.models.status === "ok") {
this.client.setQueryData(appServerModelsQueryKey(context), cloneModelMetadata(next.availableModels));
}
return cloneSharedServerMetadata(next);
@ -315,15 +315,15 @@ export class AppServerQueryCache {
client: AppServerClient,
): Promise<{
value: readonly ModelMetadata[];
probe: SharedServerMetadata["serverDiagnostics"]["probes"]["model/list"];
probe: SharedServerMetadata["serverDiagnostics"]["probes"]["models"];
}> {
try {
const models = cloneModelMetadata(await this.client.fetchQuery(this.modelsQueryOptionsWithClient(context, client)));
return { value: models, probe: diagnosticProbeOk("model/list", `${String(models.length)} models`, Date.now()) };
return { value: models, probe: diagnosticProbeOk("models", `${String(models.length)} models`, Date.now()) };
} catch (error) {
return {
value: this.modelsSnapshot(context) ?? [],
probe: diagnosticProbeError("model/list", error, Date.now()),
probe: diagnosticProbeError("models", error, Date.now()),
};
}
}

View file

@ -11,8 +11,8 @@ interface MetadataProbeResult<T, K extends keyof Diagnostics["probes"]> {
probe: Diagnostics["probes"][K];
}
export type SkillMetadataProbeResult = MetadataProbeResult<SkillMetadata[], "skills/list">;
export type RateLimitMetadataProbeResult = MetadataProbeResult<RateLimitSnapshot | null, "account/rateLimits/read">;
export type SkillMetadataProbeResult = MetadataProbeResult<SkillMetadata[], "skills">;
export type RateLimitMetadataProbeResult = MetadataProbeResult<RateLimitSnapshot | null, "rateLimits">;
export async function readSkillMetadataProbe(
client: AppServerRequestClient | null,
@ -20,13 +20,13 @@ export async function readSkillMetadataProbe(
forceReload = false,
): Promise<SkillMetadataProbeResult> {
if (!client) {
return { value: [], probe: diagnosticProbeError("skills/list", new Error("Codex app-server is not connected."), Date.now()) };
return { value: [], probe: diagnosticProbeError("skills", new Error("Codex app-server is not connected."), Date.now()) };
}
try {
const catalog = await listSkillCatalog(client, vaultPath, { forceReload });
return { value: catalog.skills, probe: diagnosticProbeOk("skills/list", `${String(catalog.totalCount)} skills`, Date.now()) };
return { value: catalog.skills, probe: diagnosticProbeOk("skills", `${String(catalog.totalCount)} skills`, Date.now()) };
} catch (error) {
return { value: [], probe: diagnosticProbeError("skills/list", error, Date.now()) };
return { value: [], probe: diagnosticProbeError("skills", error, Date.now()) };
}
}
@ -34,16 +34,16 @@ export async function readRateLimitMetadataProbe(client: AppServerRequestClient
if (!client) {
return {
value: null,
probe: diagnosticProbeError("account/rateLimits/read", new Error("Codex app-server is not connected."), Date.now()),
probe: diagnosticProbeError("rateLimits", new Error("Codex app-server is not connected."), Date.now()),
};
}
try {
const response = await readAccountRateLimits(client);
return {
value: rateLimitSnapshotFromAccountRateLimitsResponse(response),
probe: diagnosticProbeOk("account/rateLimits/read", accountRateLimitsSummaryFromResponse(response), Date.now()),
probe: diagnosticProbeOk("rateLimits", accountRateLimitsSummaryFromResponse(response), Date.now()),
};
} catch (error) {
return { value: null, probe: diagnosticProbeError("account/rateLimits/read", error, Date.now()) };
return { value: null, probe: diagnosticProbeError("rateLimits", error, Date.now()) };
}
}

View file

@ -65,7 +65,7 @@ function readCachedSkills(
return {
items: [...skills],
error: null,
probe: probe ?? diagnosticProbeOk("skills/list", `${String(skills.length)} skills`, checkedAt),
probe: probe ?? diagnosticProbeOk("skills", `${String(skills.length)} skills`, checkedAt),
};
}
@ -86,14 +86,14 @@ async function readPlugins(
items: plugins,
marketplaceErrors,
error: null,
probe: diagnosticProbeOk("plugin/installed", `${String(plugins.length)} plugins`, checkedAt),
probe: diagnosticProbeOk("plugins", `${String(plugins.length)} plugins`, checkedAt),
};
} catch (error) {
return {
items: null,
marketplaceErrors: [],
error: shortErrorMessage(error),
probe: diagnosticProbeError("plugin/installed", error, checkedAt),
probe: diagnosticProbeError("plugins", error, checkedAt),
};
}
}
@ -113,13 +113,13 @@ async function readMcpServers(
return {
items: servers,
error: null,
probe: diagnosticProbeOk("mcpServerStatus/list", mcpSummary(servers), checkedAt),
probe: diagnosticProbeOk("mcpServers", mcpSummary(servers), checkedAt),
};
} catch (error) {
return {
items: null,
error: shortErrorMessage(error),
probe: diagnosticProbeError("mcpServerStatus/list", error, checkedAt),
probe: diagnosticProbeError("mcpServers", error, checkedAt),
};
}
}
@ -139,10 +139,10 @@ async function readSkills(
return {
items: catalog.skills,
error: null,
probe: diagnosticProbeOk("skills/list", `${String(catalog.totalCount)} skills`, checkedAt),
probe: diagnosticProbeOk("skills", `${String(catalog.totalCount)} skills`, checkedAt),
};
} catch (error) {
return { items: null, error: shortErrorMessage(error), probe: diagnosticProbeError("skills/list", error, checkedAt) };
return { items: null, error: shortErrorMessage(error), probe: diagnosticProbeError("skills", error, checkedAt) };
}
}

View file

@ -2,20 +2,20 @@ import type { ServerInitialization } from "./initialization";
import { type McpServerDiagnostic, type McpServerStatusSummary, mcpServerDiagnosticFromStatus } from "./mcp-status";
import { cloneToolInventorySnapshot, type ToolInventorySnapshot } from "./tool-inventory";
const DIAGNOSTIC_PROBE_METHODS = [
"model/list",
"skills/list",
"app/list",
"plugin/installed",
"account/rateLimits/read",
"mcpServerStatus/list",
] as const;
const DIAGNOSTIC_PROBE_DEFINITIONS = {
models: { label: "Models" },
skills: { label: "Skills" },
apps: { label: "Apps" },
plugins: { label: "Plugins" },
rateLimits: { label: "Rate limits" },
mcpServers: { label: "MCP servers" },
} as const;
export type DiagnosticProbeMethod = (typeof DIAGNOSTIC_PROBE_METHODS)[number];
export type DiagnosticProbeId = keyof typeof DIAGNOSTIC_PROBE_DEFINITIONS;
type DiagnosticProbeStatus = "unknown" | "ok" | "failed";
export interface DiagnosticProbeResult {
readonly method: DiagnosticProbeMethod;
readonly id: DiagnosticProbeId;
readonly status: DiagnosticProbeStatus;
readonly message: string | null;
readonly summary: string | null;
@ -23,17 +23,16 @@ export interface DiagnosticProbeResult {
}
export interface Diagnostics {
readonly probes: Readonly<Record<DiagnosticProbeMethod, DiagnosticProbeResult>>;
readonly probes: Readonly<Record<DiagnosticProbeId, DiagnosticProbeResult>>;
readonly mcpServers: readonly McpServerDiagnostic[];
readonly toolInventory: ToolInventorySnapshot | null;
}
export function createServerDiagnostics(): Diagnostics {
return {
probes: Object.fromEntries(DIAGNOSTIC_PROBE_METHODS.map((method) => [method, createDiagnosticProbeResult(method)])) as Record<
DiagnosticProbeMethod,
DiagnosticProbeResult
>,
probes: Object.fromEntries(
Object.keys(DIAGNOSTIC_PROBE_DEFINITIONS).map((id) => [id, createDiagnosticProbeResult(id as DiagnosticProbeId)]),
) as Record<DiagnosticProbeId, DiagnosticProbeResult>,
mcpServers: [],
toolInventory: null,
};
@ -52,7 +51,7 @@ export function diagnosticsWithProbe(diagnostics: Diagnostics, probe: Diagnostic
...diagnostics,
probes: {
...diagnostics.probes,
[probe.method]: probe,
[probe.id]: probe,
},
};
}
@ -64,9 +63,9 @@ export function diagnosticsWithToolInventory(diagnostics: Diagnostics, toolInven
};
}
function createDiagnosticProbeResult(method: DiagnosticProbeMethod): DiagnosticProbeResult {
function createDiagnosticProbeResult(id: DiagnosticProbeId): DiagnosticProbeResult {
return {
method,
id,
status: "unknown",
message: null,
summary: null,
@ -74,9 +73,9 @@ function createDiagnosticProbeResult(method: DiagnosticProbeMethod): DiagnosticP
};
}
export function diagnosticProbeOk(method: DiagnosticProbeMethod, summary: string | null, checkedAt: number): DiagnosticProbeResult {
export function diagnosticProbeOk(id: DiagnosticProbeId, summary: string | null, checkedAt: number): DiagnosticProbeResult {
return {
method,
id,
status: "ok",
message: null,
summary,
@ -84,9 +83,9 @@ export function diagnosticProbeOk(method: DiagnosticProbeMethod, summary: string
};
}
export function diagnosticProbeError(method: DiagnosticProbeMethod, error: unknown, checkedAt: number): DiagnosticProbeResult {
export function diagnosticProbeError(id: DiagnosticProbeId, error: unknown, checkedAt: number): DiagnosticProbeResult {
return {
method,
id,
status: "failed",
message: shortErrorMessage(error),
summary: null,
@ -94,6 +93,10 @@ export function diagnosticProbeError(method: DiagnosticProbeMethod, error: unkno
};
}
export function diagnosticProbeLabel(id: DiagnosticProbeId): string {
return DIAGNOSTIC_PROBE_DEFINITIONS[id].label;
}
export function serverIdentity(initializeResponse: ServerInitialization | null): string {
return initializeResponse?.userAgent ?? "(not connected)";
}

View file

@ -4,8 +4,8 @@ import { listModelMetadata } from "../../../../app-server/services/catalog";
import { readToolInventory } from "../../../../app-server/services/tool-inventory";
import {
cloneServerDiagnostics,
type DiagnosticProbeMethod,
type Diagnostics,
type DiagnosticProbeId,
type DiagnosticProbeResult,
diagnosticProbeError,
diagnosticProbeOk,
diagnosticsWithProbe,
@ -21,7 +21,7 @@ interface RefreshServerDiagnosticsOptions {
}
interface DiagnosticProbeSnapshot {
probe: Diagnostics["probes"][DiagnosticProbeMethod];
probe: DiagnosticProbeResult;
}
export interface ChatServerDiagnosticsActionsHost extends ChatServerActionsHost {
@ -58,7 +58,7 @@ async function refreshServerDiagnostics(
const cachedSkillsProbe =
options.forceResourceProbes === true
? undefined
: (metadataSnapshot?.serverDiagnostics.probes["skills/list"] ?? state.connection.serverDiagnostics.probes["skills/list"]);
: (metadataSnapshot?.serverDiagnostics.probes.skills ?? state.connection.serverDiagnostics.probes.skills);
const toolInventory = readToolInventory(client, host.vaultPath, {
threadId: activeThreadId,
mcpDiagnostics: initialDiagnostics.mcpServers,
@ -69,7 +69,7 @@ async function refreshServerDiagnostics(
if (options.forceResourceProbes === true && options.appServerMetadataSnapshot !== true) {
probes.push(
probeDiagnostic(
"model/list",
"models",
() => listModelMetadata(client),
(models) => `${String(models.length)} models`,
),
@ -108,16 +108,16 @@ function currentMetadataDiagnostics(host: ChatServerDiagnosticsActionsHost): Sha
}
async function probeDiagnostic<T>(
method: DiagnosticProbeMethod,
id: DiagnosticProbeId,
request: () => Promise<T>,
summarize: (response: T) => string | null,
): Promise<DiagnosticProbeSnapshot> {
try {
const response = await request();
return {
probe: diagnosticProbeOk(method, summarize(response), Date.now()),
probe: diagnosticProbeOk(id, summarize(response), Date.now()),
};
} catch (error) {
return { probe: diagnosticProbeError(method, error, Date.now()) };
return { probe: diagnosticProbeError(id, error, Date.now()) };
}
}

View file

@ -1,9 +1,9 @@
import { CLIENT_VERSION } from "../../../../constants";
import type { DiagnosticProbeResult, Diagnostics } from "../../../../domain/server/diagnostics";
import { type DiagnosticProbeMethod, serverIdentity, serverPlatform } from "../../../../domain/server/diagnostics";
import type { DiagnosticProbeId, DiagnosticProbeResult, Diagnostics } from "../../../../domain/server/diagnostics";
import { diagnosticProbeLabel, serverIdentity, serverPlatform } from "../../../../domain/server/diagnostics";
import type { ServerInitialization } from "../../../../domain/server/initialization";
const RUNTIME_CHECK_PROBE_METHODS: readonly DiagnosticProbeMethod[] = ["model/list", "account/rateLimits/read"];
const RUNTIME_CHECK_PROBE_IDS: readonly DiagnosticProbeId[] = ["models", "rateLimits"];
export interface DiagnosticRow {
label: string;
@ -38,7 +38,7 @@ export function appServerDiagnosticSections(input: AppServerDiagnosticSectionsIn
},
{
title: "Runtime Checks",
rows: RUNTIME_CHECK_PROBE_METHODS.map((method) => diagnosticProbeRow(input.diagnostics.probes[method])),
rows: RUNTIME_CHECK_PROBE_IDS.map((id) => diagnosticProbeRow(input.diagnostics.probes[id])),
},
];
}
@ -46,7 +46,7 @@ export function appServerDiagnosticSections(input: AppServerDiagnosticSectionsIn
function diagnosticProbeRow(probe: DiagnosticProbeResult): DiagnosticRow {
const detail = probe.message ? ` - ${probe.message}` : probe.summary ? ` (${probe.summary})` : "";
return {
label: probe.method,
label: diagnosticProbeLabel(probe.id),
value: `${probe.status}${detail}`,
level: probe.status === "failed" ? "error" : probe.status === "unknown" ? "warning" : "normal",
};

View file

@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
createServerDiagnostics,
diagnosticProbeError,
diagnosticProbeLabel,
diagnosticProbeOk,
serverIdentity,
serverPlatform,
@ -27,43 +28,37 @@ describe("app-server diagnostics", () => {
it("creates generic capability probe defaults", () => {
const diagnostics = createServerDiagnostics();
expect(Object.keys(diagnostics.probes)).toEqual([
"model/list",
"skills/list",
"app/list",
"plugin/installed",
"account/rateLimits/read",
"mcpServerStatus/list",
]);
expect(diagnostics.probes["model/list"]).toMatchObject({
method: "model/list",
expect(Object.keys(diagnostics.probes)).toEqual(["models", "skills", "apps", "plugins", "rateLimits", "mcpServers"]);
expect(diagnostics.probes.models).toMatchObject({
id: "models",
status: "unknown",
message: null,
summary: null,
checkedAt: null,
});
expect(diagnosticProbeLabel("models")).toBe("Models");
});
it("classifies ok and failed capability probes", () => {
expect(diagnosticProbeOk("skills/list", "3 skills", 123)).toEqual({
method: "skills/list",
expect(diagnosticProbeOk("skills", "3 skills", 123)).toEqual({
id: "skills",
status: "ok",
message: null,
summary: "3 skills",
checkedAt: 123,
});
expect(diagnosticProbeError("plugin/installed", new Error("boom"), 456)).toMatchObject({
method: "plugin/installed",
expect(diagnosticProbeError("plugins", new Error("boom"), 456)).toMatchObject({
id: "plugins",
status: "failed",
message: "boom",
checkedAt: 456,
});
expect(diagnosticProbeError("model/list", new Error("unknown provider failure"), 792).status).toBe("failed");
expect(diagnosticProbeError("models", new Error("unknown provider failure"), 792).status).toBe("failed");
});
it("shortens error messages and tracks MCP server diagnostics", () => {
expect(diagnosticProbeError("model/list", "a\n b\t c", 1).message).toBe("a b c");
expect(diagnosticProbeError("model/list", "x".repeat(200), 1).message).toHaveLength(160);
expect(diagnosticProbeError("models", "a\n b\t c", 1).message).toBe("a b c");
expect(diagnosticProbeError("models", "x".repeat(200), 1).message).toHaveLength(160);
let diagnostics = upsertMcpServerDiagnostic(createServerDiagnostics(), {
name: "github",

View file

@ -39,12 +39,12 @@ 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)?.rateLimit?.primary?.usedPercent).toBe(42);
expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes["skills/list"].status).toBe("failed");
expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes["account/rateLimits/read"].status).toBe("failed");
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"]);
expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes["model/list"].status).toBe("failed");
expect(cache.appServerMetadataSnapshot(context)?.serverDiagnostics.probes.models.status).toBe("failed");
});
it("does not accept failed metadata resource payloads as cache truth", () => {
@ -65,7 +65,7 @@ describe("AppServerQueryCache", () => {
const cached = cache.appServerMetadataSnapshot(context);
expect(cached?.runtimeConfig).not.toBeNull();
expect(cached?.serverDiagnostics.probes["model/list"].status).toBe("failed");
expect(cached?.serverDiagnostics.probes.models.status).toBe("failed");
expect(cached?.availableModels).toEqual([]);
expect(cached?.availableSkills).toEqual([]);
expect(cached?.rateLimit).toBeNull();
@ -192,7 +192,7 @@ describe("AppServerQueryCache", () => {
expect(metadata?.availableModels.map((model) => model.model)).toEqual(["gpt-meta"]);
expect(metadata?.availableSkills.map((skill) => skill.name)).toEqual(["writer"]);
expect(metadata?.rateLimit?.primary?.usedPercent).toBe(64);
expect(metadata?.serverDiagnostics.probes["model/list"].status).toBe("ok");
expect(metadata?.serverDiagnostics.probes.models.status).toBe("ok");
expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-meta"]);
});
@ -237,7 +237,7 @@ describe("AppServerQueryCache", () => {
const metadataSnapshot = await cache.refreshAppServerMetadata(context);
expect(metadataSnapshot?.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]);
expect(metadataSnapshot?.serverDiagnostics.probes["model/list"].status).toBe("failed");
expect(metadataSnapshot?.serverDiagnostics.probes.models.status).toBe("failed");
expect(cache.modelsSnapshot(context)?.map((model) => model.model)).toEqual(["gpt-cached"]);
});
@ -329,20 +329,20 @@ function metadata(
diagnostics = diagnosticsWithProbe(
diagnostics,
overrides.modelProbeStatus === "failed"
? diagnosticProbeError("model/list", new Error("offline"), 1)
: diagnosticProbeOk("model/list", "1 models", 1),
? diagnosticProbeError("models", new Error("offline"), 1)
: diagnosticProbeOk("models", "1 models", 1),
);
diagnostics = diagnosticsWithProbe(
diagnostics,
overrides.skillsProbeStatus === "failed"
? diagnosticProbeError("skills/list", new Error("offline"), 1)
: diagnosticProbeOk("skills/list", "0 skills", 1),
? diagnosticProbeError("skills", new Error("offline"), 1)
: diagnosticProbeOk("skills", "0 skills", 1),
);
diagnostics = diagnosticsWithProbe(
diagnostics,
overrides.rateLimitProbeStatus === "failed"
? diagnosticProbeError("account/rateLimits/read", new Error("offline"), 1)
: diagnosticProbeOk("account/rateLimits/read", "available", 1),
? diagnosticProbeError("rateLimits", new Error("offline"), 1)
: diagnosticProbeOk("rateLimits", "available", 1),
);
return {
runtimeConfig: overrides.runtimeConfig ?? emptyRuntimeConfigSnapshot(),

View file

@ -194,7 +194,7 @@ function model(modelId: string): ModelMetadata {
}
function serverMetadata(overrides: Partial<SharedServerMetadata> = {}): SharedServerMetadata {
const diagnostics = diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("model/list", "0 models", 1));
const diagnostics = diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "0 models", 1));
return {
runtimeConfig: null,
availableModels: [],

View file

@ -59,7 +59,7 @@ describe("tool inventory", () => {
const result = await readToolInventory(client, "/vault");
expect(client.request).not.toHaveBeenCalledWith("app/list", expect.anything());
expect(result.probes.some((probe) => probe.method === "app/list")).toBe(false);
expect(result.probes.some((probe) => probe.id === "apps")).toBe(false);
});
it("preserves plugin order from installed plugin summaries", async () => {

View file

@ -253,10 +253,10 @@ describe("chat app-server actions", () => {
rateLimit: rateLimitFixture(),
serverDiagnostics: diagnosticsWithProbe(
diagnosticsWithProbe(
diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("model/list", "1 models", 1)),
diagnosticProbeOk("skills/list", "1 skills", 1),
diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 models", 1)),
diagnosticProbeOk("skills", "1 skills", 1),
),
diagnosticProbeOk("account/rateLimits/read", "available", 1),
diagnosticProbeOk("rateLimits", "available", 1),
),
});
const refreshAppServerMetadata = vi.fn<() => Promise<SharedServerMetadata | null>>().mockResolvedValue(refreshedMetadata);
@ -289,15 +289,15 @@ describe("chat app-server actions", () => {
expect(refreshAppServerMetadata).toHaveBeenCalledOnce();
expect(listMcpServerStatus).toHaveBeenCalledWith({ detail: "toolsAndAuthOnly", limit: 100 });
expect(stateStore.getState().connection.serverDiagnostics.probes["model/list"]).toMatchObject({
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
status: "ok",
summary: "1 models",
});
expect(stateStore.getState().connection.serverDiagnostics.probes["skills/list"]).toMatchObject({
expect(stateStore.getState().connection.serverDiagnostics.probes.skills).toMatchObject({
status: "ok",
summary: "1 skills",
});
expect(stateStore.getState().connection.serverDiagnostics.probes["account/rateLimits/read"]).toMatchObject({
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits).toMatchObject({
status: "ok",
summary: "available",
});
@ -325,7 +325,7 @@ describe("chat app-server actions", () => {
const stateStore = createChatStateStore(chatStateFixture());
const metadataCache = metadataCacheHost({
current: serverMetadataFixture({
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("model/list", "cached models", 1)),
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "cached models", 1)),
}),
});
const listModels = vi.fn().mockResolvedValue({ data: [modelFixture("gpt-direct")] });
@ -350,7 +350,7 @@ describe("chat app-server actions", () => {
expect(listModels).not.toHaveBeenCalled();
expect(listSkills).not.toHaveBeenCalled();
expect(readAccountRateLimits).not.toHaveBeenCalled();
expect(stateStore.getState().connection.serverDiagnostics.probes["model/list"]).toMatchObject({
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
status: "ok",
summary: "cached models",
});
@ -380,7 +380,7 @@ describe("chat app-server actions", () => {
expect(listModels).toHaveBeenCalledWith({ includeHidden: false, limit: 100 });
expect(listSkills).toHaveBeenCalledWith({ cwds: ["/vault"], forceReload: false });
expect(readAccountRateLimits).toHaveBeenCalledWith(undefined);
expect(stateStore.getState().connection.serverDiagnostics.probes["model/list"]).toMatchObject({
expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({
status: "ok",
summary: "1 models",
});
@ -410,7 +410,7 @@ describe("chat app-server actions", () => {
await refreshing;
expect(listMcpServerStatus).toHaveBeenCalledWith({ detail: "toolsAndAuthOnly", limit: 100 });
expect(stateStore.getState().connection.serverDiagnostics.probes["mcpServerStatus/list"].status).toBe("unknown");
expect(stateStore.getState().connection.serverDiagnostics.probes.mcpServers.status).toBe("unknown");
expect(stateStore.getState().connection.serverDiagnostics.mcpServers).toEqual([]);
expect(updateAppServerMetadata).not.toHaveBeenCalled();
});
@ -438,7 +438,7 @@ describe("chat app-server actions", () => {
const stateStore = createChatStateStore(state);
const metadata = serverMetadataFixture({
availableModels: modelMetadataFromCatalogModels([modelFixture("gpt-cached")]),
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("model/list", new Error("offline"), 1)),
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("models", new Error("offline"), 1)),
});
const controller = createChatServerMetadataActions({
stateStore,
@ -451,7 +451,7 @@ describe("chat app-server actions", () => {
await controller.refreshAppServerMetadata();
expect(stateStore.getState().connection.availableModels.map((model) => model.model)).toEqual(["gpt-cached"]);
expect(stateStore.getState().connection.serverDiagnostics.probes["model/list"].status).toBe("failed");
expect(stateStore.getState().connection.serverDiagnostics.probes.models.status).toBe("failed");
});
it("does not use chat state as a second model source when metadata model refresh fails", async () => {
@ -460,7 +460,7 @@ describe("chat app-server actions", () => {
const stateStore = createChatStateStore(state);
const metadata = serverMetadataFixture({
availableModels: [],
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("model/list", new Error("offline"), 1)),
serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeError("models", new Error("offline"), 1)),
});
const controller = createChatServerMetadataActions({
stateStore,
@ -473,7 +473,7 @@ describe("chat app-server actions", () => {
await controller.refreshAppServerMetadata();
expect(stateStore.getState().connection.availableModels).toEqual([]);
expect(stateStore.getState().connection.serverDiagnostics.probes["model/list"].status).toBe("failed");
expect(stateStore.getState().connection.serverDiagnostics.probes.models.status).toBe("failed");
});
it("does not apply or publish refreshed skills after the client changes", async () => {
@ -500,7 +500,7 @@ describe("chat app-server actions", () => {
await refreshing;
expect(listSkills).toHaveBeenCalledWith({ cwds: ["/vault"], forceReload: true });
expect(stateStore.getState().connection.availableSkills).toEqual([]);
expect(stateStore.getState().connection.serverDiagnostics.probes["skills/list"].status).toBe("unknown");
expect(stateStore.getState().connection.serverDiagnostics.probes.skills.status).toBe("unknown");
expect(updateAppServerMetadata).not.toHaveBeenCalled();
});
@ -523,7 +523,7 @@ describe("chat app-server actions", () => {
expect(listSkills).toHaveBeenCalledWith({ cwds: ["/vault"], forceReload: true });
expect(stateStore.getState().connection.availableSkills).toEqual(previousSkills);
expect(stateStore.getState().connection.serverDiagnostics.probes["skills/list"]).toMatchObject({ status: "failed" });
expect(stateStore.getState().connection.serverDiagnostics.probes.skills).toMatchObject({ status: "failed" });
});
it("publishes refreshed rate limits from sparse update notifications", async () => {
@ -570,7 +570,7 @@ describe("chat app-server actions", () => {
await controller.applyAppServerResourceEvent({ type: "rate-limits-updated", preserveExistingOnFailure: true });
expect(stateStore.getState().connection.rateLimit).toBe(previousRateLimit);
expect(stateStore.getState().connection.serverDiagnostics.probes["account/rateLimits/read"]).toMatchObject({ status: "failed" });
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits).toMatchObject({ status: "failed" });
});
it("does not apply or publish sparse rate limit refreshes after the client changes", async () => {
@ -600,7 +600,7 @@ describe("chat app-server actions", () => {
await refreshing;
expect(stateStore.getState().connection.rateLimit).toBeNull();
expect(stateStore.getState().connection.serverDiagnostics.probes["account/rateLimits/read"].status).toBe("unknown");
expect(stateStore.getState().connection.serverDiagnostics.probes.rateLimits.status).toBe("unknown");
expect(updateAppServerMetadata).not.toHaveBeenCalled();
});

View file

@ -23,12 +23,9 @@ function diagnosticsWithToolInventory(inventory: ToolInventorySnapshot) {
describe("connection diagnostics", () => {
it("formats connection rows and runtime checks for /doctor", () => {
let diagnostics = createServerDiagnostics();
diagnostics = diagnosticsWithProbe(diagnostics, diagnosticProbeOk("model/list", "12 models", 1));
diagnostics = diagnosticsWithProbe(
diagnostics,
diagnosticProbeError("account/rateLimits/read", new Error("rate limit request failed"), 2),
);
diagnostics = diagnosticsWithProbe(diagnostics, diagnosticProbeError("skills/list", new Error("unknown method skills/list"), 3));
diagnostics = diagnosticsWithProbe(diagnostics, diagnosticProbeOk("models", "12 models", 1));
diagnostics = diagnosticsWithProbe(diagnostics, diagnosticProbeError("rateLimits", new Error("rate limit request failed"), 2));
diagnostics = diagnosticsWithProbe(diagnostics, diagnosticProbeError("skills", new Error("unknown method skills/list"), 3));
diagnostics = upsertMcpServerDiagnostic(diagnostics, {
name: "github",
startupStatus: "failed",
@ -59,15 +56,11 @@ describe("connection diagnostics", () => {
const rows = sections.flatMap((section) => section.rows);
expect(sections.map((section) => section.title)).toEqual(["Process", "Runtime Checks"]);
expect(rows.map((row) => `${row.label}: ${row.value}`)).toEqual(
expect.arrayContaining([
"connection: connected",
"model/list: ok (12 models)",
"account/rateLimits/read: failed - rate limit request failed",
]),
expect.arrayContaining(["connection: connected", "Models: ok (12 models)", "Rate limits: failed - rate limit request failed"]),
);
expect(rows.find((row) => row.label === "account/rateLimits/read")?.level).toBe("error");
expect(rows.find((row) => row.label === "skills/list")).toBeUndefined();
expect(rows.find((row) => row.label === "mcpServerStatus/list")).toBeUndefined();
expect(rows.find((row) => row.label === "Rate limits")?.level).toBe("error");
expect(rows.find((row) => row.label === "Skills")).toBeUndefined();
expect(rows.find((row) => row.label === "MCP servers")).toBeUndefined();
expect(rows.find((row) => row.label === "mcp github")).toBeUndefined();
});