mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-29 01:07:04 +00:00
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
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")
|
|
})
|
|
})
|