import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { DEFAULT_TOOL_BUDGET_BLOCK, initialToolBudgetState, isToolBudgetBlockedMessage, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge, toolBudgetState, validateToolBudgetConfig, } from "../../src/runs/shared/tool-budget.ts"; describe("tool-budget module", () => { it("defaults block tools to read/search tools", () => { const resolved = validateToolBudgetConfig({ hard: 5 }); assert.deepEqual(resolved.budget, { hard: 5, block: [...DEFAULT_TOOL_BUDGET_BLOCK] }); }); it("accepts soft and wildcard block", () => { const resolved = validateToolBudgetConfig({ soft: 2, hard: 4, block: "*" }); assert.deepEqual(resolved.budget, { soft: 2, hard: 4, block: "*" }); }); it("accepts a zero hard limit only when the internal minimum opts in", () => { assert.deepEqual( validateToolBudgetConfig({ hard: 0, block: "*" }, "toolBudget", { minimumHard: 0 }).budget, { hard: 0, block: "*" }, ); assert.equal( validateToolBudgetConfig({ soft: 0, hard: 0, block: "*" }, "toolBudget", { minimumHard: 0 }).error, "toolBudget.soft must be an integer >= 1 when provided.", ); }); it("rejects unsafe configs", () => { assert.equal(validateToolBudgetConfig({ hard: 0 }).error, "toolBudget.hard must be an integer >= 1."); assert.equal(validateToolBudgetConfig({ soft: 5, hard: 4 }).error, "toolBudget.soft must be <= toolBudget.hard."); assert.equal(validateToolBudgetConfig({ hard: 4, block: [] }).error, "toolBudget.block must contain at least one tool name."); assert.equal(validateToolBudgetConfig({ hard: 4, block: [""] }).error, "toolBudget.block must contain non-empty tool names."); }); it("tracks state and block decisions", () => { const budget = { soft: 2, hard: 3, block: ["read"] }; assert.deepEqual(initialToolBudgetState(budget), { soft: 2, hard: 3, block: ["read"], toolCount: 0, outcome: "within-budget" }); assert.equal(toolBudgetState(budget, 2).outcome, "soft-reached"); assert.equal(toolBudgetState(budget, 4, "read").outcome, "hard-blocked"); assert.equal(shouldBlockToolForBudget(budget, "read", 4), true); assert.equal(shouldBlockToolForBudget(budget, "write", 4), false); assert.equal(shouldBlockToolForBudget({ hard: 0, block: "*" }, "read", 1), true); }); it("recognizes only the exact runtime block message for this budget and tool", () => { const budget = { hard: 0, block: "*" }; const exact = toolBudgetBlockedMessage(budget, "bash", 1); assert.equal(isToolBudgetBlockedMessage(budget, exact, "bash"), true); assert.equal(isToolBudgetBlockedMessage({ hard: 2, block: "*" }, toolBudgetBlockedMessage({ hard: 2, block: "*" }, "bash", 3), "bash"), true, "plural count past the hard limit"); assert.equal(isToolBudgetBlockedMessage(budget, toolBudgetBlockedMessage({ hard: 3, block: "*" }, "bash", 4), "bash"), false, "different hard limit"); assert.equal(isToolBudgetBlockedMessage(budget, toolBudgetBlockedMessage(budget, "read", 1), "bash"), false, "different tool name"); assert.equal(isToolBudgetBlockedMessage(budget, `src/log.md:12:${exact}`, "bash"), false, "phrase embedded in ordinary tool output"); assert.equal(isToolBudgetBlockedMessage(budget, `${exact}\nnext line`, "bash"), false, "trailing output lines"); assert.equal(isToolBudgetBlockedMessage(budget, exact, undefined), false, "missing resolved tool name fails closed"); assert.equal(isToolBudgetBlockedMessage(budget, exact.replace("after 1 tool call", "after 0 tool call"), "bash"), false, "blocked count must exceed the hard limit"); }); it("formats user-facing budget messages", () => { const budget = { soft: 2, hard: 3, block: ["read"] }; assert.match(toolBudgetSoftNudge(budget, 2), /soft limit reached after 2 tool calls/); assert.match(toolBudgetBlockedMessage(budget, "read", 4), /'read' tool is blocked/); }); });