mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
- Renamed test case to clarify that it handles conversation history with sdkSessionId using the resume option. - Updated assertions to verify that the sdk.query method is called with the correct options when a session ID is provided.
404 lines
11 KiB
TypeScript
404 lines
11 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { ClaudeProvider } from "@/providers/claude-provider.js";
|
|
import * as sdk from "@anthropic-ai/claude-agent-sdk";
|
|
import { collectAsyncGenerator } from "../../utils/helpers.js";
|
|
|
|
vi.mock("@anthropic-ai/claude-agent-sdk");
|
|
|
|
describe("claude-provider.ts", () => {
|
|
let provider: ClaudeProvider;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
provider = new ClaudeProvider();
|
|
delete process.env.ANTHROPIC_API_KEY;
|
|
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
});
|
|
|
|
describe("getName", () => {
|
|
it("should return 'claude' as provider name", () => {
|
|
expect(provider.getName()).toBe("claude");
|
|
});
|
|
});
|
|
|
|
describe("executeQuery", () => {
|
|
it("should execute simple text query", async () => {
|
|
const mockMessages = [
|
|
{ type: "text", text: "Response 1" },
|
|
{ type: "text", text: "Response 2" },
|
|
];
|
|
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
for (const msg of mockMessages) {
|
|
yield msg;
|
|
}
|
|
})()
|
|
);
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: "Hello",
|
|
cwd: "/test",
|
|
});
|
|
|
|
const results = await collectAsyncGenerator(generator);
|
|
|
|
expect(results).toHaveLength(2);
|
|
expect(results[0]).toEqual({ type: "text", text: "Response 1" });
|
|
expect(results[1]).toEqual({ type: "text", text: "Response 2" });
|
|
});
|
|
|
|
it("should pass correct options to SDK", async () => {
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
yield { type: "text", text: "test" };
|
|
})()
|
|
);
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: "Test prompt",
|
|
model: "claude-opus-4-5-20251101",
|
|
cwd: "/test/dir",
|
|
systemPrompt: "You are helpful",
|
|
maxTurns: 10,
|
|
allowedTools: ["Read", "Write"],
|
|
});
|
|
|
|
await collectAsyncGenerator(generator);
|
|
|
|
expect(sdk.query).toHaveBeenCalledWith({
|
|
prompt: "Test prompt",
|
|
options: expect.objectContaining({
|
|
model: "claude-opus-4-5-20251101",
|
|
systemPrompt: "You are helpful",
|
|
maxTurns: 10,
|
|
cwd: "/test/dir",
|
|
allowedTools: ["Read", "Write"],
|
|
permissionMode: "acceptEdits",
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("should use default allowed tools when not specified", async () => {
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
yield { type: "text", text: "test" };
|
|
})()
|
|
);
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: "Test",
|
|
cwd: "/test",
|
|
});
|
|
|
|
await collectAsyncGenerator(generator);
|
|
|
|
expect(sdk.query).toHaveBeenCalledWith({
|
|
prompt: "Test",
|
|
options: expect.objectContaining({
|
|
allowedTools: [
|
|
"Read",
|
|
"Write",
|
|
"Edit",
|
|
"Glob",
|
|
"Grep",
|
|
"Bash",
|
|
"WebSearch",
|
|
"WebFetch",
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("should enable sandbox by default", async () => {
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
yield { type: "text", text: "test" };
|
|
})()
|
|
);
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: "Test",
|
|
cwd: "/test",
|
|
});
|
|
|
|
await collectAsyncGenerator(generator);
|
|
|
|
expect(sdk.query).toHaveBeenCalledWith({
|
|
prompt: "Test",
|
|
options: expect.objectContaining({
|
|
sandbox: {
|
|
enabled: true,
|
|
autoAllowBashIfSandboxed: true,
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("should pass abortController if provided", async () => {
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
yield { type: "text", text: "test" };
|
|
})()
|
|
);
|
|
|
|
const abortController = new AbortController();
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: "Test",
|
|
cwd: "/test",
|
|
abortController,
|
|
});
|
|
|
|
await collectAsyncGenerator(generator);
|
|
|
|
expect(sdk.query).toHaveBeenCalledWith({
|
|
prompt: "Test",
|
|
options: expect.objectContaining({
|
|
abortController,
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("should handle conversation history with sdkSessionId using resume option", async () => {
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
yield { type: "text", text: "test" };
|
|
})()
|
|
);
|
|
|
|
const conversationHistory = [
|
|
{ role: "user" as const, content: "Previous message" },
|
|
{ role: "assistant" as const, content: "Previous response" },
|
|
];
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: "Current message",
|
|
cwd: "/test",
|
|
conversationHistory,
|
|
sdkSessionId: "test-session-id",
|
|
});
|
|
|
|
await collectAsyncGenerator(generator);
|
|
|
|
// Should use resume option when sdkSessionId is provided with history
|
|
expect(sdk.query).toHaveBeenCalledWith({
|
|
prompt: "Current message",
|
|
options: expect.objectContaining({
|
|
resume: "test-session-id",
|
|
}),
|
|
});
|
|
});
|
|
|
|
it("should handle array prompt (with images)", async () => {
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
yield { type: "text", text: "test" };
|
|
})()
|
|
);
|
|
|
|
const arrayPrompt = [
|
|
{ type: "text", text: "Describe this" },
|
|
{ type: "image", source: { type: "base64", data: "..." } },
|
|
];
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: arrayPrompt as any,
|
|
cwd: "/test",
|
|
});
|
|
|
|
await collectAsyncGenerator(generator);
|
|
|
|
// Should pass an async generator as prompt for array inputs
|
|
const callArgs = vi.mocked(sdk.query).mock.calls[0][0];
|
|
expect(typeof callArgs.prompt).not.toBe("string");
|
|
});
|
|
|
|
it("should use maxTurns default of 20", async () => {
|
|
vi.mocked(sdk.query).mockReturnValue(
|
|
(async function* () {
|
|
yield { type: "text", text: "test" };
|
|
})()
|
|
);
|
|
|
|
const generator = provider.executeQuery({
|
|
prompt: "Test",
|
|
cwd: "/test",
|
|
});
|
|
|
|
await collectAsyncGenerator(generator);
|
|
|
|
expect(sdk.query).toHaveBeenCalledWith({
|
|
prompt: "Test",
|
|
options: expect.objectContaining({
|
|
maxTurns: 20,
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("detectInstallation", () => {
|
|
it("should return installed with SDK method", async () => {
|
|
const result = await provider.detectInstallation();
|
|
|
|
expect(result.installed).toBe(true);
|
|
expect(result.method).toBe("sdk");
|
|
});
|
|
|
|
it("should detect ANTHROPIC_API_KEY", async () => {
|
|
process.env.ANTHROPIC_API_KEY = "test-key";
|
|
|
|
const result = await provider.detectInstallation();
|
|
|
|
expect(result.hasApiKey).toBe(true);
|
|
expect(result.authenticated).toBe(true);
|
|
});
|
|
|
|
it("should detect CLAUDE_CODE_OAUTH_TOKEN", async () => {
|
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = "oauth-token";
|
|
|
|
const result = await provider.detectInstallation();
|
|
|
|
expect(result.hasApiKey).toBe(true);
|
|
expect(result.authenticated).toBe(true);
|
|
});
|
|
|
|
it("should return hasApiKey false when no keys present", async () => {
|
|
const result = await provider.detectInstallation();
|
|
|
|
expect(result.hasApiKey).toBe(false);
|
|
expect(result.authenticated).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("getAvailableModels", () => {
|
|
it("should return 4 Claude models", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
expect(models).toHaveLength(4);
|
|
});
|
|
|
|
it("should include Claude Opus 4.5", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
const opus = models.find((m) => m.id === "claude-opus-4-5-20251101");
|
|
expect(opus).toBeDefined();
|
|
expect(opus?.name).toBe("Claude Opus 4.5");
|
|
expect(opus?.provider).toBe("anthropic");
|
|
});
|
|
|
|
it("should include Claude Sonnet 4", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
const sonnet = models.find((m) => m.id === "claude-sonnet-4-20250514");
|
|
expect(sonnet).toBeDefined();
|
|
expect(sonnet?.name).toBe("Claude Sonnet 4");
|
|
});
|
|
|
|
it("should include Claude 3.5 Sonnet", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
const sonnet35 = models.find(
|
|
(m) => m.id === "claude-3-5-sonnet-20241022"
|
|
);
|
|
expect(sonnet35).toBeDefined();
|
|
});
|
|
|
|
it("should include Claude 3.5 Haiku", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
const haiku = models.find((m) => m.id === "claude-3-5-haiku-20241022");
|
|
expect(haiku).toBeDefined();
|
|
});
|
|
|
|
it("should mark Opus as default", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
const opus = models.find((m) => m.id === "claude-opus-4-5-20251101");
|
|
expect(opus?.default).toBe(true);
|
|
});
|
|
|
|
it("should all support vision and tools", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
models.forEach((model) => {
|
|
expect(model.supportsVision).toBe(true);
|
|
expect(model.supportsTools).toBe(true);
|
|
});
|
|
});
|
|
|
|
it("should have correct context windows", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
models.forEach((model) => {
|
|
expect(model.contextWindow).toBe(200000);
|
|
});
|
|
});
|
|
|
|
it("should have modelString field matching id", () => {
|
|
const models = provider.getAvailableModels();
|
|
|
|
models.forEach((model) => {
|
|
expect(model.modelString).toBe(model.id);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("supportsFeature", () => {
|
|
it("should support 'tools' feature", () => {
|
|
expect(provider.supportsFeature("tools")).toBe(true);
|
|
});
|
|
|
|
it("should support 'text' feature", () => {
|
|
expect(provider.supportsFeature("text")).toBe(true);
|
|
});
|
|
|
|
it("should support 'vision' feature", () => {
|
|
expect(provider.supportsFeature("vision")).toBe(true);
|
|
});
|
|
|
|
it("should support 'thinking' feature", () => {
|
|
expect(provider.supportsFeature("thinking")).toBe(true);
|
|
});
|
|
|
|
it("should not support 'mcp' feature", () => {
|
|
expect(provider.supportsFeature("mcp")).toBe(false);
|
|
});
|
|
|
|
it("should not support 'cli' feature", () => {
|
|
expect(provider.supportsFeature("cli")).toBe(false);
|
|
});
|
|
|
|
it("should not support unknown features", () => {
|
|
expect(provider.supportsFeature("unknown")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("validateConfig", () => {
|
|
it("should validate config from base class", () => {
|
|
const result = provider.validateConfig();
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("config management", () => {
|
|
it("should get and set config", () => {
|
|
provider.setConfig({ apiKey: "test-key" });
|
|
|
|
const config = provider.getConfig();
|
|
expect(config.apiKey).toBe("test-key");
|
|
});
|
|
|
|
it("should merge config updates", () => {
|
|
provider.setConfig({ apiKey: "key1" });
|
|
provider.setConfig({ model: "model1" });
|
|
|
|
const config = provider.getConfig();
|
|
expect(config.apiKey).toBe("key1");
|
|
expect(config.model).toBe("model1");
|
|
});
|
|
});
|
|
});
|