import { describe, expect, it, vi } from "vitest"; import type { SkillMetadata } from "../../../../../src/domain/catalog/metadata"; import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config"; import type { RateLimitSnapshot } from "../../../../../src/domain/runtime/metrics"; import { createServerDiagnostics, type DiagnosticProbeResult, diagnosticProbeOk, diagnosticsWithProbe, diagnosticsWithToolInventory, upsertMcpServerDiagnostic, } from "../../../../../src/domain/server/diagnostics"; import type { McpServerStatusSummary } from "../../../../../src/domain/server/mcp-status"; import type { SharedServerMetadata } from "../../../../../src/domain/server/metadata"; import type { ToolInventorySnapshot } from "../../../../../src/domain/server/tool-inventory"; import { createServerDiagnosticsActions } from "../../../../../src/features/chat/application/connection/server-diagnostics-actions"; import { createServerMetadataActions } from "../../../../../src/features/chat/application/connection/server-metadata-actions"; import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { toolInventoryDiagnosticSections } from "../../../../../src/features/chat/presentation/runtime/tool-inventory-diagnostic-sections"; import { deferred } from "../../../../support/async"; import { chatStateFixture, chatStateWith } from "../../support/state"; describe("server metadata actions", () => { it("applies refreshed non-model app-server metadata", async () => { const stateStore = createChatStateStore(chatStateFixture()); const metadata = serverMetadataFixture({ availableSkills: [skillFixture("writer")] }); const actions = createServerMetadataActions({ stateStore, ...metadataCacheHost(), refreshAppServerMetadata: vi.fn().mockResolvedValue(metadata), isStaleResourceContextError: () => false, }); await actions.refreshAppServerMetadata(); expect(stateStore.getState().connection.availableSkills.map((skill) => skill.name)).toEqual(["writer"]); }); it("preserves panel-local tool diagnostics when applying shared metadata", async () => { const localDiagnostics = diagnosticsWithToolInventory( upsertMcpServerDiagnostic(createServerDiagnostics(), { name: "github", startupStatus: "ready", authStatus: null, toolCount: null, message: null, }), toolInventory(), ); const stateStore = createChatStateStore(chatStateFixture({ connection: { serverDiagnostics: localDiagnostics } })); const metadata = serverMetadataFixture({ serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 model", 2)), }); const actions = createServerMetadataActions({ stateStore, ...metadataCacheHost(), refreshAppServerMetadata: vi.fn().mockResolvedValue(metadata), isStaleResourceContextError: () => false, }); await actions.refreshAppServerMetadata(); expect(stateStore.getState().connection.serverDiagnostics).toMatchObject({ probes: { models: { status: "ok", summary: "1 model" } }, mcpServers: [{ name: "github", startupStatus: "ready" }], toolInventory: { checkedAt: 1 }, }); }); it("keeps MCP startup notifications out of shared metadata", async () => { const stateStore = createChatStateStore(chatStateFixture()); const cache = { current: serverMetadataFixture() as SharedServerMetadata | null }; const actions = createServerMetadataActions({ stateStore, ...metadataCacheHost(cache), refreshAppServerMetadata: vi.fn().mockResolvedValue(null), isStaleResourceContextError: () => false, }); await actions.applyAppServerResourceEvent({ type: "mcp-startup-status-updated", name: "github", status: "ready", message: null, }); expect(stateStore.getState().connection.serverDiagnostics.mcpServers).toMatchObject([{ name: "github", startupStatus: "ready" }]); expect(cache.current?.serverDiagnostics.mcpServers).toEqual([]); }); it("ignores stale shared app-server metadata refreshes without applying state", async () => { const stateStore = createChatStateStore(chatStateFixture()); const stale = new Error("stale"); const actions = createServerMetadataActions({ stateStore, ...metadataCacheHost(), refreshAppServerMetadata: vi.fn().mockRejectedValue(stale), isStaleResourceContextError: (error) => error === stale, }); await expect(actions.refreshAppServerMetadata()).resolves.toBeNull(); expect(stateStore.getState().connection.availableModels).toEqual([]); expect(stateStore.getState().connection.runtimeConfig).toBeNull(); }); it("does not apply stale skill refreshes", async () => { const stateStore = createChatStateStore(chatStateFixture()); const stale = new Error("stale"); const actions = createServerMetadataActions({ stateStore, ...metadataCacheHost(), refreshSkills: vi.fn().mockRejectedValue(stale), isStaleResourceContextError: (error) => error === stale, }); await actions.applyAppServerResourceEvent({ type: "skills-changed" }); expect(stateStore.getState().connection.availableSkills).toEqual([]); }); it("applies authoritative skills refreshed after a change event", async () => { let state = chatStateFixture(); const previousSkills = [skillFixture("writer")]; state = chatStateWith(state, { connection: { availableSkills: previousSkills } }); const stateStore = createChatStateStore(state); const refreshed = serverMetadataFixture({ availableSkills: [skillFixture("editor")], serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("skills", "1 skill", 1)), }); const refreshSkills = vi.fn().mockResolvedValue(refreshed); const actions = createServerMetadataActions({ stateStore, ...metadataCacheHost(), refreshSkills, }); await actions.applyAppServerResourceEvent({ type: "skills-changed" }); expect(refreshSkills).toHaveBeenCalledOnce(); expect(stateStore.getState().connection.availableSkills.map((skill) => skill.name)).toEqual(["editor"]); }); it("publishes refreshed rate limits from update notifications", async () => { const stateStore = createChatStateStore(chatStateFixture()); const rateLimit = rateLimitFixture({ primary: { usedPercent: 64, windowDurationMins: 300, resetsAt: null } }); const actions = createServerMetadataActions({ stateStore, ...metadataCacheHost(), refreshRateLimits: vi.fn().mockResolvedValue(serverMetadataFixture({ rateLimit })), }); await actions.applyAppServerResourceEvent({ type: "rate-limits-updated" }); expect(stateStore.getState().connection.rateLimit).toMatchObject({ primary: { usedPercent: 64 } }); }); }); describe("server diagnostics actions", () => { it("reuses refreshed app-server metadata for deferred diagnostics", async () => { const stateStore = createChatStateStore(chatStateFixture()); const refreshedMetadata = serverMetadataFixture({ availableSkills: [skillFixture("writer")], serverDiagnostics: diagnosticsWithProbe( diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 models", 1)), diagnosticProbeOk("skills", "1 skills", 1), ), }); const cache = { current: null as SharedServerMetadata | null }; const metadataCache = metadataCacheHost(cache); const metadata = createServerMetadataActions({ stateStore, ...metadataCache, refreshAppServerMetadata: vi.fn().mockImplementation(async () => { cache.current = refreshedMetadata; return refreshedMetadata; }), isStaleResourceContextError: () => false, }); const readServerDiagnostics = vi.fn().mockResolvedValue(serverDiagnosticsSnapshot()); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics }, ...metadataCache, }); await metadata.refreshAppServerMetadata(); await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true }); expect(readServerDiagnostics).toHaveBeenCalledWith( expect.objectContaining({ appServerMetadataSnapshot: true, cachedSkills: [skillFixture("writer")], cachedSkillsProbe: expect.objectContaining({ status: "ok", summary: "1 skills" }), }), ); expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({ status: "ok", summary: "1 models", }); }); it("uses metadata diagnostics as the default resource probe source", async () => { const stateStore = createChatStateStore(chatStateFixture()); const metadataCache = metadataCacheHost({ current: serverMetadataFixture({ serverDiagnostics: diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "cached models", 1)), }), }); const readServerDiagnostics = vi.fn().mockResolvedValue(serverDiagnosticsSnapshot()); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics }, ...metadataCache, }); await diagnostics.refreshServerDiagnostics(); expect(readServerDiagnostics).toHaveBeenCalledWith( expect.objectContaining({ appServerMetadataSnapshot: false, forceResourceProbes: false, }), ); expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({ status: "ok", summary: "cached models", }); }); it("can force resource probes for explicit health checks", async () => { const stateStore = createChatStateStore(chatStateFixture()); const readServerDiagnostics = vi.fn().mockResolvedValue( serverDiagnosticsSnapshot({ resourceProbes: [diagnosticProbeOk("models", "1 models", 1), diagnosticProbeOk("rateLimits", "available", 1)], }), ); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics }, ...metadataCacheHost(), }); await diagnostics.refreshServerDiagnostics({ forceResourceProbes: true }); expect(readServerDiagnostics).toHaveBeenCalledWith(expect.objectContaining({ forceResourceProbes: true })); expect(stateStore.getState().connection.serverDiagnostics.probes.models).toMatchObject({ status: "ok", summary: "1 models", }); }); it("does not apply diagnostic probes when the transport returns no snapshot", async () => { const stateStore = createChatStateStore(chatStateFixture()); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics: vi.fn().mockResolvedValue(null) }, appServerMetadataSnapshot: () => null, }); await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true }); expect(stateStore.getState().connection.serverDiagnostics.probes.mcpServers.status).toBe("unknown"); }); it("refreshes tool provider snapshots with cached MCP startup diagnostics", async () => { let state = chatStateFixture(); state = chatStateWith(state, { activeThread: { id: "thread-1" } }); const stateStore = createChatStateStore(state); const metadataCache = metadataCacheHost({ current: serverMetadataFixture() }); const metadata = createServerMetadataActions({ stateStore, ...metadataCache, refreshAppServerMetadata: vi.fn().mockResolvedValue(null), isStaleResourceContextError: () => false, }); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics: vi.fn().mockResolvedValue( serverDiagnosticsSnapshot({ mcpServerStatuses: [mcpServerStatus()], }), ), }, ...metadataCache, }); await metadata.applyAppServerResourceEvent({ type: "mcp-startup-status-updated", name: "github", status: "ready", message: null, }); await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true }); const sections = toolInventoryDiagnosticSections(stateStore.getState().connection.serverDiagnostics); const toolProviderRows = sections.find((section) => section.title === "Tool providers")?.rows ?? []; expect(sections.map((section) => section.title)).toEqual(["Plugins", "Tool providers", "Skills"]); expect(toolProviderRows.map((row) => `${row.label}: ${row.value}`)).toEqual(["github: MCP server, ready, auth oAuth, 1 tool"]); }); it("drops a diagnostics result when its active thread is no longer current", async () => { const pending = deferred>(); const stateStore = createChatStateStore(chatStateFixture({ activeThread: { id: "thread-1" } })); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics: vi.fn(() => pending.promise) }, ...metadataCacheHost(), }); const refreshing = diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true }); stateStore.dispatch({ type: "active-thread/cleared" }); pending.resolve(serverDiagnosticsSnapshot({ mcpServerStatuses: [mcpServerStatus()] })); await refreshing; expect(stateStore.getState().connection.serverDiagnostics.toolInventory).toBeNull(); expect(stateStore.getState().connection.serverDiagnostics.mcpServers).toEqual([]); }); it("drops a diagnostics result after its connection scope is invalidated", async () => { const pending = deferred>(); const stateStore = createChatStateStore(chatStateFixture({ activeThread: { id: "thread-1" } })); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics: vi.fn(() => pending.promise) }, ...metadataCacheHost(), }); const refreshing = diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true }); diagnostics.invalidate(); pending.resolve(serverDiagnosticsSnapshot({ mcpServerStatuses: [mcpServerStatus()] })); await refreshing; expect(stateStore.getState().connection.serverDiagnostics.toolInventory).toBeNull(); expect(stateStore.getState().connection.serverDiagnostics.mcpServers).toEqual([]); }); it("removes MCP providers missing from the latest thread-scoped inventory", async () => { let initialDiagnostics = upsertMcpServerDiagnostic(createServerDiagnostics(), { name: "github", startupStatus: "ready", authStatus: null, toolCount: null, message: null, }); initialDiagnostics = upsertMcpServerDiagnostic(initialDiagnostics, { name: "removed-provider", startupStatus: "ready", authStatus: null, toolCount: null, message: null, }); const stateStore = createChatStateStore( chatStateFixture({ activeThread: { id: "thread-1" }, connection: { serverDiagnostics: initialDiagnostics } }), ); const diagnostics = createServerDiagnosticsActions({ stateStore, diagnosticsTransport: { readServerDiagnostics: vi.fn().mockResolvedValue(serverDiagnosticsSnapshot({ mcpServerStatuses: [mcpServerStatus()] })), }, ...metadataCacheHost(), }); await diagnostics.refreshServerDiagnostics({ appServerMetadataSnapshot: true }); expect(stateStore.getState().connection.serverDiagnostics.mcpServers.map((server) => server.name)).toEqual(["github"]); }); }); function serverDiagnosticsSnapshot( overrides: { resourceProbes?: DiagnosticProbeResult[]; mcpServerStatuses?: McpServerStatusSummary[] | null } = {}, ) { return { resourceProbes: overrides.resourceProbes ?? [], toolInventory: { inventory: toolInventory(), probes: [ diagnosticProbeOk("plugins", "0 plugins", 1), diagnosticProbeOk("mcpServers", "0 servers", 1), diagnosticProbeOk("skills", "0 skills", 1), ], mcpServerStatuses: overrides.mcpServerStatuses ?? [], }, }; } function toolInventory(): ToolInventorySnapshot { return { checkedAt: 1, plugins: [], pluginMarketplaceErrors: [], pluginsError: null, mcpServers: [], mcpDiagnostics: [], mcpError: null, skills: [], skillsError: null, }; } function skillFixture(name: string): SkillMetadata { return { name, description: "", path: `/skills/${name}`, enabled: true, }; } function rateLimitFixture(overrides: Partial = {}): RateLimitSnapshot { return { limitId: "codex", limitName: "Codex", primary: null, secondary: null, individualLimit: null, rateLimitReachedType: null, ...overrides, }; } function serverMetadataFixture(overrides: Partial = {}): SharedServerMetadata { return { runtimeConfig: emptyRuntimeConfigSnapshot(), availableSkills: [], availablePermissionProfiles: [], rateLimit: null, serverDiagnostics: createServerDiagnostics(), ...overrides, }; } function metadataCacheHost(cache: { current: SharedServerMetadata | null } = { current: null }): { appServerMetadataSnapshot: () => SharedServerMetadata | null; refreshAppServerMetadata: () => Promise; refreshSkills: () => Promise; refreshRateLimits: () => Promise; isStaleResourceContextError: (error: unknown) => boolean; } { return { appServerMetadataSnapshot: () => cache.current, refreshAppServerMetadata: vi.fn().mockResolvedValue(null), refreshSkills: vi.fn().mockResolvedValue(null), refreshRateLimits: vi.fn().mockResolvedValue(null), isStaleResourceContextError: () => false, }; } function mcpServerStatus(): McpServerStatusSummary { return { name: "github", authStatus: "oAuth", toolCount: 1, resourceCount: 0, resourceTemplateCount: 0, }; }