408 lines
10 KiB
JavaScript
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'"
|
|
);
|
|
});
|
|
});
|
|
});
|