mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
test(app-server): cover RPC boundary failures
This commit is contained in:
parent
d1337a6c85
commit
d568f9abb1
2 changed files with 117 additions and 3 deletions
|
|
@ -22,6 +22,7 @@ const TEST_INITIALIZE_PARAMS: InitializeParams = {
|
|||
class FakeTransport implements AppServerTransport {
|
||||
readonly sent: RpcOutboundMessage[] = [];
|
||||
running = false;
|
||||
sendError: Error | null = null;
|
||||
|
||||
constructor(private readonly handlers: AppServerTransportHandlers) {}
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ class FakeTransport implements AppServerTransport {
|
|||
}
|
||||
|
||||
send(message: RpcOutboundMessage): void {
|
||||
if (this.sendError) throw this.sendError;
|
||||
this.sent.push(message);
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +47,14 @@ class FakeTransport implements AppServerTransport {
|
|||
this.handlers.onLine(JSON.stringify(message));
|
||||
}
|
||||
|
||||
emitRawLine(line: string): void {
|
||||
this.handlers.onLine(line);
|
||||
}
|
||||
|
||||
emitLog(message: string): void {
|
||||
this.handlers.onLog(message);
|
||||
}
|
||||
|
||||
emitExit(code: number | null = 0, signal: NodeJS.Signals | null = null): void {
|
||||
this.running = false;
|
||||
this.handlers.onExit(code, signal);
|
||||
|
|
@ -244,7 +254,31 @@ describe("AppServerClient", () => {
|
|||
expect(logs.every((message) => message.startsWith("Invalid app-server JSON-RPC message:"))).toBe(true);
|
||||
});
|
||||
|
||||
it("contains inbound handler failures at the JSON-RPC boundary", async () => {
|
||||
it("logs invalid raw JSON without throwing from the transport callback", async () => {
|
||||
const logs: string[] = [];
|
||||
let transport!: FakeTransport;
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: (message) => logs.push(message),
|
||||
onExit: () => undefined,
|
||||
},
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
});
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
await connecting;
|
||||
|
||||
expect(() => transport.emitRawLine("{not JSON")).not.toThrow();
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0]).toContain("Invalid app-server JSON");
|
||||
});
|
||||
|
||||
it("contains notification handler failures at the JSON-RPC boundary", async () => {
|
||||
const logs: string[] = [];
|
||||
let transport!: FakeTransport;
|
||||
const client = createTestClient({
|
||||
|
|
@ -269,6 +303,35 @@ describe("AppServerClient", () => {
|
|||
expect(logs).toEqual(["App-server notification handler failed: notification failed"]);
|
||||
});
|
||||
|
||||
it("contains server-request handler failures and sends an internal-error response", async () => {
|
||||
const logs: string[] = [];
|
||||
let transport!: FakeTransport;
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification: () => undefined,
|
||||
onServerRequest: () => {
|
||||
throw new Error("request failed");
|
||||
},
|
||||
onLog: (message) => logs.push(message),
|
||||
onExit: () => undefined,
|
||||
},
|
||||
transportFactory: (handlers) => {
|
||||
transport = new FakeTransport(handlers);
|
||||
return transport;
|
||||
},
|
||||
});
|
||||
const connecting = client.connect();
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } satisfies Partial<InitializeResponse> });
|
||||
await connecting;
|
||||
|
||||
expect(() => transport.emitLine({ id: 12, method: "currentTime/read", params: { threadId: "thread" } })).not.toThrow();
|
||||
expect(logs).toEqual(["App-server request handler failed: request failed"]);
|
||||
expect(latestSent(transport)).toEqual({
|
||||
id: 12,
|
||||
error: { code: -32603, message: "Codex Panel failed to handle the app-server request." },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects known server requests with malformed required params", async () => {
|
||||
let transport!: FakeTransport;
|
||||
const client = createTestClient({
|
||||
|
|
@ -510,11 +573,13 @@ describe("AppServerClient", () => {
|
|||
const transports: FakeTransport[] = [];
|
||||
const onExit = vi.fn();
|
||||
const onNotification = vi.fn();
|
||||
const onServerRequest = vi.fn();
|
||||
const onLog = vi.fn();
|
||||
const client = createTestClient({
|
||||
handlers: {
|
||||
onNotification,
|
||||
onServerRequest: () => undefined,
|
||||
onLog: () => undefined,
|
||||
onServerRequest,
|
||||
onLog,
|
||||
onExit,
|
||||
},
|
||||
transportFactory: (handlers) => {
|
||||
|
|
@ -535,11 +600,15 @@ describe("AppServerClient", () => {
|
|||
await secondConnect;
|
||||
|
||||
transports[0]?.emitLine({ method: "warning", params: { message: "stale" } });
|
||||
transports[0]?.emitLine({ id: 9, method: "currentTime/read", params: { threadId: "stale" } });
|
||||
transports[0]?.emitLog("stale log");
|
||||
transports[0]?.emitError(new Error("stale failure"));
|
||||
transports[0]?.emitExit(1);
|
||||
|
||||
expect(client.isConnected()).toBe(true);
|
||||
expect(onNotification).not.toHaveBeenCalled();
|
||||
expect(onServerRequest).not.toHaveBeenCalled();
|
||||
expect(onLog).not.toHaveBeenCalled();
|
||||
expect(onExit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -606,6 +675,20 @@ describe("AppServerClient", () => {
|
|||
expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not leave a delayed timeout after a synchronous transport send failure", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal("window", {
|
||||
clearTimeout,
|
||||
setTimeout,
|
||||
});
|
||||
const { client, transport } = await connectedClient();
|
||||
transport.sendError = new Error("write failed");
|
||||
|
||||
expect(() => listModels(client)).toThrow("write failed");
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
});
|
||||
|
||||
it("bounds timed-out response suppression when responses never arrive", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal("window", {
|
||||
|
|
@ -667,4 +750,13 @@ describe("AppServerClient", () => {
|
|||
message: "Method not found",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects malformed RPC error responses without constructing an RPC error", async () => {
|
||||
const { client, transport } = await connectedClient();
|
||||
|
||||
const listing = listModels(client);
|
||||
transport.emitLine({ id: 2, error: { code: "invalid", message: 42 } });
|
||||
|
||||
await expect(listing).rejects.toThrow("Codex app-server returned an invalid error response for model/list.");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -152,6 +152,28 @@ describe("ConnectionManager", () => {
|
|||
expect(onNotification).toHaveBeenCalledWith(notification, { codexPath: "/bin/codex-a", cwd: "/vault", generation: 0 });
|
||||
});
|
||||
|
||||
it("transitions to disconnected and reports the captured context when a connected client exits", async () => {
|
||||
let transport!: SilentTransport;
|
||||
const onExit = vi.fn();
|
||||
const manager = new ConnectionManager(
|
||||
() => "/bin/codex",
|
||||
"/vault",
|
||||
TEST_INITIALIZE_PARAMS,
|
||||
testClientFactory({ onTransport: (next) => (transport = next) }),
|
||||
() => 4,
|
||||
);
|
||||
const connecting = manager.connect({ ...silentConnectionHandlers(), onExit });
|
||||
transport.emitLine({ id: 1, result: { codexHome: "/tmp/codex" } });
|
||||
await connecting;
|
||||
|
||||
transport.emitExit();
|
||||
|
||||
expect(manager.currentClient()).toBeNull();
|
||||
expect(manager.currentConnectionContext()).toBeNull();
|
||||
expect(onExit).toHaveBeenCalledOnce();
|
||||
expect(onExit).toHaveBeenCalledWith({ codexPath: "/bin/codex", cwd: "/vault", generation: 4 });
|
||||
});
|
||||
|
||||
it("does not reuse a connected client across generations of the same raw context", async () => {
|
||||
let contextGeneration = 1;
|
||||
const transports: SilentTransport[] = [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue