import { AnthropicBackend } from "../backends/anthropic.js"; import { ChatCompletionResponse } from "../backends/llm-base.js"; describe("AnthropicBackend", () => { const originalFetch = globalThis.fetch; let fetchCalls: Array<{ url: string; headers: Record; body: string }>; beforeEach(() => { fetchCalls = []; }); afterEach(() => { globalThis.fetch = originalFetch; delete process.env.TEST_ANTHROPIC_KEY; delete process.env.TEST_ANTHROPIC_KEY_EMPTY; }); function mockFetch(response: Record, status = 200): void { globalThis.fetch = ((url: string, init: RequestInit) => { fetchCalls.push({ url, headers: (init.headers as Record) || {}, body: init.body as string, }); return Promise.resolve({ ok: status >= 200 && status < 300, status, text: () => Promise.resolve(JSON.stringify(response)), json: () => Promise.resolve(response), } as Response); }) as typeof fetch; } function makeAnthropicResponse(text: string, usage = { input_tokens: 10, output_tokens: 20 }): Record { return { content: [{ type: "text", text }], usage, model: "claude-sonnet-4-20250514", }; } describe("isAvailable", () => { it("returns true when API key is present", async () => { process.env.TEST_ANTHROPIC_KEY = "sk-ant-test-key-123"; const backend = new AnthropicBackend({ base_url: "https://api.anthropic.com", api_key_env: "TEST_ANTHROPIC_KEY", model: "claude-sonnet-4-20250514", model_profile: "quality", }); expect(await backend.isAvailable()).toBe(true); }); it("returns false when API key is absent", async () => { const backend = new AnthropicBackend({ base_url: "https://api.anthropic.com", api_key_env: "NONEXISTENT_ANTHROPIC_KEY_VAR_99999", model: "claude-sonnet-4-20250514", model_profile: "quality", }); expect(await backend.isAvailable()).toBe(false); }); }); describe("resolveModel", () => { it("returns config.model when set", async () => { process.env.TEST_ANTHROPIC_KEY = "sk-ant-test"; mockFetch(makeAnthropicResponse('{"success": true, "output": "done"}')); const backend = new AnthropicBackend({ base_url: "https://api.anthropic.com", api_key_env: "TEST_ANTHROPIC_KEY", model: "claude-3-haiku-20240307", model_profile: "speed", }); const request = { persona: "executor" as const, workflow: "execute", task: "test", context: { project_path: "/tmp", phase: 1, stage: "execute" as const, specification: "", config_path: "", }, autonomy: "full" as const, }; await backend.execute(request); const body = JSON.parse(fetchCalls[0].body); expect(body.model).toBe("claude-3-haiku-20240307"); }); it("defaults to claude-sonnet-4-20250514 when model not specified", async () => { process.env.TEST_ANTHROPIC_KEY = "sk-ant-test"; mockFetch(makeAnthropicResponse('{"success": true, "output": "done"}')); const backend = new AnthropicBackend({ base_url: "https://api.anthropic.com", api_key_env: "TEST_ANTHROPIC_KEY", model: "", model_profile: "quality", }); const request = { persona: "executor" as const, workflow: "execute", task: "test", context: { project_path: "/tmp", phase: 1, stage: "execute" as const, specification: "", config_path: "", }, autonomy: "full" as const, }; await backend.execute(request); const body = JSON.parse(fetchCalls[0].body); expect(body.model).toBe("claude-sonnet-4-20250514"); }); }); describe("callModel request format", () => { it("sends correct URL, x-api-key header, anthropic-version header, system field, max_tokens", async () => { process.env.TEST_ANTHROPIC_KEY = "sk-ant-test-key-abc"; mockFetch(makeAnthropicResponse('{"success": true, "output": "done"}')); const backend = new AnthropicBackend({ base_url: "https://api.anthropic.com", api_key_env: "TEST_ANTHROPIC_KEY", model: "claude-sonnet-4-20250514", model_profile: "quality", }); const request = { persona: "executor" as const, workflow: "execute", task: "Do the thing", context: { project_path: "/tmp", phase: 1, stage: "execute" as const, specification: "", config_path: "", }, autonomy: "full" as const, }; await backend.execute(request); expect(fetchCalls.length).toBe(1); expect(fetchCalls[0].url).toBe("https://api.anthropic.com/v1/messages"); expect(fetchCalls[0].headers["x-api-key"]).toBe("sk-ant-test-key-abc"); expect(fetchCalls[0].headers["anthropic-version"]).toBe("2023-06-01"); expect(fetchCalls[0].headers["Content-Type"]).toBe("application/json"); expect(fetchCalls[0].headers["Authorization"]).toBeUndefined(); const body = JSON.parse(fetchCalls[0].body); expect(body.model).toBe("claude-sonnet-4-20250514"); expect(body.max_tokens).toBe(4096); expect(typeof body.system).toBe("string"); expect(body.system.length).toBeGreaterThan(0); expect(Array.isArray(body.messages)).toBe(true); expect(body.messages.length).toBeGreaterThanOrEqual(1); }); }); describe("custom base_url override", () => { it("sends request to custom base_url", async () => { process.env.TEST_ANTHROPIC_KEY = "sk-ant-test"; mockFetch(makeAnthropicResponse('{"success": true, "output": "done"}')); const backend = new AnthropicBackend({ base_url: "https://custom-proxy.example.com/api", api_key_env: "TEST_ANTHROPIC_KEY", model: "claude-sonnet-4-20250514", model_profile: "quality", }); const request = { persona: "executor" as const, workflow: "execute", task: "test", context: { project_path: "/tmp", phase: 1, stage: "execute" as const, specification: "", config_path: "", }, autonomy: "full" as const, }; await backend.execute(request); expect(fetchCalls[0].url).toBe("https://custom-proxy.example.com/api/v1/messages"); }); }); });