mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
460 lines
18 KiB
TypeScript
460 lines
18 KiB
TypeScript
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<ReturnType<typeof serverDiagnosticsSnapshot>>();
|
|
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<ReturnType<typeof serverDiagnosticsSnapshot>>();
|
|
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> = {}): RateLimitSnapshot {
|
|
return {
|
|
limitId: "codex",
|
|
limitName: "Codex",
|
|
primary: null,
|
|
secondary: null,
|
|
individualLimit: null,
|
|
rateLimitReachedType: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function serverMetadataFixture(overrides: Partial<SharedServerMetadata> = {}): SharedServerMetadata {
|
|
return {
|
|
runtimeConfig: emptyRuntimeConfigSnapshot(),
|
|
availableSkills: [],
|
|
availablePermissionProfiles: [],
|
|
rateLimit: null,
|
|
serverDiagnostics: createServerDiagnostics(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function metadataCacheHost(cache: { current: SharedServerMetadata | null } = { current: null }): {
|
|
appServerMetadataSnapshot: () => SharedServerMetadata | null;
|
|
refreshAppServerMetadata: () => Promise<SharedServerMetadata | null>;
|
|
refreshSkills: () => Promise<SharedServerMetadata | null>;
|
|
refreshRateLimits: () => Promise<SharedServerMetadata | null>;
|
|
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,
|
|
};
|
|
}
|