From 97252642764240a1cc9e5c6271af8105cfed6762 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Sat, 30 May 2026 02:10:10 -0400 Subject: [PATCH] fix: guard chat event history IPC service --- .../src/main/services/ipc/registerIpc.ts | 11 ++- .../main/services/ipc/runtimeBridge.test.ts | 93 +++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/main/services/ipc/registerIpc.ts b/apps/desktop/src/main/services/ipc/registerIpc.ts index 45ee3d2e1..d3262bbcf 100644 --- a/apps/desktop/src/main/services/ipc/registerIpc.ts +++ b/apps/desktop/src/main/services/ipc/registerIpc.ts @@ -5859,9 +5859,16 @@ export function registerIpc({ _event, arg: { sessionId?: string; maxEvents?: number }, ): Promise => { - const ctx = ensureAgentChatContext(); + const ctx = getCtx(); const sessionId = typeof arg?.sessionId === "string" ? arg.sessionId.trim() : ""; if (!sessionId) return { sessionId: "", events: [], truncated: false, sessionFound: false }; + const service = ctx.agentChatService; + if ( + !service || + typeof (service as unknown as { getChatEventHistory?: unknown }).getChatEventHistory !== "function" + ) { + return { sessionId, events: [], truncated: false, sessionFound: false }; + } // Only forward maxEvents when it is a finite positive number; the service // layer applies its own clamp but guarding here avoids ambiguous NaN/0 // inputs from untrusted renderer IPC. @@ -5870,7 +5877,7 @@ export function registerIpc({ rawMaxEvents != null && Number.isFinite(rawMaxEvents) && rawMaxEvents > 0 ? rawMaxEvents : undefined; - return ctx.agentChatService.getChatEventHistory(sessionId, maxEvents != null ? { maxEvents } : undefined); + return service.getChatEventHistory(sessionId, maxEvents != null ? { maxEvents } : undefined); }); ipcMain.handle(IPC.agentChatReadTranscript, async ( diff --git a/apps/desktop/src/main/services/ipc/runtimeBridge.test.ts b/apps/desktop/src/main/services/ipc/runtimeBridge.test.ts index 23a6e7d65..cb99aa2b1 100644 --- a/apps/desktop/src/main/services/ipc/runtimeBridge.test.ts +++ b/apps/desktop/src/main/services/ipc/runtimeBridge.test.ts @@ -892,6 +892,99 @@ describe("registerIpc sync bridge", () => { expect(listSessions).toHaveBeenCalledWith("lane-1", { includeAutomation: true }); }); + it("returns an empty chat event history when the agent chat service is unavailable", async () => { + registerIpc({ + getCtx: () => ({ + agentChatService: null, + }) as any, + getWindowSession: () => ({ + windowId: 7, + project: { rootPath: "/repo", displayName: "Repo" } as any, + binding: localBinding("/repo"), + }), + switchProjectFromDialog: vi.fn(), + closeCurrentProject: vi.fn(), + closeProjectByPath: vi.fn(), + globalStatePath: "/tmp/ade-state.json", + }); + + await expect( + ipcHandlers.get(IPC.agentChatGetEventHistory)?.( + eventForSender(), + { sessionId: " chat-1 ", maxEvents: 10 }, + ), + ).resolves.toEqual({ + sessionId: "chat-1", + events: [], + truncated: false, + sessionFound: false, + }); + }); + + it("returns an empty chat event history when the agent chat service lacks event history support", async () => { + registerIpc({ + getCtx: () => ({ + agentChatService: {}, + }) as any, + getWindowSession: () => ({ + windowId: 7, + project: { rootPath: "/repo", displayName: "Repo" } as any, + binding: localBinding("/repo"), + }), + switchProjectFromDialog: vi.fn(), + closeCurrentProject: vi.fn(), + closeProjectByPath: vi.fn(), + globalStatePath: "/tmp/ade-state.json", + }); + + await expect( + ipcHandlers.get(IPC.agentChatGetEventHistory)?.( + eventForSender(), + { sessionId: " chat-1 ", maxEvents: 10 }, + ), + ).resolves.toEqual({ + sessionId: "chat-1", + events: [], + truncated: false, + sessionFound: false, + }); + }); + + it("forwards chat event history requests when the agent chat service is available", async () => { + const snapshot = { + sessionId: "chat-1", + events: [], + truncated: false, + sessionFound: true, + }; + const getChatEventHistory = vi.fn(() => snapshot); + registerIpc({ + getCtx: () => ({ + agentChatService: { + getChatEventHistory, + }, + }) as any, + getWindowSession: () => ({ + windowId: 7, + project: { rootPath: "/repo", displayName: "Repo" } as any, + binding: localBinding("/repo"), + }), + switchProjectFromDialog: vi.fn(), + closeCurrentProject: vi.fn(), + closeProjectByPath: vi.fn(), + globalStatePath: "/tmp/ade-state.json", + }); + + await expect( + ipcHandlers.get(IPC.agentChatGetEventHistory)?.( + eventForSender(), + { sessionId: " chat-1 ", maxEvents: 25 }, + ), + ).resolves.toBe(snapshot); + + expect(getChatEventHistory).toHaveBeenCalledWith("chat-1", { maxEvents: 25 }); + }); + it("disposes a live terminal runtime before deleting the session", async () => { const terminalSession = { id: "terminal-1",