feat(ci): v0.9.0 — Distribution & Expansion milestone complete
---ci---
project: ci
phase: 6
milestone: v0.9
status: complete
artifacts:
tags: [v0.9.0]
decisions:
- id: D-047
decision: v0.9 theme = Distribution & Expansion
rationale: npm publish + OpenAI/Anthropic backends + agent flesh + parallel execution
confidence: 0.92
- id: D-049
decision: Feature milestone — patch tags v0.8.1-v0.8.6 then v0.9.0
rationale: OpenAI backend, agent flesh, npm publish all feat
confidence: 0.95
- id: D-059
decision: Rename OllamaBaseBackend to LLMBaseBackend + thin OllamaBaseBackend subclass
rationale: 15 of 17 methods backend-agnostic
confidence: 0.92
- id: D-060
decision: OpenAI/Anthropic backends use native fetch() not SDK packages
rationale: No dependency bloat; fetch native in Node 18+
confidence: 0.85
- id: D-066
decision: Concurrency limiter internal (no p-limit dependency)
rationale: 15 lines; avoids dependency for trivial feature
confidence: 0.90
- id: D-067
decision: Promise.allSettled for review agents at orchestrator lines 373-400
rationale: Current sequential loop replaced with parallel execution
confidence: 0.88
requirements:
covered: [PUBLISH-01, PUBLISH-02, PUBLISH-03, PUBLISH-04, OPENAI-01, OPENAI-02, OPENAI-03, OPENAI-04, OPENAI-05, FLESH-01, FLESH-02, FLESH-03, FLESH-04, FLESH-05, ANTHROPIC-01, ANTHROPIC-02, FLESH-06, FLESH-07, NPM-01, NPM-02, PARALLEL-01, PARALLEL-02, PARALLEL-03, INTEG-01, INTEG-02, INTEG-03, INTEG-04, INTEG-05]
---/ci---
6 phases, 28 tasks, 4077 net lines added, 57 test suites, 527 tests, zero stub agents
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
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<string, string>; 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<string, unknown>, status = 200): void {
|
||||
globalThis.fetch = ((url: string, init: RequestInit) => {
|
||||
fetchCalls.push({
|
||||
url,
|
||||
headers: (init.headers as Record<string, string>) || {},
|
||||
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<string, unknown> {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user