tool-define.test.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { describe, test, expect } from "bun:test"
  2. import z from "zod"
  3. import { Tool } from "../../src/tool/tool"
  4. const params = z.object({ input: z.string() })
  5. const defaultArgs = { input: "test" }
  6. function makeTool(id: string, executeFn?: () => void) {
  7. return {
  8. description: "test tool",
  9. parameters: params,
  10. async execute() {
  11. executeFn?.()
  12. return { title: "test", output: "ok", metadata: {} }
  13. },
  14. }
  15. }
  16. describe("Tool.define", () => {
  17. test("object-defined tool does not mutate the original init object", async () => {
  18. const original = makeTool("test")
  19. const originalExecute = original.execute
  20. const tool = Tool.define("test-tool", original)
  21. await tool.init()
  22. await tool.init()
  23. await tool.init()
  24. // The original object's execute should never be overwritten
  25. expect(original.execute).toBe(originalExecute)
  26. })
  27. test("object-defined tool does not accumulate wrapper layers across init() calls", async () => {
  28. let executeCalls = 0
  29. const tool = Tool.define("test-tool", makeTool("test", () => executeCalls++))
  30. // Call init() many times to simulate many agentic steps
  31. for (let i = 0; i < 100; i++) {
  32. await tool.init()
  33. }
  34. // Resolve the tool and call execute
  35. const resolved = await tool.init()
  36. executeCalls = 0
  37. // Capture the stack trace inside execute to measure wrapper depth
  38. let stackInsideExecute = ""
  39. const origExec = resolved.execute
  40. resolved.execute = async (args: any, ctx: any) => {
  41. const result = await origExec.call(resolved, args, ctx)
  42. const err = new Error()
  43. stackInsideExecute = err.stack || ""
  44. return result
  45. }
  46. await resolved.execute(defaultArgs, {} as any)
  47. expect(executeCalls).toBe(1)
  48. // Count how many times tool.ts appears in the stack.
  49. // With the fix: 1 wrapper layer (from the most recent init()).
  50. // Without the fix: 101 wrapper layers from accumulated closures.
  51. const toolTsFrames = stackInsideExecute.split("\n").filter((l) => l.includes("tool.ts")).length
  52. expect(toolTsFrames).toBeLessThan(5)
  53. })
  54. test("function-defined tool returns fresh objects and is unaffected", async () => {
  55. const tool = Tool.define("test-fn-tool", () => Promise.resolve(makeTool("test")))
  56. const first = await tool.init()
  57. const second = await tool.init()
  58. // Function-defined tools return distinct objects each time
  59. expect(first).not.toBe(second)
  60. })
  61. test("object-defined tool returns distinct objects per init() call", async () => {
  62. const tool = Tool.define("test-copy", makeTool("test"))
  63. const first = await tool.init()
  64. const second = await tool.init()
  65. // Each init() should return a separate object so wrappers don't accumulate
  66. expect(first).not.toBe(second)
  67. })
  68. test("validation still works after many init() calls", async () => {
  69. const tool = Tool.define("test-validation", {
  70. description: "validation test",
  71. parameters: z.object({ count: z.number().int().positive() }),
  72. async execute(args) {
  73. return { title: "test", output: String(args.count), metadata: {} }
  74. },
  75. })
  76. for (let i = 0; i < 100; i++) {
  77. await tool.init()
  78. }
  79. const resolved = await tool.init()
  80. const result = await resolved.execute({ count: 42 }, {} as any)
  81. expect(result.output).toBe("42")
  82. await expect(resolved.execute({ count: -1 }, {} as any)).rejects.toThrow("invalid arguments")
  83. })
  84. })