import { describe, expect, it } from "bun:test"; import { adaptiveMaxTokens, conversationChars, ErrorTracker, truncateToolOutput, } from "./turn_service.ts"; import { type ChatMessage, newConversation, systemMessage, userMessage, toolResultMessage } from "@zesdex/domain"; describe("truncateToolOutput", () => { it("short output is unchanged", () => { expect(truncateToolOutput("short")).toBe("short"); }); it("long output preserves head and marks cut", () => { const long = "x".repeat(12_000 + 500); const truncated = truncateToolOutput(long); expect(truncated.length).toBeLessThan(long.length); expect(truncated).toContain("...[truncated"); expect(truncated.startsWith("xxx")).toBe(true); }); }); describe("adaptiveMaxTokens", () => { it("scales with request length", () => { expect(adaptiveMaxTokens(10)).toBe(800); expect(adaptiveMaxTokens(200)).toBe(1600); expect(adaptiveMaxTokens(5000)).toBe(4096); }); }); describe("ErrorTracker", () => { it("injects recovery note after repeated errors", () => { const tracker = new ErrorTracker(); const messages: ChatMessage[] = []; tracker.record("read", "Error: File not found", messages); tracker.record("read", "Error: File not found", messages); expect(tracker.shouldStop()).toBe(false); tracker.record("read", "Error: File not found", messages); expect(messages.some((m) => m.content?.includes("[System note]"))).toBe(true); }); it("stops after too many errors", () => { const tracker = new ErrorTracker(); const messages: ChatMessage[] = []; for (let i = 0; i < 8; i++) { tracker.record("bash", `Error: boom ${i}`, messages); } expect(tracker.shouldStop()).toBe(true); }); }); describe("conversationChars", () => { it("sums content only", () => { const conv = newConversation("sys", "s1"); conv.messages.push(systemMessage("sys"), userMessage("hello world"), toolResultMessage("id", "output")); expect(conversationChars(conv.messages)).toBe(3 + 11 + 6); }); });