Files
diegosouzapw--omniroute/tests/unit/claude-tool-search-beta-3974.test.ts
2026-07-13 13:39:12 +08:00

104 lines
4.7 KiB
TypeScript

/**
* TDD regression for #3974: the client-negotiated `anthropic-beta:
* tool-search-tool-2025-10-19` is dropped on BOTH Claude code paths, so the
* claude.ai backend rejects deferred-tool requests with
* `400 Tool reference '<name>' not found in available tools`.
*
* - default executor (`claude` uses executor:"default"): buildHeaders set the
* beta from the static registry header (ANTHROPIC_BETA_CLAUDE_OAUTH, which
* lacks tool-search) and the client-header forward block is allowlist-only.
* - selectBetaFlags path (base.ts): rebuilds the beta from a fixed vocabulary
* and only reads the client beta to GATE thinking/effort, never to ADD.
*
* Fix: allowlist-merge the client beta on both paths (preserving #3415 — never
* force thinking/effort the client did not send).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
const { selectBetaFlags } = await import("../../open-sse/executors/claudeIdentity.ts");
const { mergeClientAnthropicBeta, FORWARDABLE_CLIENT_BETAS } = await import(
"../../open-sse/config/anthropicHeaders.ts"
);
const TOOL_SEARCH = "tool-search-tool-2025-10-19";
function fullAgentBody(model: string) {
return {
model,
system: "You are a coding agent.",
tools: [{ name: "read_file", description: "x", input_schema: { type: "object" } }],
};
}
function betaTokens(headers: Record<string, string>): string[] {
const key = Object.keys(headers).find((k) => k.toLowerCase() === "anthropic-beta");
return key ? headers[key].split(",").map((s) => s.trim()) : [];
}
// ── helper unit (allowlist-merge) ────────────────────────────────────────────
test("#3974 mergeClientAnthropicBeta appends an allowlisted client beta once, preserving base order", () => {
const out = mergeClientAnthropicBeta(
"claude-code-20250219,oauth-2025-04-20",
`oauth-2025-04-20,${TOOL_SEARCH}`
);
const tokens = out.split(",");
assert.deepEqual(tokens.slice(0, 2), ["claude-code-20250219", "oauth-2025-04-20"]);
assert.equal(tokens.filter((t) => t === TOOL_SEARCH).length, 1, "appended once, no dup");
});
test("#3974 mergeClientAnthropicBeta ignores non-allowlisted client betas", () => {
const out = mergeClientAnthropicBeta("oauth-2025-04-20", "some-random-future-beta-2099-01-01");
assert.equal(out, "oauth-2025-04-20", "only allowlisted betas are forwarded");
assert.ok(FORWARDABLE_CLIENT_BETAS.includes(TOOL_SEARCH));
});
// ── selectBetaFlags path (base.ts) ───────────────────────────────────────────
test("#3974 selectBetaFlags + merge preserves the client tool-search beta", () => {
const clientBeta = `claude-code-20250219,oauth-2025-04-20,${TOOL_SEARCH}`;
const out = mergeClientAnthropicBeta(
selectBetaFlags(fullAgentBody("claude-opus-4-8"), null, clientBeta),
clientBeta
);
assert.ok(out.split(",").includes(TOOL_SEARCH));
});
test("#3974 merge does not force interleaved-thinking the client omitted (guards #3415)", () => {
const clientBeta = `oauth-2025-04-20,${TOOL_SEARCH}`;
const out = mergeClientAnthropicBeta(
selectBetaFlags(fullAgentBody("claude-opus-4-8"), null, clientBeta),
clientBeta
);
assert.ok(out.split(",").includes(TOOL_SEARCH));
assert.ok(
!out.split(",").includes("interleaved-thinking-2025-05-14"),
"must NOT force interleaved-thinking when the client did not request it"
);
});
// ── default executor path (default.ts buildHeaders) ──────────────────────────
test("#3974 DefaultExecutor('claude') merges the client tool-search beta into outbound headers", () => {
const executor = new DefaultExecutor("claude");
const headers = executor.buildHeaders({ accessToken: "sk-ant-oat-x" }, true, {
"anthropic-beta": `oauth-2025-04-20,${TOOL_SEARCH}`,
}) as Record<string, string>;
const tokens = betaTokens(headers);
assert.ok(tokens.includes(TOOL_SEARCH), `outbound beta missing tool-search: ${tokens.join(",")}`);
assert.equal(tokens.filter((t) => t === TOOL_SEARCH).length, 1, "no duplicate");
// The provider's own base betas are still present.
assert.ok(tokens.includes("claude-code-20250219"));
});
test("#3974 DefaultExecutor('claude') without a client beta leaves the static set unchanged", () => {
const executor = new DefaultExecutor("claude");
const headers = executor.buildHeaders({ accessToken: "sk-ant-oat-x" }, true, {}) as Record<
string,
string
>;
assert.ok(!betaTokens(headers).includes(TOOL_SEARCH), "must not invent tool-search unprompted");
});