Files
claude-task-master/tests/unit/ai-services.test.js
2025-04-09 00:25:27 +02:00

408 lines
10 KiB
JavaScript

/**
* AI Services module tests
*/
import { jest } from '@jest/globals';
import { parseSubtasksFromText } from '../../scripts/modules/ai-services.js';
// Create a mock log function we can check later
const mockLog = jest.fn();
// Mock dependencies
jest.mock('@anthropic-ai/sdk', () => {
const mockCreate = jest.fn().mockResolvedValue({
content: [{ text: 'AI response' }]
});
const mockAnthropicInstance = {
messages: {
create: mockCreate
}
};
const mockAnthropicConstructor = jest
.fn()
.mockImplementation(() => mockAnthropicInstance);
return {
Anthropic: mockAnthropicConstructor
};
});
// Use jest.fn() directly for OpenAI mock
const mockOpenAIInstance = {
chat: {
completions: {
create: jest.fn().mockResolvedValue({
choices: [{ message: { content: 'Perplexity response' } }]
})
}
}
};
const mockOpenAI = jest.fn().mockImplementation(() => mockOpenAIInstance);
jest.mock('openai', () => {
return { default: mockOpenAI };
});
jest.mock('dotenv', () => ({
config: jest.fn()
}));
jest.mock('../../scripts/modules/utils.js', () => ({
CONFIG: {
model: 'claude-3-sonnet-20240229',
temperature: 0.7,
maxTokens: 4000
},
log: mockLog,
sanitizePrompt: jest.fn((text) => text)
}));
jest.mock('../../scripts/modules/ui.js', () => ({
startLoadingIndicator: jest.fn().mockReturnValue('mockLoader'),
stopLoadingIndicator: jest.fn()
}));
// Mock anthropic global object
global.anthropic = {
messages: {
create: jest.fn().mockResolvedValue({
content: [
{
text: '[{"id": 1, "title": "Test", "description": "Test", "dependencies": [], "details": "Test"}]'
}
]
})
}
};
// Mock process.env
const originalEnv = process.env;
// Import Anthropic for testing constructor arguments
import { Anthropic } from '@anthropic-ai/sdk';
describe('AI Services Module', () => {
beforeEach(() => {
jest.clearAllMocks();
process.env = { ...originalEnv };
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
process.env.PERPLEXITY_API_KEY = 'test-perplexity-key';
});
afterEach(() => {
process.env = originalEnv;
});
describe('parseSubtasksFromText function', () => {
test('should parse subtasks from JSON text', () => {
const text = `Here's your list of subtasks:
[
{
"id": 1,
"title": "Implement database schema",
"description": "Design and implement the database schema for user data",
"dependencies": [],
"details": "Create tables for users, preferences, and settings"
},
{
"id": 2,
"title": "Create API endpoints",
"description": "Develop RESTful API endpoints for user operations",
"dependencies": [],
"details": "Implement CRUD operations for user management"
}
]
These subtasks will help you implement the parent task efficiently.`;
const result = parseSubtasksFromText(text, 1, 2, 5);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: 1,
title: 'Implement database schema',
description: 'Design and implement the database schema for user data',
status: 'pending',
dependencies: [],
details: 'Create tables for users, preferences, and settings',
parentTaskId: 5
});
expect(result[1]).toEqual({
id: 2,
title: 'Create API endpoints',
description: 'Develop RESTful API endpoints for user operations',
status: 'pending',
dependencies: [],
details: 'Implement CRUD operations for user management',
parentTaskId: 5
});
});
test('should handle subtasks with dependencies', () => {
const text = `
[
{
"id": 1,
"title": "Setup React environment",
"description": "Initialize React app with necessary dependencies",
"dependencies": [],
"details": "Use Create React App or Vite to set up a new project"
},
{
"id": 2,
"title": "Create component structure",
"description": "Design and implement component hierarchy",
"dependencies": [1],
"details": "Organize components by feature and reusability"
}
]`;
const result = parseSubtasksFromText(text, 1, 2, 5);
expect(result).toHaveLength(2);
expect(result[0].dependencies).toEqual([]);
expect(result[1].dependencies).toEqual([1]);
});
test('should handle complex dependency lists', () => {
const text = `
[
{
"id": 1,
"title": "Setup database",
"description": "Initialize database structure",
"dependencies": [],
"details": "Set up PostgreSQL database"
},
{
"id": 2,
"title": "Create models",
"description": "Implement data models",
"dependencies": [1],
"details": "Define Prisma models"
},
{
"id": 3,
"title": "Implement controllers",
"description": "Create API controllers",
"dependencies": [1, 2],
"details": "Build controllers for all endpoints"
}
]`;
const result = parseSubtasksFromText(text, 1, 3, 5);
expect(result).toHaveLength(3);
expect(result[2].dependencies).toEqual([1, 2]);
});
test('should create fallback subtasks for empty text', () => {
const emptyText = '';
const result = parseSubtasksFromText(emptyText, 1, 2, 5);
// Verify fallback subtasks structure
expect(result).toHaveLength(2);
expect(result[0]).toMatchObject({
id: 1,
title: 'Subtask 1',
description: 'Auto-generated fallback subtask',
status: 'pending',
dependencies: [],
parentTaskId: 5
});
expect(result[1]).toMatchObject({
id: 2,
title: 'Subtask 2',
description: 'Auto-generated fallback subtask',
status: 'pending',
dependencies: [],
parentTaskId: 5
});
});
test('should normalize subtask IDs', () => {
const text = `
[
{
"id": 10,
"title": "First task with incorrect ID",
"description": "First description",
"dependencies": [],
"details": "First details"
},
{
"id": 20,
"title": "Second task with incorrect ID",
"description": "Second description",
"dependencies": [],
"details": "Second details"
}
]`;
const result = parseSubtasksFromText(text, 1, 2, 5);
expect(result).toHaveLength(2);
expect(result[0].id).toBe(1); // Should normalize to starting ID
expect(result[1].id).toBe(2); // Should normalize to starting ID + 1
});
test('should convert string dependencies to numbers', () => {
const text = `
[
{
"id": 1,
"title": "First task",
"description": "First description",
"dependencies": [],
"details": "First details"
},
{
"id": 2,
"title": "Second task",
"description": "Second description",
"dependencies": ["1"],
"details": "Second details"
}
]`;
const result = parseSubtasksFromText(text, 1, 2, 5);
expect(result[1].dependencies).toEqual([1]);
expect(typeof result[1].dependencies[0]).toBe('number');
});
test('should create fallback subtasks for invalid JSON', () => {
const text = `This is not valid JSON and cannot be parsed`;
const result = parseSubtasksFromText(text, 1, 2, 5);
// Verify fallback subtasks structure
expect(result).toHaveLength(2);
expect(result[0]).toMatchObject({
id: 1,
title: 'Subtask 1',
description: 'Auto-generated fallback subtask',
status: 'pending',
dependencies: [],
parentTaskId: 5
});
expect(result[1]).toMatchObject({
id: 2,
title: 'Subtask 2',
description: 'Auto-generated fallback subtask',
status: 'pending',
dependencies: [],
parentTaskId: 5
});
});
});
describe('handleClaudeError function', () => {
// Import the function directly for testing
let handleClaudeError;
beforeAll(async () => {
// Dynamic import to get the actual function
const module = await import('../../scripts/modules/ai-services.js');
handleClaudeError = module.handleClaudeError;
});
test('should handle overloaded_error type', () => {
const error = {
type: 'error',
error: {
type: 'overloaded_error',
message: 'Claude is experiencing high volume'
}
};
// Mock process.env to include PERPLEXITY_API_KEY
const originalEnv = process.env;
process.env = { ...originalEnv, PERPLEXITY_API_KEY: 'test-key' };
const result = handleClaudeError(error);
// Restore original env
process.env = originalEnv;
expect(result).toContain('Claude is currently overloaded');
expect(result).toContain('fall back to Perplexity AI');
});
test('should handle rate_limit_error type', () => {
const error = {
type: 'error',
error: {
type: 'rate_limit_error',
message: 'Rate limit exceeded'
}
};
const result = handleClaudeError(error);
expect(result).toContain('exceeded the rate limit');
});
test('should handle invalid_request_error type', () => {
const error = {
type: 'error',
error: {
type: 'invalid_request_error',
message: 'Invalid request parameters'
}
};
const result = handleClaudeError(error);
expect(result).toContain('issue with the request format');
});
test('should handle timeout errors', () => {
const error = {
message: 'Request timed out after 60000ms'
};
const result = handleClaudeError(error);
expect(result).toContain('timed out');
});
test('should handle network errors', () => {
const error = {
message: 'Network error occurred'
};
const result = handleClaudeError(error);
expect(result).toContain('network error');
});
test('should handle generic errors', () => {
const error = {
message: 'Something unexpected happened'
};
const result = handleClaudeError(error);
expect(result).toContain('Error communicating with Claude');
expect(result).toContain('Something unexpected happened');
});
});
describe('Anthropic client configuration', () => {
test('should include output-128k beta header in client configuration', async () => {
// Read the file content to verify the change is present
const fs = await import('fs');
const path = await import('path');
const filePath = path.resolve('./scripts/modules/ai-services.js');
const fileContent = fs.readFileSync(filePath, 'utf8');
// Check if the beta header is in the file
expect(fileContent).toContain(
"'anthropic-beta': 'output-128k-2025-02-19'"
);
});
});
});