openskynet / src /tui /tui-event-handlers.test.ts
Darochin's picture
Mirror OpenSkyNet workspace snapshot from Git HEAD
fc93158 verified
import { describe, expect, it, vi } from "vitest";
import { createEventHandlers } from "./tui-event-handlers.js";
import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js";
type MockFn = ReturnType<typeof vi.fn>;
type HandlerChatLog = {
startTool: (...args: unknown[]) => void;
updateToolResult: (...args: unknown[]) => void;
addSystem: (...args: unknown[]) => void;
updateAssistant: (...args: unknown[]) => void;
finalizeAssistant: (...args: unknown[]) => void;
dropAssistant: (...args: unknown[]) => void;
};
type HandlerTui = { requestRender: (...args: unknown[]) => void };
type MockChatLog = {
startTool: MockFn;
updateToolResult: MockFn;
addSystem: MockFn;
updateAssistant: MockFn;
finalizeAssistant: MockFn;
dropAssistant: MockFn;
};
type MockTui = { requestRender: MockFn };
function createMockChatLog(): MockChatLog & HandlerChatLog {
return {
startTool: vi.fn(),
updateToolResult: vi.fn(),
addSystem: vi.fn(),
updateAssistant: vi.fn(),
finalizeAssistant: vi.fn(),
dropAssistant: vi.fn(),
} as unknown as MockChatLog & HandlerChatLog;
}
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: { verboseLevel: "on" },
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 = createMockChatLog();
const tui = { requestRender: vi.fn() } as unknown as MockTui & HandlerTui;
const setActivityStatus = vi.fn();
const loadHistory = vi.fn();
const localRunIds = new Set<string>();
const noteLocalRunId = (runId: string) => {
localRunIds.add(runId);
};
const forgetLocalRunId = localRunIds.delete.bind(localRunIds);
const isLocalRunId = localRunIds.has.bind(localRunIds);
const clearLocalRunIds = localRunIds.clear.bind(localRunIds);
return {
chatLog,
tui,
state,
setActivityStatus,
loadHistory,
noteLocalRunId,
forgetLocalRunId,
isLocalRunId,
clearLocalRunIds,
};
};
const createHandlersHarness = (params?: {
state?: Partial<TuiStateAccess>;
chatLog?: HandlerChatLog;
}) => {
const state = makeState(params?.state);
const context = makeContext(state);
const chatLog = (params?.chatLog ?? context.chatLog) as MockChatLog & HandlerChatLog;
const handlers = createEventHandlers({
chatLog,
tui: context.tui,
state,
setActivityStatus: context.setActivityStatus,
loadHistory: context.loadHistory,
isLocalRunId: context.isLocalRunId,
forgetLocalRunId: context.forgetLocalRunId,
});
return {
...context,
state,
chatLog,
...handlers,
};
};
it("processes tool events when runId matches activeChatRunId (even if sessionId differs)", () => {
const { chatLog, tui, handleAgentEvent } = createHandlersHarness({
state: { currentSessionId: "session-xyz", activeChatRunId: "run-123" },
});
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 { chatLog, tui, handleAgentEvent } = createHandlersHarness({
state: { activeChatRunId: "run-1" },
});
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 chatLog = createMockChatLog();
const { tui, setActivityStatus, handleAgentEvent } = createHandlersHarness({
state: { activeChatRunId: "run-9" },
chatLog,
});
const evt: AgentEvent = {
runId: "run-9",
stream: "lifecycle",
data: { phase: "start" },
};
handleAgentEvent(evt);
expect(setActivityStatus).toHaveBeenCalledWith("running");
expect(tui.requestRender).toHaveBeenCalledTimes(1);
});
it("formats provider quota errors on chat run error events", () => {
const { chatLog, handleChatEvent } = createHandlersHarness({
state: { activeChatRunId: "run-err" },
});
handleChatEvent({
runId: "run-err",
sessionKey: "agent:main:main",
state: "error",
errorMessage:
"429 Too Many Requests: you have reached your weekly usage limit, please wait or upgrade to continue",
});
expect(chatLog.addSystem).toHaveBeenCalledWith(
"run error: ⚠️ Model usage limit reached for the current period. Wait for quota reset or switch to another model.",
);
});
it("captures runId from chat events when activeChatRunId is unset", () => {
const { state, chatLog, handleChatEvent, handleAgentEvent } = createHandlersHarness({
state: { activeChatRunId: null },
});
const chatEvt: ChatEvent = {
runId: "run-42",
sessionKey: state.currentSessionKey,
state: "delta",
message: { content: "hello" },
};
handleChatEvent(chatEvt);
expect(state.activeChatRunId).toBe("run-42");
const agentEvt: AgentEvent = {
runId: "run-42",
stream: "tool",
data: { phase: "start", toolCallId: "tc1", name: "exec" },
};
handleAgentEvent(agentEvt);
expect(chatLog.startTool).toHaveBeenCalledWith("tc1", "exec", undefined);
});
it("accepts chat events when session key is an alias of the active canonical key", () => {
const { state, chatLog, handleChatEvent } = createHandlersHarness({
state: {
currentSessionKey: "agent:main:main",
activeChatRunId: null,
},
});
handleChatEvent({
runId: "run-alias",
sessionKey: "main",
state: "delta",
message: { content: "hello" },
});
expect(state.activeChatRunId).toBe("run-alias");
expect(chatLog.updateAssistant).toHaveBeenCalledWith("hello", "run-alias");
});
it("does not cross-match canonical session keys from different agents", () => {
const { chatLog, handleChatEvent } = createHandlersHarness({
state: {
currentAgentId: "alpha",
currentSessionKey: "agent:alpha:main",
activeChatRunId: null,
},
});
handleChatEvent({
runId: "run-other-agent",
sessionKey: "agent:beta:main",
state: "delta",
message: { content: "should be ignored" },
});
expect(chatLog.updateAssistant).not.toHaveBeenCalled();
});
it("clears run mapping when the session changes", () => {
const { state, chatLog, tui, handleChatEvent, handleAgentEvent } = createHandlersHarness({
state: { activeChatRunId: null },
});
handleChatEvent({
runId: "run-old",
sessionKey: state.currentSessionKey,
state: "delta",
message: { content: "hello" },
});
state.currentSessionKey = "agent:main:other";
state.activeChatRunId = null;
tui.requestRender.mockClear();
handleAgentEvent({
runId: "run-old",
stream: "tool",
data: { phase: "start", toolCallId: "tc2", name: "exec" },
});
expect(chatLog.startTool).not.toHaveBeenCalled();
expect(tui.requestRender).not.toHaveBeenCalled();
});
it("accepts tool events after chat final for the same run", () => {
const { state, chatLog, tui, handleChatEvent, handleAgentEvent } = createHandlersHarness({
state: { activeChatRunId: null },
});
handleChatEvent({
runId: "run-final",
sessionKey: state.currentSessionKey,
state: "final",
message: { content: [{ type: "text", text: "done" }] },
});
handleAgentEvent({
runId: "run-final",
stream: "tool",
data: { phase: "start", toolCallId: "tc-final", name: "session_status" },
});
expect(chatLog.startTool).toHaveBeenCalledWith("tc-final", "session_status", undefined);
expect(tui.requestRender).toHaveBeenCalled();
});
it("ignores lifecycle updates for non-active runs in the same session", () => {
const { state, tui, setActivityStatus, handleChatEvent, handleAgentEvent } =
createHandlersHarness({
state: { activeChatRunId: "run-active" },
});
handleChatEvent({
runId: "run-other",
sessionKey: state.currentSessionKey,
state: "delta",
message: { content: "hello" },
});
setActivityStatus.mockClear();
tui.requestRender.mockClear();
handleAgentEvent({
runId: "run-other",
stream: "lifecycle",
data: { phase: "end" },
});
expect(setActivityStatus).not.toHaveBeenCalled();
expect(tui.requestRender).not.toHaveBeenCalled();
});
it("suppresses tool events when verbose is off", () => {
const { chatLog, tui, handleAgentEvent } = createHandlersHarness({
state: {
activeChatRunId: "run-123",
sessionInfo: { verboseLevel: "off" },
},
});
handleAgentEvent({
runId: "run-123",
stream: "tool",
data: { phase: "start", toolCallId: "tc-off", name: "session_status" },
});
expect(chatLog.startTool).not.toHaveBeenCalled();
expect(tui.requestRender).not.toHaveBeenCalled();
});
it("omits tool output when verbose is on (non-full)", () => {
const { chatLog, handleAgentEvent } = createHandlersHarness({
state: {
activeChatRunId: "run-123",
sessionInfo: { verboseLevel: "on" },
},
});
handleAgentEvent({
runId: "run-123",
stream: "tool",
data: {
phase: "update",
toolCallId: "tc-on",
name: "session_status",
partialResult: { content: [{ type: "text", text: "secret" }] },
},
});
handleAgentEvent({
runId: "run-123",
stream: "tool",
data: {
phase: "result",
toolCallId: "tc-on",
name: "session_status",
result: { content: [{ type: "text", text: "secret" }] },
isError: false,
},
});
expect(chatLog.updateToolResult).toHaveBeenCalledTimes(1);
expect(chatLog.updateToolResult).toHaveBeenCalledWith(
"tc-on",
{ content: [] },
{ isError: false },
);
});
it("refreshes history after a non-local chat final", () => {
const { state, loadHistory, handleChatEvent } = createHandlersHarness({
state: { activeChatRunId: null },
});
handleChatEvent({
runId: "external-run",
sessionKey: state.currentSessionKey,
state: "final",
message: { content: [{ type: "text", text: "done" }] },
});
expect(loadHistory).toHaveBeenCalledTimes(1);
});
function createConcurrentRunHarness(localContent = "partial") {
const { state, chatLog, setActivityStatus, loadHistory, handleChatEvent } =
createHandlersHarness({
state: { activeChatRunId: "run-active" },
});
handleChatEvent({
runId: "run-active",
sessionKey: state.currentSessionKey,
state: "delta",
message: { content: localContent },
});
return { state, chatLog, setActivityStatus, loadHistory, handleChatEvent };
}
it("does not reload history or clear active run when another run final arrives mid-stream", () => {
const { state, chatLog, setActivityStatus, loadHistory, handleChatEvent } =
createConcurrentRunHarness("partial");
loadHistory.mockClear();
setActivityStatus.mockClear();
handleChatEvent({
runId: "run-other",
sessionKey: state.currentSessionKey,
state: "final",
message: { content: [{ type: "text", text: "other final" }] },
});
expect(loadHistory).not.toHaveBeenCalled();
expect(state.activeChatRunId).toBe("run-active");
expect(setActivityStatus).not.toHaveBeenCalledWith("idle");
handleChatEvent({
runId: "run-active",
sessionKey: state.currentSessionKey,
state: "delta",
message: { content: "continued" },
});
expect(chatLog.updateAssistant).toHaveBeenLastCalledWith("continued", "run-active");
});
it("suppresses non-local empty final placeholders during concurrent runs", () => {
const { state, chatLog, loadHistory, handleChatEvent } =
createConcurrentRunHarness("local stream");
loadHistory.mockClear();
chatLog.finalizeAssistant.mockClear();
chatLog.dropAssistant.mockClear();
handleChatEvent({
runId: "run-other",
sessionKey: state.currentSessionKey,
state: "final",
message: { content: [] },
});
expect(chatLog.finalizeAssistant).not.toHaveBeenCalledWith("(no output)", "run-other");
expect(chatLog.dropAssistant).toHaveBeenCalledWith("run-other");
expect(loadHistory).not.toHaveBeenCalled();
expect(state.activeChatRunId).toBe("run-active");
});
it("renders final error text when chat final has no content but includes event errorMessage", () => {
const { state, chatLog, handleChatEvent } = createHandlersHarness({
state: { activeChatRunId: null },
});
handleChatEvent({
runId: "run-error-envelope",
sessionKey: state.currentSessionKey,
state: "final",
message: { content: [] },
errorMessage: '401 {"error":{"message":"Missing scopes: model.request"}}',
});
expect(chatLog.finalizeAssistant).toHaveBeenCalledTimes(1);
const [rendered] = chatLog.finalizeAssistant.mock.calls[0] ?? [];
expect(String(rendered)).toContain("HTTP 401");
expect(String(rendered)).toContain("Missing scopes: model.request");
expect(chatLog.dropAssistant).not.toHaveBeenCalledWith("run-error-envelope");
});
it("drops streaming assistant when chat final has no message", () => {
const { state, chatLog, handleChatEvent } = createHandlersHarness({
state: { activeChatRunId: null },
});
handleChatEvent({
runId: "run-silent",
sessionKey: state.currentSessionKey,
state: "delta",
message: { content: "hello" },
});
chatLog.dropAssistant.mockClear();
chatLog.finalizeAssistant.mockClear();
handleChatEvent({
runId: "run-silent",
sessionKey: state.currentSessionKey,
state: "final",
});
expect(chatLog.dropAssistant).toHaveBeenCalledWith("run-silent");
expect(chatLog.finalizeAssistant).not.toHaveBeenCalled();
});
it("reloads history when a local run ends without a displayable final message", () => {
const { state, loadHistory, noteLocalRunId, handleChatEvent } = createHandlersHarness({
state: { activeChatRunId: "run-local-silent" },
});
noteLocalRunId("run-local-silent");
handleChatEvent({
runId: "run-local-silent",
sessionKey: state.currentSessionKey,
state: "final",
});
expect(loadHistory).toHaveBeenCalledTimes(1);
});
});