Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions src/tui/components/tool-execution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import type { TUI } from "@mariozechner/pi-tui";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ToolExecutionComponent } from "./tool-execution.js";

const mockSetBgFn = vi.fn();
const mockClear = vi.fn();
const mockAddChild = vi.fn();
const mockRender = vi.fn().mockReturnValue(["rendered box"]);
const mockInvalidateBox = vi.fn();

const mockStopLoader = vi.fn();
const mockInvalidateLoader = vi.fn();

vi.mock("@mariozechner/pi-tui", () => {
return {
Box: class {
setBgFn = mockSetBgFn;
clear = mockClear;
addChild = mockAddChild;
render = mockRender;
invalidate = mockInvalidateBox;
},
Loader: class {
stop = mockStopLoader;
invalidate = mockInvalidateLoader;
// biome-ignore lint/suspicious/noExplicitAny: Mocking external constructor
constructor(_tui: any, color1: any, color2: any) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Loader mock constructor signature does not match its usage in ToolExecutionComponent. It should accept four arguments to correctly represent the external dependency.

Suggested change
constructor(_tui: any, color1: any, color2: any) {
constructor(_tui: any, color1: any, color2: any, _text: any) {

// Exercise the callbacks to get coverage
color1("spinner text");
color2("muted text");
}
},
Text: class {
constructor(
public text: string,
public x: number,
public y: number,
) {}
},
TUI: class {},
};
});

describe("ToolExecutionComponent", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should initialize with 'running' status and loader", () => {
const mockTui = {} as unknown as TUI;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The mockTui variable is redefined in almost every test case. To improve maintainability and reduce boilerplate, consider moving it to a beforeEach block or defining it once at the top level of the describe block.

const component = new ToolExecutionComponent(
"testAction",
{ arg1: "value" },
mockTui,
);

expect(component).toBeDefined();

// Test the callbacks passed to Box
const setBgFnCall = mockSetBgFn.mock.calls;
// To trigger the background callback:
if (setBgFnCall.length > 0 && typeof setBgFnCall[0][0] === "function") {
setBgFnCall[0][0]("bgtext");
}
Comment on lines +60 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code is currently ineffective. mockSetBgFn is only called when updateResult is invoked, not during the component's initialization. Furthermore, the Box mock does not capture the background function passed to its constructor, so it cannot be retrieved or triggered here. If the intent is to test the initial background logic, the Box mock needs to be updated to store the constructor's callback.


const lines = component.render(100);
expect(lines).toEqual(["rendered box"]);
expect(mockRender).toHaveBeenCalledWith(100);
expect(mockClear).toHaveBeenCalled();
expect(mockAddChild).toHaveBeenCalled(); // Header and Loader
});

it("should update result to success", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent(
"testAction",
{ arg1: "value" },
mockTui,
);

component.updateResult({ text: "Success result" });

expect(mockStopLoader).toHaveBeenCalled();
expect(mockSetBgFn).toHaveBeenCalled();

// Trigger the success bg callback
const setBgFnCall = mockSetBgFn.mock.calls;
const lastCall = setBgFnCall[setBgFnCall.length - 1];
if (lastCall && typeof lastCall[0] === "function") {
lastCall[0]("successbg");
}

// After clear, it should add header, and text body.
expect(mockClear).toHaveBeenCalledTimes(2);
});

it("should update result to error", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent(
"testAction",
{ arg1: "value" },
mockTui,
);

component.updateResult({ text: "Error result", isError: true });

expect(mockStopLoader).toHaveBeenCalled();
expect(mockSetBgFn).toHaveBeenCalled();

// Trigger the error bg callback
const setBgFnCall = mockSetBgFn.mock.calls;
const lastCall = setBgFnCall[setBgFnCall.length - 1];
if (lastCall && typeof lastCall[0] === "function") {
lastCall[0]("errorbg");
}
});

it("should handle updateResult with missing text", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent("testAction", {}, mockTui);
component.updateResult({});
expect(mockClear).toHaveBeenCalledTimes(2);
});

it("should summarize large arguments", () => {
const mockTui = {} as unknown as TUI;
const largeString = "a".repeat(100);
const _component = new ToolExecutionComponent(
"testAction",
{ arg1: largeString },
mockTui,
);

expect(mockAddChild).toHaveBeenCalled();
const calls = mockAddChild.mock.calls;
// Check header text creation arg: it should contain truncated text
const headerCall = calls.find((call) =>
call[0].text?.includes("arg1=aaaaaaaaaaaaaaaaaaaaaaaaaaa..."),
);
expect(headerCall).toBeDefined();
});

it("should handle non-string arguments in summary", () => {
const mockTui = {} as unknown as TUI;
const _component = new ToolExecutionComponent(
"testAction",
{ arg1: 123, arg2: { nested: true } },
mockTui,
);

const calls = mockAddChild.mock.calls;
const headerCall = calls.find((call) => call[0].text?.includes("arg1=123"));
expect(headerCall).toBeDefined();
});

it("should handle empty arguments", () => {
const mockTui = {} as unknown as TUI;
const _component = new ToolExecutionComponent("testAction", {}, mockTui);
const calls = mockAddChild.mock.calls;
const headerCall = calls.find((call) =>
call[0].text?.includes("testAction"),
);
expect(headerCall).toBeDefined();
});

it("should truncate multiple summary args when total length exceeds 80", () => {
const mockTui = {} as unknown as TUI;
const _component = new ToolExecutionComponent(
"testAction",
{ a: "A".repeat(30), b: "B".repeat(30), c: "C".repeat(30) },
mockTui,
);

const calls = mockAddChild.mock.calls;
const headerCall = calls.find((call) => call[0].text?.includes("..."));
expect(headerCall).toBeDefined();
});

it("should toggle expanded state", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent("testAction", {}, mockTui);

component.updateResult({ text: "1\n2\n3\n4\n5\n6\n7" });

mockAddChild.mockClear();
component.setExpanded(true);

// After expanding, the last child added shouldn't be the "... more lines" message
const calls = mockAddChild.mock.calls;
const textAdded = calls.map((c) => c[0].text).join(" ");
expect(textAdded).not.toContain("more lines");
expect(textAdded).toContain("7"); // Should contain the 7th line
});

it("should handle multiline output when short", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent("testAction", {}, mockTui);

mockAddChild.mockClear();
component.updateResult({ text: "Line 1\nLine 2\nLine 3" });

const calls = mockAddChild.mock.calls;
const textAdded = calls.map((c) => c[0].text).join(" ");
expect(textAdded).not.toContain("more lines");
});

it("should truncate multiline output when long and not expanded", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent("testAction", {}, mockTui);

mockAddChild.mockClear();
component.updateResult({ text: "1\n2\n3\n4\n5\n6\n7" });

const calls = mockAddChild.mock.calls;
const textAdded = calls.map((c) => c[0].text).join(" ");
expect(textAdded).toContain("more lines");
expect(textAdded).not.toContain("7");
});

it("should correctly handle invalidate", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent("testAction", {}, mockTui);

component.invalidate();
expect(mockInvalidateBox).toHaveBeenCalled();
expect(mockInvalidateLoader).toHaveBeenCalled();
});

it("should safely handle invalidate without loader", () => {
const mockTui = {} as unknown as TUI;
const component = new ToolExecutionComponent("testAction", {}, mockTui);
component.updateResult({ text: "Success" }); // sets loader to null

mockInvalidateBox.mockClear();
mockInvalidateLoader.mockClear();

component.invalidate();
expect(mockInvalidateBox).toHaveBeenCalled();
expect(mockInvalidateLoader).not.toHaveBeenCalled();
});
});
Loading