diff --git a/src/tui/tui-event-handlers.test.ts b/src/tui/tui-event-handlers.test.ts new file mode 100644 index 000000000..767681c5e --- /dev/null +++ b/src/tui/tui-event-handlers.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createEventHandlers } from "./tui-event-handlers.js"; +import type { AgentEvent, TuiStateAccess } from "./tui-types.js"; + +type MockChatLog = { + startTool: ReturnType; + updateToolResult: ReturnType; + addSystem: ReturnType; + updateAssistant: ReturnType; + finalizeAssistant: ReturnType; +}; + +describe("tui-event-handlers: handleAgentEvent", () => { + const makeState = (overrides?: Partial): TuiStateAccess => ({ + agentDefaultId: "main", + sessionMainKey: "agent:main:main", + sessionScope: "global", + agents: [], + currentAgentId: "main", + currentSessionKey: "agent:main:main", + currentSessionId: "session-1", + activeChatRunId: "run-1", + historyLoaded: true, + sessionInfo: {}, + initialSessionApplied: true, + isConnected: true, + autoMessageSent: false, + toolsExpanded: false, + showThinking: false, + connectionStatus: "connected", + activityStatus: "idle", + statusTimeout: null, + lastCtrlCAt: 0, + ...overrides, + }); + + const makeContext = (state: TuiStateAccess) => { + const chatLog: MockChatLog = { + startTool: vi.fn(), + updateToolResult: vi.fn(), + addSystem: vi.fn(), + updateAssistant: vi.fn(), + finalizeAssistant: vi.fn(), + }; + const tui = { requestRender: vi.fn() }; + const setActivityStatus = vi.fn(); + + return { chatLog, tui, state, setActivityStatus }; + }; + + it("processes tool events when runId matches activeChatRunId (even if sessionId differs)", () => { + const state = makeState({ currentSessionId: "session-xyz", activeChatRunId: "run-123" }); + const { chatLog, tui, setActivityStatus } = makeContext(state); + const { handleAgentEvent } = createEventHandlers({ + // Casts are fine here: TUI runtime shape is larger than we need in unit tests. + chatLog: chatLog as any, + tui: tui as any, + state, + setActivityStatus, + }); + + const evt: AgentEvent = { + runId: "run-123", + stream: "tool", + data: { + phase: "start", + toolCallId: "tc1", + name: "exec", + args: { command: "echo hi" }, + }, + }; + + handleAgentEvent(evt); + + expect(chatLog.startTool).toHaveBeenCalledWith("tc1", "exec", { command: "echo hi" }); + expect(tui.requestRender).toHaveBeenCalledTimes(1); + }); + + it("ignores tool events when runId does not match activeChatRunId", () => { + const state = makeState({ activeChatRunId: "run-1" }); + const { chatLog, tui, setActivityStatus } = makeContext(state); + const { handleAgentEvent } = createEventHandlers({ + chatLog: chatLog as any, + tui: tui as any, + state, + setActivityStatus, + }); + + const evt: AgentEvent = { + runId: "run-2", + stream: "tool", + data: { phase: "start", toolCallId: "tc1", name: "exec" }, + }; + + handleAgentEvent(evt); + + expect(chatLog.startTool).not.toHaveBeenCalled(); + expect(chatLog.updateToolResult).not.toHaveBeenCalled(); + expect(tui.requestRender).not.toHaveBeenCalled(); + }); + + it("processes lifecycle events when runId matches activeChatRunId", () => { + const state = makeState({ activeChatRunId: "run-9" }); + const { tui, setActivityStatus } = makeContext(state); + const { handleAgentEvent } = createEventHandlers({ + chatLog: { startTool: vi.fn(), updateToolResult: vi.fn() } as any, + tui: tui as any, + state, + setActivityStatus, + }); + + const evt: AgentEvent = { + runId: "run-9", + stream: "lifecycle", + data: { phase: "start" }, + }; + + handleAgentEvent(evt); + + expect(setActivityStatus).toHaveBeenCalledWith("running"); + expect(tui.requestRender).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/tui/tui-event-handlers.ts b/src/tui/tui-event-handlers.ts index 148dca67a..66b8129e4 100644 --- a/src/tui/tui-event-handlers.ts +++ b/src/tui/tui-event-handlers.ts @@ -95,7 +95,9 @@ export function createEventHandlers(context: EventHandlerContext) { const handleAgentEvent = (payload: unknown) => { if (!payload || typeof payload !== "object") return; const evt = payload as AgentEvent; - if (!state.currentSessionId || evt.runId !== state.currentSessionId) return; + // Agent events (tool streaming, lifecycle) are emitted per-run. Filter against the + // active chat run id, not the session id. + if (!state.activeChatRunId || evt.runId !== state.activeChatRunId) return; if (evt.stream === "tool") { const data = evt.data ?? {}; const phase = asString(data.phase, "");