diff --git a/packages/core/test/capped-buffer.test.ts b/packages/core/test/capped-buffer.test.ts new file mode 100644 index 0000000..0608a7e --- /dev/null +++ b/packages/core/test/capped-buffer.test.ts @@ -0,0 +1,62 @@ +/** + * CappedTextBuffer unit tests: capacity drop keeps the tail, the omitted tally accumulates, + * drain() prefixes the drop marker and clears the buffer, and isEmpty tracks both text and tally. + */ +import { describe, expect, it } from "vitest"; +import { CappedTextBuffer } from "../src/environment/tools/background/index.js"; + +describe("CappedTextBuffer", () => { + it("starts empty and drains to an empty string", () => { + const buf = new CappedTextBuffer(10, "earlier output"); + expect(buf.isEmpty).toBe(true); + expect(buf.drain()).toBe(""); + }); + + it("keeps content under capacity verbatim", () => { + const buf = new CappedTextBuffer(10, "earlier output"); + buf.append("hello"); + expect(buf.isEmpty).toBe(false); + expect(buf.drain()).toBe("hello"); + expect(buf.isEmpty).toBe(true); + }); + + it("drops the oldest chars past capacity and keeps the tail", () => { + const buf = new CappedTextBuffer(5, "earlier output"); + buf.append("abcdefg"); // 7 chars, cap 5 -> drop "ab", keep "cdefg" + expect(buf.drain()).toBe("[... 2 chars of earlier output dropped ...]\ncdefg"); + }); + + it("handles a single append larger than the cap", () => { + const buf = new CappedTextBuffer(3, "earlier output"); + buf.append("0123456789"); // drop 7, keep "789" + expect(buf.drain()).toBe("[... 7 chars of earlier output dropped ...]\n789"); + }); + + it("accumulates the omitted tally across multiple over-cap appends", () => { + const buf = new CappedTextBuffer(4, "earlier output"); + buf.append("aaaaaa"); // drop 2, keep "aaaa" + buf.append("bb"); // now "aaaabb" -> drop 2 more, keep "aabb" + expect(buf.drain()).toBe("[... 4 chars of earlier output dropped ...]\naabb"); + }); + + it("clears text and tally after drain", () => { + const buf = new CappedTextBuffer(2, "earlier output"); + buf.append("xyz"); // drop 1, keep "yz" + buf.drain(); + expect(buf.isEmpty).toBe(true); + expect(buf.drain()).toBe(""); + }); + + it("uses the drop label in the marker", () => { + const buf = new CappedTextBuffer(1, "earlier subagent output"); + buf.append("ab"); // drop 1 + expect(buf.drain()).toBe("[... 1 chars of earlier subagent output dropped ...]\nb"); + }); + + it("stays non-empty when only dropped chars remain unread", () => { + const buf = new CappedTextBuffer(0, "earlier output"); + buf.append("ab"); // cap 0 -> everything dropped, text empty but tally=2 + expect(buf.isEmpty).toBe(false); + expect(buf.drain()).toBe("[... 2 chars of earlier output dropped ...]\n"); + }); +}); diff --git a/packages/core/test/tool-call-ids.test.ts b/packages/core/test/tool-call-ids.test.ts new file mode 100644 index 0000000..5ee7535 --- /dev/null +++ b/packages/core/test/tool-call-ids.test.ts @@ -0,0 +1,71 @@ +/** + * ToolCallIdAllocator / stripToolCallIdSuffix unit tests: unique ids pass through, collisions + * get a `#n` suffix (n from 2, first free slot), markUsed seeds the taken set, and the suffix + * strip is shape-based and idempotent. + */ +import { describe, expect, it } from "vitest"; +import { ToolCallIdAllocator, stripToolCallIdSuffix } from "../src/llm/tool-call-ids.js"; + +describe("ToolCallIdAllocator", () => { + it("passes a unique id through unchanged", () => { + const a = new ToolCallIdAllocator(); + expect(a.allocate("call_abc")).toBe("call_abc"); + }); + + it("disambiguates repeats with #n starting at 2", () => { + const a = new ToolCallIdAllocator(); + expect(a.allocate("get_weather")).toBe("get_weather"); + expect(a.allocate("get_weather")).toBe("get_weather#2"); + expect(a.allocate("get_weather")).toBe("get_weather#3"); + }); + + it("keeps distinct base ids independent", () => { + const a = new ToolCallIdAllocator(); + expect(a.allocate("f")).toBe("f"); + expect(a.allocate("g")).toBe("g"); + expect(a.allocate("f")).toBe("f#2"); + }); + + it("skips ids already reserved by markUsed", () => { + const a = new ToolCallIdAllocator(); + a.markUsed("dupe"); + a.markUsed("dupe#2"); + expect(a.allocate("dupe")).toBe("dupe#3"); + }); + + it("markUsed twice is harmless", () => { + const a = new ToolCallIdAllocator(); + a.markUsed("x"); + a.markUsed("x"); + expect(a.allocate("x")).toBe("x#2"); + }); +}); + +describe("stripToolCallIdSuffix", () => { + it("strips a #n suffix", () => { + expect(stripToolCallIdSuffix("get_weather#2")).toBe("get_weather"); + }); + + it("returns an unsuffixed id as-is", () => { + expect(stripToolCallIdSuffix("call_abc")).toBe("call_abc"); + expect(stripToolCallIdSuffix("toolu_123")).toBe("toolu_123"); + }); + + it("is idempotent", () => { + const once = stripToolCallIdSuffix("f#3"); + expect(stripToolCallIdSuffix(once)).toBe("f"); + }); + + it("only strips a trailing #, not other hashes", () => { + expect(stripToolCallIdSuffix("a#b")).toBe("a#b"); + expect(stripToolCallIdSuffix("a#2b")).toBe("a#2b"); + }); + + it("round-trips an allocated collision id back to the provider id", () => { + const a = new ToolCallIdAllocator(); + a.allocate("name"); + const dup = a.allocate("name"); + expect(dup).toBe("name#2"); + expect(stripToolCallIdSuffix(dup)).toBe("name"); + }); +});