murashit_codex-panel/tests/features/chat/host/connection-bundle.test.ts

152 lines
5.5 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { ConnectionManagerHandlers } from "../../../../src/app-server/connection/connection-manager";
import type { ServerRequest } from "../../../../src/app-server/connection/rpc-messages";
import { createChatStateStore } from "../../../../src/features/chat/application/state/store";
import { createConnectionBundle } from "../../../../src/features/chat/host/bundles/connection-bundle";
import { chatStateFixture } from "../support/state";
type ConnectionBundleHost = Parameters<typeof createConnectionBundle>[0];
type ConnectionBundleInput = Parameters<typeof createConnectionBundle>[1];
describe("connection bundle", () => {
it("responds through the responder that delivered the request", async () => {
const fixture = connectionBundleFixture();
await fixture.bundle.connection.actions.ensureConnected();
const responder = { respond: vi.fn(), reject: vi.fn() };
fixture.connectionHandlers().onServerRequest(userInputRequest(1), responder);
fixture.bundle.inboundHandler.resolveUserInput(1, { note: "Continue" });
expect(responder.respond).toHaveBeenCalledWith({ answers: { note: { answers: ["Continue"] } } });
});
it("cannot reuse a responder after its connection scope is invalidated", async () => {
const fixture = connectionBundleFixture();
await fixture.bundle.connection.actions.ensureConnected();
const responder = { respond: vi.fn(), reject: vi.fn() };
fixture.connectionHandlers().onServerRequest(userInputRequest(1), responder);
fixture.bundle.invalidateConnectionScope();
fixture.bundle.inboundHandler.resolveUserInput(1, { note: "Continue" });
expect(responder.respond).not.toHaveBeenCalled();
expect(responder.reject).not.toHaveBeenCalled();
});
it("reports deferred diagnostics failures as system messages", async () => {
const error = new Error("diagnostics failed");
const fixture = connectionBundleFixture({
readServerDiagnostics: vi.fn().mockRejectedValue(error),
});
await fixture.bundle.connection.actions.ensureConnected();
fixture.runScheduledDiagnostics();
await vi.waitFor(() => {
expect(fixture.addSystemMessage).toHaveBeenCalledWith("diagnostics failed");
});
});
it("does not run deferred diagnostics after disconnect", async () => {
const readServerDiagnostics = vi.fn().mockResolvedValue(null);
const fixture = connectionBundleFixture({ readServerDiagnostics });
await fixture.bundle.connection.actions.ensureConnected();
fixture.setConnected(false);
fixture.runScheduledDiagnostics();
expect(readServerDiagnostics).not.toHaveBeenCalled();
});
});
function connectionBundleFixture(overrides: { readServerDiagnostics?: ReturnType<typeof vi.fn> } = {}) {
let connected = false;
let handlers: ConnectionManagerHandlers | null = null;
let scheduledDiagnostics: (() => void) | null = null;
const addSystemMessage = vi.fn();
const stateStore = createChatStateStore(
chatStateFixture({
activeThread: { id: "thread-active" },
turn: { lifecycle: { kind: "running", turnId: "turn-active" } },
}),
);
const host = {
environment: {
plugin: {
appServerQueries: {
appServerMetadataSnapshot: () => null,
refreshAppServerMetadata: vi.fn().mockResolvedValue(undefined),
refreshSkills: vi.fn().mockResolvedValue(undefined),
refreshRateLimits: vi.fn().mockResolvedValue(undefined),
},
threadCatalog: {
refreshActiveThreads: vi.fn().mockResolvedValue(undefined),
apply: vi.fn(),
},
appServerContext: { codexPath: "codex", vaultPath: "/vault" },
},
},
stateStore,
canConnect: () => true,
deferredTasks: {
scheduleDiagnostics: (callback: () => void) => {
scheduledDiagnostics = callback;
},
clearDiagnostics: vi.fn(),
},
invalidateThreadWork: vi.fn(),
refreshTabHeader: vi.fn(),
} as unknown as ConnectionBundleHost;
const input = {
connection: {
connect: async (nextHandlers: ConnectionManagerHandlers) => {
handlers = nextHandlers;
connected = true;
return { codexHome: "/codex", platformFamily: "unix", platformOs: "macos", userAgent: "test" };
},
isConnected: () => connected,
},
diagnosticsTransport: {
readServerDiagnostics: overrides.readServerDiagnostics ?? vi.fn().mockResolvedValue(null),
},
localItemIds: {
next: (prefix: string) => `${prefix}-1`,
},
status: {
set: vi.fn(),
addSystemMessage,
},
autoTitleCoordinator: {
maybeAutoTitleThread: vi.fn(),
resetThreadTurnPresence: vi.fn(),
},
} as unknown as ConnectionBundleInput;
return {
addSystemMessage,
bundle: createConnectionBundle(host, input),
connectionHandlers: () => {
if (!handlers) throw new Error("Expected connection handlers.");
return handlers;
},
runScheduledDiagnostics: () => {
if (!scheduledDiagnostics) throw new Error("Expected deferred diagnostics callback.");
scheduledDiagnostics();
},
setConnected: (value: boolean) => {
connected = value;
},
};
}
function userInputRequest(id: number): Extract<ServerRequest, { method: "item/tool/requestUserInput" }> {
return {
id,
method: "item/tool/requestUserInput",
params: {
threadId: "thread-active",
turnId: "turn-active",
itemId: "input-1",
questions: [{ id: "note", header: "Note", question: "What now?", isOther: false, isSecret: false, options: null }],
autoResolutionMs: null,
},
};
}