| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108 |
- import { describe, test, expect } from "bun:test"
- import z from "zod"
- import { Tool } from "../../src/tool/tool"
- const params = z.object({ input: z.string() })
- const defaultArgs = { input: "test" }
- function makeTool(id: string, executeFn?: () => void) {
- return {
- description: "test tool",
- parameters: params,
- async execute() {
- executeFn?.()
- return { title: "test", output: "ok", metadata: {} }
- },
- }
- }
- describe("Tool.define", () => {
- test("object-defined tool does not mutate the original init object", async () => {
- const original = makeTool("test")
- const originalExecute = original.execute
- const tool = Tool.define("test-tool", original)
- await tool.init()
- await tool.init()
- await tool.init()
- // The original object's execute should never be overwritten
- expect(original.execute).toBe(originalExecute)
- })
- test("object-defined tool does not accumulate wrapper layers across init() calls", async () => {
- let executeCalls = 0
- const tool = Tool.define("test-tool", makeTool("test", () => executeCalls++))
- // Call init() many times to simulate many agentic steps
- for (let i = 0; i < 100; i++) {
- await tool.init()
- }
- // Resolve the tool and call execute
- const resolved = await tool.init()
- executeCalls = 0
- // Capture the stack trace inside execute to measure wrapper depth
- let stackInsideExecute = ""
- const origExec = resolved.execute
- resolved.execute = async (args: any, ctx: any) => {
- const result = await origExec.call(resolved, args, ctx)
- const err = new Error()
- stackInsideExecute = err.stack || ""
- return result
- }
- await resolved.execute(defaultArgs, {} as any)
- expect(executeCalls).toBe(1)
- // Count how many times tool.ts appears in the stack.
- // With the fix: 1 wrapper layer (from the most recent init()).
- // Without the fix: 101 wrapper layers from accumulated closures.
- const toolTsFrames = stackInsideExecute.split("\n").filter((l) => l.includes("tool.ts")).length
- expect(toolTsFrames).toBeLessThan(5)
- })
- test("function-defined tool returns fresh objects and is unaffected", async () => {
- const tool = Tool.define("test-fn-tool", () => Promise.resolve(makeTool("test")))
- const first = await tool.init()
- const second = await tool.init()
- // Function-defined tools return distinct objects each time
- expect(first).not.toBe(second)
- })
- test("object-defined tool returns distinct objects per init() call", async () => {
- const tool = Tool.define("test-copy", makeTool("test"))
- const first = await tool.init()
- const second = await tool.init()
- // Each init() should return a separate object so wrappers don't accumulate
- expect(first).not.toBe(second)
- })
- test("validation still works after many init() calls", async () => {
- const tool = Tool.define("test-validation", {
- description: "validation test",
- parameters: z.object({ count: z.number().int().positive() }),
- async execute(args) {
- return { title: "test", output: String(args.count), metadata: {} }
- },
- })
- for (let i = 0; i < 100; i++) {
- await tool.init()
- }
- const resolved = await tool.init()
- const result = await resolved.execute({ count: 42 }, {} as any)
- expect(result.output).toBe("42")
- await expect(resolved.execute({ count: -1 }, {} as any)).rejects.toThrow("invalid arguments")
- })
- })
|