Files
ralph-vibe/src/__tests__/claude.test.ts
Debian ae0165a802 Implement Phase 2: Core API Integration
- Add Claude API client with Anthropic SDK
  - Retry logic with exponential backoff (3 attempts)
  - Handle auth errors, rate limits, and server errors
  - Architecture classification with JSON parsing
  - Research query generation from architecture
- Add Perplexity API client with native fetch
  - Retry logic with exponential backoff
  - Parse responses with sources and citations
  - Execute multiple research queries
- Add prompt templates for architecture classification
- Add default query generation as fallback
- Add ArchitectureGenerator combining Claude + Perplexity
- Add 44 new tests (80 total, all passing)

All Phase 2 acceptance criteria met:
- Claude client sends prompts and retries on failure
- Perplexity client searches and parses with sources
- Architecture classification returns valid JSON
- Research queries generated (4-6 per architecture)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 12:17:42 +00:00

200 lines
5.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
// Create a mock for the messages.create function
const mockCreate = vi.fn();
// Mock the Anthropic SDK as a class
vi.mock('@anthropic-ai/sdk', () => {
return {
default: class MockAnthropic {
messages = {
create: mockCreate,
};
},
};
});
// Import after mocking
import { ClaudeClient } from '../clients/claude.js';
describe('ClaudeClient', () => {
let client: ClaudeClient;
beforeEach(() => {
vi.clearAllMocks();
client = new ClaudeClient({ apiKey: 'test-key' });
});
describe('analyze', () => {
it('returns text response from Claude', async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Hello, world!' }],
});
const result = await client.analyze('test prompt');
expect(result).toBe('Hello, world!');
});
it('throws error when no text block in response', async () => {
mockCreate.mockResolvedValueOnce({
content: [],
});
await expect(client.analyze('test prompt')).rejects.toThrow('No text response from Claude');
});
it('throws error for invalid API key', async () => {
const authError = new Error('Unauthorized') as Error & { status: number };
authError.status = 401;
mockCreate.mockRejectedValueOnce(authError);
await expect(client.analyze('test prompt')).rejects.toThrow('Invalid Claude API key');
});
it('retries on server errors', async () => {
const serverError = new Error('Server Error') as Error & { status: number };
serverError.status = 500;
mockCreate
.mockRejectedValueOnce(serverError)
.mockRejectedValueOnce(serverError)
.mockResolvedValueOnce({
content: [{ type: 'text', text: 'Success after retry' }],
});
const result = await client.analyze('test prompt');
expect(result).toBe('Success after retry');
expect(mockCreate).toHaveBeenCalledTimes(3);
});
});
describe('classifyArchitecture', () => {
it('parses valid architecture JSON', async () => {
const architectureJson = JSON.stringify({
appType: 'cli',
appTypeReason: 'Command-line tool',
interfaces: ['cli_args'],
interfacesReason: 'CLI arguments',
persistence: 'file_based',
persistenceReason: 'File storage',
deployment: 'package_registry',
deploymentReason: 'npm',
suggestedTechStack: {
language: 'TypeScript',
framework: 'Commander.js',
reasoning: 'CLI framework',
},
});
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: architectureJson }],
});
const result = await client.classifyArchitecture('A CLI tool for converting files');
expect(result.appType).toBe('cli');
expect(result.interfaces).toContain('cli_args');
expect(result.persistence).toBe('file_based');
expect(result.deployment).toBe('package_registry');
});
it('handles JSON wrapped in markdown code blocks', async () => {
const architectureJson = `\`\`\`json
{
"appType": "web",
"appTypeReason": "Web app",
"interfaces": ["rest_api"],
"interfacesReason": "REST API",
"persistence": "remote_db",
"persistenceReason": "Database",
"deployment": "cloud",
"deploymentReason": "Cloud deployment",
"suggestedTechStack": {
"language": "TypeScript",
"framework": "Express",
"reasoning": "Node.js backend"
}
}
\`\`\``;
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: architectureJson }],
});
const result = await client.classifyArchitecture('A web application');
expect(result.appType).toBe('web');
expect(result.interfaces).toContain('rest_api');
});
it('throws error for invalid JSON', async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: 'not valid json' }],
});
await expect(client.classifyArchitecture('test')).rejects.toThrow('Failed to parse architecture');
});
it('throws error for missing required fields', async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: '{"appType": "web"}' }],
});
await expect(client.classifyArchitecture('test')).rejects.toThrow('Missing required fields');
});
});
describe('generateResearchQueries', () => {
const mockArchitecture = {
appType: 'cli' as const,
appTypeReason: 'Command-line tool',
interfaces: ['cli_args' as const],
interfacesReason: 'CLI arguments',
persistence: 'file_based' as const,
persistenceReason: 'File storage',
deployment: 'package_registry' as const,
deploymentReason: 'npm',
suggestedTechStack: {
language: 'TypeScript',
framework: 'Commander.js',
reasoning: 'CLI framework',
},
};
it('parses valid query array', async () => {
const queries = ['query1', 'query2', 'query3', 'query4'];
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: JSON.stringify(queries) }],
});
const result = await client.generateResearchQueries(mockArchitecture);
expect(result).toEqual(queries);
expect(result.length).toBe(4);
});
it('handles JSON wrapped in markdown code blocks', async () => {
const queriesJson = `\`\`\`json
["query1", "query2", "query3", "query4"]
\`\`\``;
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: queriesJson }],
});
const result = await client.generateResearchQueries(mockArchitecture);
expect(result).toEqual(['query1', 'query2', 'query3', 'query4']);
});
it('throws error for non-array response', async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: 'text', text: '{"not": "an array"}' }],
});
await expect(client.generateResearchQueries(mockArchitecture)).rejects.toThrow('not an array');
});
});
});