tui: filter agent events by active chat run id
Agent events are emitted per run; filter against activeChatRunId instead of session id. Adds unit tests for tool + lifecycle events.
This commit is contained in:
committed by
Peter Steinberger
parent
7e498ab94a
commit
f56f799990
124
src/tui/tui-event-handlers.test.ts
Normal file
124
src/tui/tui-event-handlers.test.ts
Normal file
@@ -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<typeof vi.fn>;
|
||||||
|
updateToolResult: ReturnType<typeof vi.fn>;
|
||||||
|
addSystem: ReturnType<typeof vi.fn>;
|
||||||
|
updateAssistant: ReturnType<typeof vi.fn>;
|
||||||
|
finalizeAssistant: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("tui-event-handlers: handleAgentEvent", () => {
|
||||||
|
const makeState = (overrides?: Partial<TuiStateAccess>): 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -95,7 +95,9 @@ export function createEventHandlers(context: EventHandlerContext) {
|
|||||||
const handleAgentEvent = (payload: unknown) => {
|
const handleAgentEvent = (payload: unknown) => {
|
||||||
if (!payload || typeof payload !== "object") return;
|
if (!payload || typeof payload !== "object") return;
|
||||||
const evt = payload as AgentEvent;
|
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") {
|
if (evt.stream === "tool") {
|
||||||
const data = evt.data ?? {};
|
const data = evt.data ?? {};
|
||||||
const phase = asString(data.phase, "");
|
const phase = asString(data.phase, "");
|
||||||
|
|||||||
Reference in New Issue
Block a user