import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test" import { createToolOutputTruncatorHook } from "./tool-output-truncator" import * as dynamicTruncator from "../shared/dynamic-truncator" describe("createToolOutputTruncatorHook", () => { let hook: ReturnType let truncateSpy: ReturnType beforeEach(() => { truncateSpy = spyOn(dynamicTruncator, "createDynamicTruncator").mockReturnValue({ truncate: mock(async (_sessionID: string, output: string, options?: { targetMaxTokens?: number }) => ({ result: output, truncated: false, targetMaxTokens: options?.targetMaxTokens, })), getUsage: mock(async () => null), truncateSync: mock(() => ({ result: "", truncated: false })), }) hook = createToolOutputTruncatorHook({} as never) }) describe("tool.execute.after", () => { const createInput = (tool: string) => ({ tool, sessionID: "test-session", callID: "test-call-id", }) const createOutput = (outputText: string) => ({ title: "Result", output: outputText, metadata: {}, }) describe("#given webfetch tool", () => { describe("#when output is processed", () => { it("#then should use aggressive truncation limit (10k tokens)", async () => { const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({ result: "truncated", truncated: true, targetMaxTokens: options?.targetMaxTokens, })) truncateSpy.mockReturnValue({ truncate: truncateMock, getUsage: mock(async () => null), truncateSync: mock(() => ({ result: "", truncated: false })), }) hook = createToolOutputTruncatorHook({} as never) const input = createInput("webfetch") const output = createOutput("large content") await hook["tool.execute.after"](input, output) expect(truncateMock).toHaveBeenCalledWith( "test-session", "large content", { targetMaxTokens: 10_000 } ) }) }) describe("#when using WebFetch variant", () => { it("#then should also use aggressive truncation limit", async () => { const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({ result: "truncated", truncated: true, })) truncateSpy.mockReturnValue({ truncate: truncateMock, getUsage: mock(async () => null), truncateSync: mock(() => ({ result: "", truncated: false })), }) hook = createToolOutputTruncatorHook({} as never) const input = createInput("WebFetch") const output = createOutput("large content") await hook["tool.execute.after"](input, output) expect(truncateMock).toHaveBeenCalledWith( "test-session", "large content", { targetMaxTokens: 10_000 } ) }) }) }) describe("#given grep tool", () => { describe("#when output is processed", () => { it("#then should use default truncation limit (50k tokens)", async () => { const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({ result: "truncated", truncated: true, })) truncateSpy.mockReturnValue({ truncate: truncateMock, getUsage: mock(async () => null), truncateSync: mock(() => ({ result: "", truncated: false })), }) hook = createToolOutputTruncatorHook({} as never) const input = createInput("grep") const output = createOutput("grep output") await hook["tool.execute.after"](input, output) expect(truncateMock).toHaveBeenCalledWith( "test-session", "grep output", { targetMaxTokens: 50_000 } ) }) }) }) describe("#given non-truncatable tool", () => { describe("#when tool is not in TRUNCATABLE_TOOLS list", () => { it("#then should not call truncator", async () => { const truncateMock = mock(async () => ({ result: "truncated", truncated: true, })) truncateSpy.mockReturnValue({ truncate: truncateMock, getUsage: mock(async () => null), truncateSync: mock(() => ({ result: "", truncated: false })), }) hook = createToolOutputTruncatorHook({} as never) const input = createInput("Read") const output = createOutput("file content") await hook["tool.execute.after"](input, output) expect(truncateMock).not.toHaveBeenCalled() }) }) }) describe("#given truncate_all_tool_outputs enabled", () => { describe("#when any tool output is processed", () => { it("#then should truncate non-listed tools too", async () => { const truncateMock = mock(async (_sessionID: string, _output: string, options?: { targetMaxTokens?: number }) => ({ result: "truncated", truncated: true, })) truncateSpy.mockReturnValue({ truncate: truncateMock, getUsage: mock(async () => null), truncateSync: mock(() => ({ result: "", truncated: false })), }) hook = createToolOutputTruncatorHook({} as never, { experimental: { truncate_all_tool_outputs: true }, }) const input = createInput("Read") const output = createOutput("file content") await hook["tool.execute.after"](input, output) expect(truncateMock).toHaveBeenCalled() }) }) }) }) })