chore: run npm run format
This commit is contained in:
@@ -60,4 +60,4 @@ We aim for at least 80% test coverage for all code paths. Coverage reports can b
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": [],
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": [],
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"dependencies": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
86
tests/fixtures/sample-claude-response.js
vendored
86
tests/fixtures/sample-claude-response.js
vendored
@@ -3,42 +3,50 @@
|
||||
*/
|
||||
|
||||
export const sampleClaudeResponse = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup Task Data Structure",
|
||||
description: "Implement the core task data structure and file operations",
|
||||
status: "pending",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create the tasks.json file structure with support for task properties including ID, title, description, status, dependencies, priority, details, and test strategy. Implement file system operations for reading and writing task data.",
|
||||
testStrategy: "Verify tasks.json is created with the correct structure and that task data can be read from and written to the file."
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement CLI Foundation",
|
||||
description: "Create the command-line interface foundation with basic commands",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Set up Commander.js for handling CLI commands. Implement the basic command structure including help documentation. Create the foundational command parsing logic.",
|
||||
testStrategy: "Test each command to ensure it properly parses arguments and options. Verify help documentation is displayed correctly."
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Develop Task Management Operations",
|
||||
description: "Implement core operations for creating, reading, updating, and deleting tasks",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "medium",
|
||||
details: "Implement functions for listing tasks, adding new tasks, updating task status, and removing tasks. Include support for filtering tasks by status and other properties.",
|
||||
testStrategy: "Create unit tests for each CRUD operation to verify they correctly modify the task data."
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
projectName: "Task Management CLI",
|
||||
totalTasks: 3,
|
||||
sourceFile: "tests/fixtures/sample-prd.txt",
|
||||
generatedAt: "2023-12-15"
|
||||
}
|
||||
};
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Setup Task Data Structure',
|
||||
description: 'Implement the core task data structure and file operations',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
priority: 'high',
|
||||
details:
|
||||
'Create the tasks.json file structure with support for task properties including ID, title, description, status, dependencies, priority, details, and test strategy. Implement file system operations for reading and writing task data.',
|
||||
testStrategy:
|
||||
'Verify tasks.json is created with the correct structure and that task data can be read from and written to the file.'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Implement CLI Foundation',
|
||||
description:
|
||||
'Create the command-line interface foundation with basic commands',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'high',
|
||||
details:
|
||||
'Set up Commander.js for handling CLI commands. Implement the basic command structure including help documentation. Create the foundational command parsing logic.',
|
||||
testStrategy:
|
||||
'Test each command to ensure it properly parses arguments and options. Verify help documentation is displayed correctly.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Develop Task Management Operations',
|
||||
description:
|
||||
'Implement core operations for creating, reading, updating, and deleting tasks',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'medium',
|
||||
details:
|
||||
'Implement functions for listing tasks, adding new tasks, updating task status, and removing tasks. Include support for filtering tasks by status and other properties.',
|
||||
testStrategy:
|
||||
'Create unit tests for each CRUD operation to verify they correctly modify the task data.'
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
projectName: 'Task Management CLI',
|
||||
totalTasks: 3,
|
||||
sourceFile: 'tests/fixtures/sample-prd.txt',
|
||||
generatedAt: '2023-12-15'
|
||||
}
|
||||
};
|
||||
|
||||
162
tests/fixtures/sample-tasks.js
vendored
162
tests/fixtures/sample-tasks.js
vendored
@@ -3,86 +3,88 @@
|
||||
*/
|
||||
|
||||
export const sampleTasks = {
|
||||
meta: {
|
||||
projectName: "Test Project",
|
||||
projectVersion: "1.0.0",
|
||||
createdAt: "2023-01-01T00:00:00.000Z",
|
||||
updatedAt: "2023-01-01T00:00:00.000Z"
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Initialize Project",
|
||||
description: "Set up the project structure and dependencies",
|
||||
status: "done",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create directory structure, initialize package.json, and install dependencies",
|
||||
testStrategy: "Verify all directories and files are created correctly"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Create Core Functionality",
|
||||
description: "Implement the main features of the application",
|
||||
status: "in-progress",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Implement user authentication, data processing, and API endpoints",
|
||||
testStrategy: "Write unit tests for all core functions",
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Implement Authentication",
|
||||
description: "Create user authentication system",
|
||||
status: "done",
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Set Up Database",
|
||||
description: "Configure database connection and models",
|
||||
status: "pending",
|
||||
dependencies: [1]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Implement UI Components",
|
||||
description: "Create the user interface components",
|
||||
status: "pending",
|
||||
dependencies: [2],
|
||||
priority: "medium",
|
||||
details: "Design and implement React components for the user interface",
|
||||
testStrategy: "Test components with React Testing Library",
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Create Header Component",
|
||||
description: "Implement the header component",
|
||||
status: "pending",
|
||||
dependencies: [],
|
||||
details: "Create a responsive header with navigation links"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Create Footer Component",
|
||||
description: "Implement the footer component",
|
||||
status: "pending",
|
||||
dependencies: [],
|
||||
details: "Create a footer with copyright information and links"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
meta: {
|
||||
projectName: 'Test Project',
|
||||
projectVersion: '1.0.0',
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
updatedAt: '2023-01-01T00:00:00.000Z'
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Initialize Project',
|
||||
description: 'Set up the project structure and dependencies',
|
||||
status: 'done',
|
||||
dependencies: [],
|
||||
priority: 'high',
|
||||
details:
|
||||
'Create directory structure, initialize package.json, and install dependencies',
|
||||
testStrategy: 'Verify all directories and files are created correctly'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Create Core Functionality',
|
||||
description: 'Implement the main features of the application',
|
||||
status: 'in-progress',
|
||||
dependencies: [1],
|
||||
priority: 'high',
|
||||
details:
|
||||
'Implement user authentication, data processing, and API endpoints',
|
||||
testStrategy: 'Write unit tests for all core functions',
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Implement Authentication',
|
||||
description: 'Create user authentication system',
|
||||
status: 'done',
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Set Up Database',
|
||||
description: 'Configure database connection and models',
|
||||
status: 'pending',
|
||||
dependencies: [1]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Implement UI Components',
|
||||
description: 'Create the user interface components',
|
||||
status: 'pending',
|
||||
dependencies: [2],
|
||||
priority: 'medium',
|
||||
details: 'Design and implement React components for the user interface',
|
||||
testStrategy: 'Test components with React Testing Library',
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Create Header Component',
|
||||
description: 'Implement the header component',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
details: 'Create a responsive header with navigation links'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Create Footer Component',
|
||||
description: 'Implement the footer component',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
details: 'Create a footer with copyright information and links'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const emptySampleTasks = {
|
||||
meta: {
|
||||
projectName: "Empty Project",
|
||||
projectVersion: "1.0.0",
|
||||
createdAt: "2023-01-01T00:00:00.000Z",
|
||||
updatedAt: "2023-01-01T00:00:00.000Z"
|
||||
},
|
||||
tasks: []
|
||||
};
|
||||
meta: {
|
||||
projectName: 'Empty Project',
|
||||
projectVersion: '1.0.0',
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
updatedAt: '2023-01-01T00:00:00.000Z'
|
||||
},
|
||||
tasks: []
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Jest setup file
|
||||
*
|
||||
*
|
||||
* This file is run before each test suite to set up the test environment.
|
||||
*/
|
||||
|
||||
@@ -16,15 +16,15 @@ process.env.PROJECT_NAME = 'Test Project';
|
||||
process.env.PROJECT_VERSION = '1.0.0';
|
||||
|
||||
// Add global test helpers if needed
|
||||
global.wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
global.wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// If needed, silence console during tests
|
||||
if (process.env.SILENCE_CONSOLE === 'true') {
|
||||
global.console = {
|
||||
...console,
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
}
|
||||
global.console = {
|
||||
...console,
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,331 +4,347 @@
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP,
|
||||
getModelConfig,
|
||||
getBestAvailableAIModel,
|
||||
handleClaudeError
|
||||
import {
|
||||
getAnthropicClientForMCP,
|
||||
getPerplexityClientForMCP,
|
||||
getModelConfig,
|
||||
getBestAvailableAIModel,
|
||||
handleClaudeError
|
||||
} from '../../mcp-server/src/core/utils/ai-client-utils.js';
|
||||
|
||||
// Mock the Anthropic constructor
|
||||
jest.mock('@anthropic-ai/sdk', () => {
|
||||
return {
|
||||
Anthropic: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
messages: {
|
||||
create: jest.fn().mockResolvedValue({})
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
return {
|
||||
Anthropic: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
messages: {
|
||||
create: jest.fn().mockResolvedValue({})
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the OpenAI dynamic import
|
||||
jest.mock('openai', () => {
|
||||
return {
|
||||
default: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
chat: {
|
||||
completions: {
|
||||
create: jest.fn().mockResolvedValue({})
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
return {
|
||||
default: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
chat: {
|
||||
completions: {
|
||||
create: jest.fn().mockResolvedValue({})
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('AI Client Utilities', () => {
|
||||
const originalEnv = process.env;
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset process.env before each test
|
||||
process.env = { ...originalEnv };
|
||||
|
||||
// Clear all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
// Reset process.env before each test
|
||||
process.env = { ...originalEnv };
|
||||
|
||||
afterAll(() => {
|
||||
// Restore process.env
|
||||
process.env = originalEnv;
|
||||
});
|
||||
// Clear all mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getAnthropicClientForMCP', () => {
|
||||
it('should initialize client with API key from session', () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: 'test-key-from-session'
|
||||
}
|
||||
};
|
||||
const mockLog = { error: jest.fn() };
|
||||
afterAll(() => {
|
||||
// Restore process.env
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
// Execute
|
||||
const client = getAnthropicClientForMCP(session, mockLog);
|
||||
describe('getAnthropicClientForMCP', () => {
|
||||
it('should initialize client with API key from session', () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: 'test-key-from-session'
|
||||
}
|
||||
};
|
||||
const mockLog = { error: jest.fn() };
|
||||
|
||||
// Verify
|
||||
expect(client).toBeDefined();
|
||||
expect(client.messages.create).toBeDefined();
|
||||
expect(mockLog.error).not.toHaveBeenCalled();
|
||||
});
|
||||
// Execute
|
||||
const client = getAnthropicClientForMCP(session, mockLog);
|
||||
|
||||
it('should fall back to process.env when session key is missing', () => {
|
||||
// Setup
|
||||
process.env.ANTHROPIC_API_KEY = 'test-key-from-env';
|
||||
const session = { env: {} };
|
||||
const mockLog = { error: jest.fn() };
|
||||
// Verify
|
||||
expect(client).toBeDefined();
|
||||
expect(client.messages.create).toBeDefined();
|
||||
expect(mockLog.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Execute
|
||||
const client = getAnthropicClientForMCP(session, mockLog);
|
||||
it('should fall back to process.env when session key is missing', () => {
|
||||
// Setup
|
||||
process.env.ANTHROPIC_API_KEY = 'test-key-from-env';
|
||||
const session = { env: {} };
|
||||
const mockLog = { error: jest.fn() };
|
||||
|
||||
// Verify
|
||||
expect(client).toBeDefined();
|
||||
expect(mockLog.error).not.toHaveBeenCalled();
|
||||
});
|
||||
// Execute
|
||||
const client = getAnthropicClientForMCP(session, mockLog);
|
||||
|
||||
it('should throw error when API key is missing', () => {
|
||||
// Setup
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
const session = { env: {} };
|
||||
const mockLog = { error: jest.fn() };
|
||||
// Verify
|
||||
expect(client).toBeDefined();
|
||||
expect(mockLog.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Execute & Verify
|
||||
expect(() => getAnthropicClientForMCP(session, mockLog)).toThrow();
|
||||
expect(mockLog.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('should throw error when API key is missing', () => {
|
||||
// Setup
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
const session = { env: {} };
|
||||
const mockLog = { error: jest.fn() };
|
||||
|
||||
describe('getPerplexityClientForMCP', () => {
|
||||
it('should initialize client with API key from session', async () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
PERPLEXITY_API_KEY: 'test-perplexity-key'
|
||||
}
|
||||
};
|
||||
const mockLog = { error: jest.fn() };
|
||||
// Execute & Verify
|
||||
expect(() => getAnthropicClientForMCP(session, mockLog)).toThrow();
|
||||
expect(mockLog.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// Execute
|
||||
const client = await getPerplexityClientForMCP(session, mockLog);
|
||||
describe('getPerplexityClientForMCP', () => {
|
||||
it('should initialize client with API key from session', async () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
PERPLEXITY_API_KEY: 'test-perplexity-key'
|
||||
}
|
||||
};
|
||||
const mockLog = { error: jest.fn() };
|
||||
|
||||
// Verify
|
||||
expect(client).toBeDefined();
|
||||
expect(client.chat.completions.create).toBeDefined();
|
||||
expect(mockLog.error).not.toHaveBeenCalled();
|
||||
});
|
||||
// Execute
|
||||
const client = await getPerplexityClientForMCP(session, mockLog);
|
||||
|
||||
it('should throw error when API key is missing', async () => {
|
||||
// Setup
|
||||
delete process.env.PERPLEXITY_API_KEY;
|
||||
const session = { env: {} };
|
||||
const mockLog = { error: jest.fn() };
|
||||
// Verify
|
||||
expect(client).toBeDefined();
|
||||
expect(client.chat.completions.create).toBeDefined();
|
||||
expect(mockLog.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Execute & Verify
|
||||
await expect(getPerplexityClientForMCP(session, mockLog)).rejects.toThrow();
|
||||
expect(mockLog.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('should throw error when API key is missing', async () => {
|
||||
// Setup
|
||||
delete process.env.PERPLEXITY_API_KEY;
|
||||
const session = { env: {} };
|
||||
const mockLog = { error: jest.fn() };
|
||||
|
||||
describe('getModelConfig', () => {
|
||||
it('should get model config from session', () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
MODEL: 'claude-3-opus',
|
||||
MAX_TOKENS: '8000',
|
||||
TEMPERATURE: '0.5'
|
||||
}
|
||||
};
|
||||
// Execute & Verify
|
||||
await expect(
|
||||
getPerplexityClientForMCP(session, mockLog)
|
||||
).rejects.toThrow();
|
||||
expect(mockLog.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// Execute
|
||||
const config = getModelConfig(session);
|
||||
describe('getModelConfig', () => {
|
||||
it('should get model config from session', () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
MODEL: 'claude-3-opus',
|
||||
MAX_TOKENS: '8000',
|
||||
TEMPERATURE: '0.5'
|
||||
}
|
||||
};
|
||||
|
||||
// Verify
|
||||
expect(config).toEqual({
|
||||
model: 'claude-3-opus',
|
||||
maxTokens: 8000,
|
||||
temperature: 0.5
|
||||
});
|
||||
});
|
||||
// Execute
|
||||
const config = getModelConfig(session);
|
||||
|
||||
it('should use default values when session values are missing', () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
// No values
|
||||
}
|
||||
};
|
||||
// Verify
|
||||
expect(config).toEqual({
|
||||
model: 'claude-3-opus',
|
||||
maxTokens: 8000,
|
||||
temperature: 0.5
|
||||
});
|
||||
});
|
||||
|
||||
// Execute
|
||||
const config = getModelConfig(session);
|
||||
it('should use default values when session values are missing', () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
// No values
|
||||
}
|
||||
};
|
||||
|
||||
// Verify
|
||||
expect(config).toEqual({
|
||||
model: 'claude-3-7-sonnet-20250219',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
});
|
||||
});
|
||||
// Execute
|
||||
const config = getModelConfig(session);
|
||||
|
||||
it('should allow custom defaults', () => {
|
||||
// Setup
|
||||
const session = { env: {} };
|
||||
const customDefaults = {
|
||||
model: 'custom-model',
|
||||
maxTokens: 2000,
|
||||
temperature: 0.3
|
||||
};
|
||||
// Verify
|
||||
expect(config).toEqual({
|
||||
model: 'claude-3-7-sonnet-20250219',
|
||||
maxTokens: 64000,
|
||||
temperature: 0.2
|
||||
});
|
||||
});
|
||||
|
||||
// Execute
|
||||
const config = getModelConfig(session, customDefaults);
|
||||
it('should allow custom defaults', () => {
|
||||
// Setup
|
||||
const session = { env: {} };
|
||||
const customDefaults = {
|
||||
model: 'custom-model',
|
||||
maxTokens: 2000,
|
||||
temperature: 0.3
|
||||
};
|
||||
|
||||
// Verify
|
||||
expect(config).toEqual(customDefaults);
|
||||
});
|
||||
});
|
||||
// Execute
|
||||
const config = getModelConfig(session, customDefaults);
|
||||
|
||||
describe('getBestAvailableAIModel', () => {
|
||||
it('should return Perplexity for research when available', async () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
PERPLEXITY_API_KEY: 'test-perplexity-key',
|
||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
||||
}
|
||||
};
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
// Verify
|
||||
expect(config).toEqual(customDefaults);
|
||||
});
|
||||
});
|
||||
|
||||
// Execute
|
||||
const result = await getBestAvailableAIModel(session, { requiresResearch: true }, mockLog);
|
||||
describe('getBestAvailableAIModel', () => {
|
||||
it('should return Perplexity for research when available', async () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
PERPLEXITY_API_KEY: 'test-perplexity-key',
|
||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
||||
}
|
||||
};
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
|
||||
// Verify
|
||||
expect(result.type).toBe('perplexity');
|
||||
expect(result.client).toBeDefined();
|
||||
});
|
||||
// Execute
|
||||
const result = await getBestAvailableAIModel(
|
||||
session,
|
||||
{ requiresResearch: true },
|
||||
mockLog
|
||||
);
|
||||
|
||||
it('should return Claude when Perplexity is not available and Claude is not overloaded', async () => {
|
||||
// Setup
|
||||
const originalPerplexityKey = process.env.PERPLEXITY_API_KEY;
|
||||
delete process.env.PERPLEXITY_API_KEY; // Make sure Perplexity is not available in process.env
|
||||
// Verify
|
||||
expect(result.type).toBe('perplexity');
|
||||
expect(result.client).toBeDefined();
|
||||
});
|
||||
|
||||
const session = {
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
||||
// Purposely not including PERPLEXITY_API_KEY
|
||||
}
|
||||
};
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
|
||||
try {
|
||||
// Execute
|
||||
const result = await getBestAvailableAIModel(session, { requiresResearch: true }, mockLog);
|
||||
it('should return Claude when Perplexity is not available and Claude is not overloaded', async () => {
|
||||
// Setup
|
||||
const originalPerplexityKey = process.env.PERPLEXITY_API_KEY;
|
||||
delete process.env.PERPLEXITY_API_KEY; // Make sure Perplexity is not available in process.env
|
||||
|
||||
// Verify
|
||||
// In our implementation, we prioritize research capability through Perplexity
|
||||
// so if we're testing research but Perplexity isn't available, Claude is used
|
||||
expect(result.type).toBe('claude');
|
||||
expect(result.client).toBeDefined();
|
||||
expect(mockLog.warn).toHaveBeenCalled(); // Warning about using Claude instead of Perplexity
|
||||
} finally {
|
||||
// Restore original env variables
|
||||
if (originalPerplexityKey) {
|
||||
process.env.PERPLEXITY_API_KEY = originalPerplexityKey;
|
||||
}
|
||||
}
|
||||
});
|
||||
const session = {
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
||||
// Purposely not including PERPLEXITY_API_KEY
|
||||
}
|
||||
};
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
|
||||
it('should fall back to Claude as last resort when overloaded', async () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
||||
}
|
||||
};
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
try {
|
||||
// Execute
|
||||
const result = await getBestAvailableAIModel(
|
||||
session,
|
||||
{ requiresResearch: true },
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Execute
|
||||
const result = await getBestAvailableAIModel(session, { claudeOverloaded: true }, mockLog);
|
||||
// Verify
|
||||
// In our implementation, we prioritize research capability through Perplexity
|
||||
// so if we're testing research but Perplexity isn't available, Claude is used
|
||||
expect(result.type).toBe('claude');
|
||||
expect(result.client).toBeDefined();
|
||||
expect(mockLog.warn).toHaveBeenCalled(); // Warning about using Claude instead of Perplexity
|
||||
} finally {
|
||||
// Restore original env variables
|
||||
if (originalPerplexityKey) {
|
||||
process.env.PERPLEXITY_API_KEY = originalPerplexityKey;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Verify
|
||||
expect(result.type).toBe('claude');
|
||||
expect(result.client).toBeDefined();
|
||||
expect(mockLog.warn).toHaveBeenCalled(); // Warning about Claude overloaded
|
||||
});
|
||||
it('should fall back to Claude as last resort when overloaded', async () => {
|
||||
// Setup
|
||||
const session = {
|
||||
env: {
|
||||
ANTHROPIC_API_KEY: 'test-anthropic-key'
|
||||
}
|
||||
};
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
|
||||
it('should throw error when no models are available', async () => {
|
||||
// Setup
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
delete process.env.PERPLEXITY_API_KEY;
|
||||
const session = { env: {} };
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
// Execute
|
||||
const result = await getBestAvailableAIModel(
|
||||
session,
|
||||
{ claudeOverloaded: true },
|
||||
mockLog
|
||||
);
|
||||
|
||||
// Execute & Verify
|
||||
await expect(getBestAvailableAIModel(session, {}, mockLog)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
// Verify
|
||||
expect(result.type).toBe('claude');
|
||||
expect(result.client).toBeDefined();
|
||||
expect(mockLog.warn).toHaveBeenCalled(); // Warning about Claude overloaded
|
||||
});
|
||||
|
||||
describe('handleClaudeError', () => {
|
||||
it('should handle overloaded error', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'overloaded_error',
|
||||
message: 'Claude is overloaded'
|
||||
}
|
||||
};
|
||||
it('should throw error when no models are available', async () => {
|
||||
// Setup
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
delete process.env.PERPLEXITY_API_KEY;
|
||||
const session = { env: {} };
|
||||
const mockLog = { warn: jest.fn(), info: jest.fn(), error: jest.fn() };
|
||||
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
// Execute & Verify
|
||||
await expect(
|
||||
getBestAvailableAIModel(session, {}, mockLog)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// Verify
|
||||
expect(message).toContain('overloaded');
|
||||
});
|
||||
describe('handleClaudeError', () => {
|
||||
it('should handle overloaded error', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'overloaded_error',
|
||||
message: 'Claude is overloaded'
|
||||
}
|
||||
};
|
||||
|
||||
it('should handle rate limit error', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'rate_limit_error',
|
||||
message: 'Rate limit exceeded'
|
||||
}
|
||||
};
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
// Verify
|
||||
expect(message).toContain('overloaded');
|
||||
});
|
||||
|
||||
// Verify
|
||||
expect(message).toContain('rate limit');
|
||||
});
|
||||
it('should handle rate limit error', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'rate_limit_error',
|
||||
message: 'Rate limit exceeded'
|
||||
}
|
||||
};
|
||||
|
||||
it('should handle timeout error', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
message: 'Request timed out after 60 seconds'
|
||||
};
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
// Verify
|
||||
expect(message).toContain('rate limit');
|
||||
});
|
||||
|
||||
// Verify
|
||||
expect(message).toContain('timed out');
|
||||
});
|
||||
it('should handle timeout error', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
message: 'Request timed out after 60 seconds'
|
||||
};
|
||||
|
||||
it('should handle generic errors', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
message: 'Something went wrong'
|
||||
};
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
// Verify
|
||||
expect(message).toContain('timed out');
|
||||
});
|
||||
|
||||
// Verify
|
||||
expect(message).toContain('Error communicating with Claude');
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should handle generic errors', () => {
|
||||
// Setup
|
||||
const error = {
|
||||
message: 'Something went wrong'
|
||||
};
|
||||
|
||||
// Execute
|
||||
const message = handleClaudeError(error);
|
||||
|
||||
// Verify
|
||||
expect(message).toContain('Error communicating with Claude');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,62 +10,68 @@ 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
|
||||
};
|
||||
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' } }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
chat: {
|
||||
completions: {
|
||||
create: jest.fn().mockResolvedValue({
|
||||
choices: [{ message: { content: 'Perplexity response' } }]
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
const mockOpenAI = jest.fn().mockImplementation(() => mockOpenAIInstance);
|
||||
|
||||
jest.mock('openai', () => {
|
||||
return { default: mockOpenAI };
|
||||
return { default: mockOpenAI };
|
||||
});
|
||||
|
||||
jest.mock('dotenv', () => ({
|
||||
config: jest.fn(),
|
||||
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),
|
||||
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(),
|
||||
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"}]' }],
|
||||
}),
|
||||
},
|
||||
messages: {
|
||||
create: jest.fn().mockResolvedValue({
|
||||
content: [
|
||||
{
|
||||
text: '[{"id": 1, "title": "Test", "description": "Test", "dependencies": [], "details": "Test"}]'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
// Mock process.env
|
||||
@@ -75,20 +81,20 @@ const originalEnv = process.env;
|
||||
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';
|
||||
});
|
||||
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;
|
||||
});
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('parseSubtasksFromText function', () => {
|
||||
test('should parse subtasks from JSON text', () => {
|
||||
const text = `Here's your list of subtasks:
|
||||
describe('parseSubtasksFromText function', () => {
|
||||
test('should parse subtasks from JSON text', () => {
|
||||
const text = `Here's your list of subtasks:
|
||||
|
||||
[
|
||||
{
|
||||
@@ -109,31 +115,31 @@ describe('AI Services Module', () => {
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
test('should handle subtasks with dependencies', () => {
|
||||
const text = `
|
||||
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,
|
||||
@@ -151,15 +157,15 @@ These subtasks will help you implement the parent task efficiently.`;
|
||||
}
|
||||
]`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].dependencies).toEqual([]);
|
||||
expect(result[1].dependencies).toEqual([1]);
|
||||
});
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
test('should handle complex dependency lists', () => {
|
||||
const text = `
|
||||
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,
|
||||
@@ -184,39 +190,39 @@ These subtasks will help you implement the parent task efficiently.`;
|
||||
}
|
||||
]`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 3, 5);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[2].dependencies).toEqual([1, 2]);
|
||||
});
|
||||
const result = parseSubtasksFromText(text, 1, 3, 5);
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[2].dependencies).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('should normalize subtask IDs', () => {
|
||||
const text = `
|
||||
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,
|
||||
@@ -234,15 +240,15 @@ These subtasks will help you implement the parent task efficiently.`;
|
||||
}
|
||||
]`;
|
||||
|
||||
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
|
||||
});
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
test('should convert string dependencies to numbers', () => {
|
||||
const text = `
|
||||
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,
|
||||
@@ -260,140 +266,142 @@ These subtasks will help you implement the parent task efficiently.`;
|
||||
}
|
||||
]`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
expect(result[1].dependencies).toEqual([1]);
|
||||
expect(typeof result[1].dependencies[0]).toBe('number');
|
||||
});
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
test('should create fallback subtasks for invalid JSON', () => {
|
||||
const text = `This is not valid JSON and cannot be parsed`;
|
||||
expect(result[1].dependencies).toEqual([1]);
|
||||
expect(typeof result[1].dependencies[0]).toBe('number');
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
test('should create fallback subtasks for invalid JSON', () => {
|
||||
const text = `This is not valid JSON and cannot be parsed`;
|
||||
|
||||
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;
|
||||
});
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
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');
|
||||
});
|
||||
// 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 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');
|
||||
});
|
||||
describe('handleClaudeError function', () => {
|
||||
// Import the function directly for testing
|
||||
let handleClaudeError;
|
||||
|
||||
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');
|
||||
});
|
||||
beforeAll(async () => {
|
||||
// Dynamic import to get the actual function
|
||||
const module = await import('../../scripts/modules/ai-services.js');
|
||||
handleClaudeError = module.handleClaudeError;
|
||||
});
|
||||
|
||||
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 overloaded_error type', () => {
|
||||
const error = {
|
||||
type: 'error',
|
||||
error: {
|
||||
type: 'overloaded_error',
|
||||
message: 'Claude is experiencing high volume'
|
||||
}
|
||||
};
|
||||
|
||||
test('should handle network errors', () => {
|
||||
const error = {
|
||||
message: 'Network error occurred'
|
||||
};
|
||||
|
||||
const result = handleClaudeError(error);
|
||||
|
||||
expect(result).toContain('network error');
|
||||
});
|
||||
// Mock process.env to include PERPLEXITY_API_KEY
|
||||
const originalEnv = process.env;
|
||||
process.env = { ...originalEnv, PERPLEXITY_API_KEY: 'test-key' };
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
const result = handleClaudeError(error);
|
||||
|
||||
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'");
|
||||
});
|
||||
});
|
||||
});
|
||||
// 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'"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,393 +5,396 @@ import os from 'os';
|
||||
|
||||
// Mock external modules
|
||||
jest.mock('child_process', () => ({
|
||||
execSync: jest.fn()
|
||||
execSync: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock('readline', () => ({
|
||||
createInterface: jest.fn(() => ({
|
||||
question: jest.fn(),
|
||||
close: jest.fn()
|
||||
}))
|
||||
createInterface: jest.fn(() => ({
|
||||
question: jest.fn(),
|
||||
close: jest.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock figlet for banner display
|
||||
jest.mock('figlet', () => ({
|
||||
default: {
|
||||
textSync: jest.fn(() => 'Task Master')
|
||||
}
|
||||
default: {
|
||||
textSync: jest.fn(() => 'Task Master')
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock console methods
|
||||
jest.mock('console', () => ({
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
clear: jest.fn()
|
||||
log: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
clear: jest.fn()
|
||||
}));
|
||||
|
||||
describe('Windsurf Rules File Handling', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('.windsurfrules')) {
|
||||
return 'Existing windsurf rules content';
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
||||
// Mock specific file existence checks
|
||||
if (filePath.toString().includes('package.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
|
||||
});
|
||||
let tempDir;
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Test function that simulates the behavior of .windsurfrules handling
|
||||
function mockCopyTemplateFile(templateName, targetPath) {
|
||||
if (templateName === 'windsurfrules') {
|
||||
const filename = path.basename(targetPath);
|
||||
|
||||
if (filename === '.windsurfrules') {
|
||||
if (fs.existsSync(targetPath)) {
|
||||
// Should append content when file exists
|
||||
const existingContent = fs.readFileSync(targetPath, 'utf8');
|
||||
const updatedContent = existingContent.trim() +
|
||||
'\n\n# Added by Claude Task Master - Development Workflow Rules\n\n' +
|
||||
'New content';
|
||||
fs.writeFileSync(targetPath, updatedContent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If file doesn't exist, create it normally
|
||||
fs.writeFileSync(targetPath, 'New content');
|
||||
}
|
||||
}
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
test('creates .windsurfrules when it does not exist', () => {
|
||||
// Arrange
|
||||
const targetPath = path.join(tempDir, '.windsurfrules');
|
||||
|
||||
// Act
|
||||
mockCopyTemplateFile('windsurfrules', targetPath);
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(targetPath, 'New content');
|
||||
});
|
||||
|
||||
test('appends content to existing .windsurfrules', () => {
|
||||
// Arrange
|
||||
const targetPath = path.join(tempDir, '.windsurfrules');
|
||||
const existingContent = 'Existing windsurf rules content';
|
||||
|
||||
// Override the existsSync mock just for this test
|
||||
fs.existsSync.mockReturnValueOnce(true); // Target file exists
|
||||
fs.readFileSync.mockReturnValueOnce(existingContent);
|
||||
|
||||
// Act
|
||||
mockCopyTemplateFile('windsurfrules', targetPath);
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
targetPath,
|
||||
expect.stringContaining(existingContent)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
targetPath,
|
||||
expect.stringContaining('Added by Claude Task Master')
|
||||
);
|
||||
});
|
||||
|
||||
test('includes .windsurfrules in project structure creation', () => {
|
||||
// This test verifies the expected behavior by using a mock implementation
|
||||
// that represents how createProjectStructure should work
|
||||
|
||||
// Mock implementation of createProjectStructure
|
||||
function mockCreateProjectStructure(projectName) {
|
||||
// Copy template files including .windsurfrules
|
||||
mockCopyTemplateFile('windsurfrules', path.join(tempDir, '.windsurfrules'));
|
||||
}
|
||||
|
||||
// Act - call our mock implementation
|
||||
mockCreateProjectStructure('test-project');
|
||||
|
||||
// Assert - verify that .windsurfrules was created
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.windsurfrules'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('.windsurfrules')) {
|
||||
return 'Existing windsurf rules content';
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
||||
// Mock specific file existence checks
|
||||
if (filePath.toString().includes('package.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test function that simulates the behavior of .windsurfrules handling
|
||||
function mockCopyTemplateFile(templateName, targetPath) {
|
||||
if (templateName === 'windsurfrules') {
|
||||
const filename = path.basename(targetPath);
|
||||
|
||||
if (filename === '.windsurfrules') {
|
||||
if (fs.existsSync(targetPath)) {
|
||||
// Should append content when file exists
|
||||
const existingContent = fs.readFileSync(targetPath, 'utf8');
|
||||
const updatedContent =
|
||||
existingContent.trim() +
|
||||
'\n\n# Added by Claude Task Master - Development Workflow Rules\n\n' +
|
||||
'New content';
|
||||
fs.writeFileSync(targetPath, updatedContent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If file doesn't exist, create it normally
|
||||
fs.writeFileSync(targetPath, 'New content');
|
||||
}
|
||||
}
|
||||
|
||||
test('creates .windsurfrules when it does not exist', () => {
|
||||
// Arrange
|
||||
const targetPath = path.join(tempDir, '.windsurfrules');
|
||||
|
||||
// Act
|
||||
mockCopyTemplateFile('windsurfrules', targetPath);
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(targetPath, 'New content');
|
||||
});
|
||||
|
||||
test('appends content to existing .windsurfrules', () => {
|
||||
// Arrange
|
||||
const targetPath = path.join(tempDir, '.windsurfrules');
|
||||
const existingContent = 'Existing windsurf rules content';
|
||||
|
||||
// Override the existsSync mock just for this test
|
||||
fs.existsSync.mockReturnValueOnce(true); // Target file exists
|
||||
fs.readFileSync.mockReturnValueOnce(existingContent);
|
||||
|
||||
// Act
|
||||
mockCopyTemplateFile('windsurfrules', targetPath);
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
targetPath,
|
||||
expect.stringContaining(existingContent)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
targetPath,
|
||||
expect.stringContaining('Added by Claude Task Master')
|
||||
);
|
||||
});
|
||||
|
||||
test('includes .windsurfrules in project structure creation', () => {
|
||||
// This test verifies the expected behavior by using a mock implementation
|
||||
// that represents how createProjectStructure should work
|
||||
|
||||
// Mock implementation of createProjectStructure
|
||||
function mockCreateProjectStructure(projectName) {
|
||||
// Copy template files including .windsurfrules
|
||||
mockCopyTemplateFile(
|
||||
'windsurfrules',
|
||||
path.join(tempDir, '.windsurfrules')
|
||||
);
|
||||
}
|
||||
|
||||
// Act - call our mock implementation
|
||||
mockCreateProjectStructure('test-project');
|
||||
|
||||
// Assert - verify that .windsurfrules was created
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.windsurfrules'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// New test suite for MCP Configuration Handling
|
||||
describe('MCP Configuration Handling', () => {
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return JSON.stringify({
|
||||
"mcpServers": {
|
||||
"existing-server": {
|
||||
"command": "node",
|
||||
"args": ["server.js"]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
||||
// Return true for specific paths to test different scenarios
|
||||
if (filePath.toString().includes('package.json')) {
|
||||
return true;
|
||||
}
|
||||
// Default to false for other paths
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
|
||||
});
|
||||
let tempDir;
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Test function that simulates the behavior of setupMCPConfiguration
|
||||
function mockSetupMCPConfiguration(targetDir, projectName) {
|
||||
const mcpDirPath = path.join(targetDir, '.cursor');
|
||||
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
|
||||
|
||||
// Create .cursor directory if it doesn't exist
|
||||
if (!fs.existsSync(mcpDirPath)) {
|
||||
fs.mkdirSync(mcpDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
// New MCP config to be added - references the installed package
|
||||
const newMCPServer = {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"task-master-ai",
|
||||
"mcp-server"
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Check if mcp.json already exists
|
||||
if (fs.existsSync(mcpJsonPath)) {
|
||||
try {
|
||||
// Read existing config
|
||||
const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
||||
|
||||
// Initialize mcpServers if it doesn't exist
|
||||
if (!mcpConfig.mcpServers) {
|
||||
mcpConfig.mcpServers = {};
|
||||
}
|
||||
|
||||
// Add the taskmaster-ai server if it doesn't exist
|
||||
if (!mcpConfig.mcpServers["task-master-ai"]) {
|
||||
mcpConfig.mcpServers["task-master-ai"] = newMCPServer["task-master-ai"];
|
||||
}
|
||||
|
||||
// Write the updated configuration
|
||||
fs.writeFileSync(
|
||||
mcpJsonPath,
|
||||
JSON.stringify(mcpConfig, null, 4)
|
||||
);
|
||||
} catch (error) {
|
||||
// Create new configuration on error
|
||||
const newMCPConfig = {
|
||||
"mcpServers": newMCPServer
|
||||
};
|
||||
|
||||
fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
|
||||
}
|
||||
} else {
|
||||
// If mcp.json doesn't exist, create it
|
||||
const newMCPConfig = {
|
||||
"mcpServers": newMCPServer
|
||||
};
|
||||
|
||||
fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
|
||||
}
|
||||
}
|
||||
// Create a temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
|
||||
|
||||
test('creates mcp.json when it does not exist', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('task-master-ai')
|
||||
);
|
||||
|
||||
// Should create a proper structure with mcpServers key
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('mcpServers')
|
||||
);
|
||||
|
||||
// Should reference npx command
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('npx')
|
||||
);
|
||||
});
|
||||
|
||||
test('updates existing mcp.json by adding new server', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Override the existsSync mock to simulate mcp.json exists
|
||||
fs.existsSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
// Should preserve existing server
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('existing-server')
|
||||
);
|
||||
|
||||
// Should add our new server
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('task-master-ai')
|
||||
);
|
||||
});
|
||||
|
||||
test('handles JSON parsing errors by creating new mcp.json', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Override existsSync to say mcp.json exists
|
||||
fs.existsSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// But make readFileSync return invalid JSON
|
||||
fs.readFileSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return '{invalid json';
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
// Should create a new valid JSON file with our server
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('task-master-ai')
|
||||
);
|
||||
});
|
||||
|
||||
test('does not modify existing server configuration if it already exists', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Override existsSync to say mcp.json exists
|
||||
fs.existsSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Return JSON that already has task-master-ai
|
||||
fs.readFileSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return JSON.stringify({
|
||||
"mcpServers": {
|
||||
"existing-server": {
|
||||
"command": "node",
|
||||
"args": ["server.js"]
|
||||
},
|
||||
"task-master-ai": {
|
||||
"command": "custom",
|
||||
"args": ["custom-args"]
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
// Spy to check what's written
|
||||
const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync');
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
// Verify the written data contains the original taskmaster configuration
|
||||
const dataWritten = JSON.parse(writeFileSyncSpy.mock.calls[0][1]);
|
||||
expect(dataWritten.mcpServers["task-master-ai"].command).toBe("custom");
|
||||
expect(dataWritten.mcpServers["task-master-ai"].args).toContain("custom-args");
|
||||
});
|
||||
|
||||
test('creates the .cursor directory if it doesnt exist', () => {
|
||||
// Arrange
|
||||
const cursorDirPath = path.join(tempDir, '.cursor');
|
||||
|
||||
// Make sure it looks like the directory doesn't exist
|
||||
fs.existsSync.mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(cursorDirPath, { recursive: true });
|
||||
});
|
||||
});
|
||||
// Spy on fs methods
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return JSON.stringify({
|
||||
mcpServers: {
|
||||
'existing-server': {
|
||||
command: 'node',
|
||||
args: ['server.js']
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
||||
// Return true for specific paths to test different scenarios
|
||||
if (filePath.toString().includes('package.json')) {
|
||||
return true;
|
||||
}
|
||||
// Default to false for other paths
|
||||
return false;
|
||||
});
|
||||
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up the temporary directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`Error cleaning up: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test function that simulates the behavior of setupMCPConfiguration
|
||||
function mockSetupMCPConfiguration(targetDir, projectName) {
|
||||
const mcpDirPath = path.join(targetDir, '.cursor');
|
||||
const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
|
||||
|
||||
// Create .cursor directory if it doesn't exist
|
||||
if (!fs.existsSync(mcpDirPath)) {
|
||||
fs.mkdirSync(mcpDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
// New MCP config to be added - references the installed package
|
||||
const newMCPServer = {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['task-master-ai', 'mcp-server']
|
||||
}
|
||||
};
|
||||
|
||||
// Check if mcp.json already exists
|
||||
if (fs.existsSync(mcpJsonPath)) {
|
||||
try {
|
||||
// Read existing config
|
||||
const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
||||
|
||||
// Initialize mcpServers if it doesn't exist
|
||||
if (!mcpConfig.mcpServers) {
|
||||
mcpConfig.mcpServers = {};
|
||||
}
|
||||
|
||||
// Add the taskmaster-ai server if it doesn't exist
|
||||
if (!mcpConfig.mcpServers['task-master-ai']) {
|
||||
mcpConfig.mcpServers['task-master-ai'] =
|
||||
newMCPServer['task-master-ai'];
|
||||
}
|
||||
|
||||
// Write the updated configuration
|
||||
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 4));
|
||||
} catch (error) {
|
||||
// Create new configuration on error
|
||||
const newMCPConfig = {
|
||||
mcpServers: newMCPServer
|
||||
};
|
||||
|
||||
fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
|
||||
}
|
||||
} else {
|
||||
// If mcp.json doesn't exist, create it
|
||||
const newMCPConfig = {
|
||||
mcpServers: newMCPServer
|
||||
};
|
||||
|
||||
fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
|
||||
}
|
||||
}
|
||||
|
||||
test('creates mcp.json when it does not exist', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('task-master-ai')
|
||||
);
|
||||
|
||||
// Should create a proper structure with mcpServers key
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('mcpServers')
|
||||
);
|
||||
|
||||
// Should reference npx command
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('npx')
|
||||
);
|
||||
});
|
||||
|
||||
test('updates existing mcp.json by adding new server', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Override the existsSync mock to simulate mcp.json exists
|
||||
fs.existsSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
// Should preserve existing server
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('existing-server')
|
||||
);
|
||||
|
||||
// Should add our new server
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('task-master-ai')
|
||||
);
|
||||
});
|
||||
|
||||
test('handles JSON parsing errors by creating new mcp.json', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Override existsSync to say mcp.json exists
|
||||
fs.existsSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// But make readFileSync return invalid JSON
|
||||
fs.readFileSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return '{invalid json';
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
// Should create a new valid JSON file with our server
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
mcpJsonPath,
|
||||
expect.stringContaining('task-master-ai')
|
||||
);
|
||||
});
|
||||
|
||||
test('does not modify existing server configuration if it already exists', () => {
|
||||
// Arrange
|
||||
const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
|
||||
|
||||
// Override existsSync to say mcp.json exists
|
||||
fs.existsSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Return JSON that already has task-master-ai
|
||||
fs.readFileSync.mockImplementation((filePath) => {
|
||||
if (filePath.toString().includes('mcp.json')) {
|
||||
return JSON.stringify({
|
||||
mcpServers: {
|
||||
'existing-server': {
|
||||
command: 'node',
|
||||
args: ['server.js']
|
||||
},
|
||||
'task-master-ai': {
|
||||
command: 'custom',
|
||||
args: ['custom-args']
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return '{}';
|
||||
});
|
||||
|
||||
// Spy to check what's written
|
||||
const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync');
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
// Verify the written data contains the original taskmaster configuration
|
||||
const dataWritten = JSON.parse(writeFileSyncSpy.mock.calls[0][1]);
|
||||
expect(dataWritten.mcpServers['task-master-ai'].command).toBe('custom');
|
||||
expect(dataWritten.mcpServers['task-master-ai'].args).toContain(
|
||||
'custom-args'
|
||||
);
|
||||
});
|
||||
|
||||
test('creates the .cursor directory if it doesnt exist', () => {
|
||||
// Arrange
|
||||
const cursorDirPath = path.join(tempDir, '.cursor');
|
||||
|
||||
// Make sure it looks like the directory doesn't exist
|
||||
fs.existsSync.mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
mockSetupMCPConfiguration(tempDir, 'test-project');
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(cursorDirPath, {
|
||||
recursive: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,114 +7,126 @@ import { toKebabCase } from '../../scripts/modules/utils.js';
|
||||
|
||||
// Create a test implementation of detectCamelCaseFlags
|
||||
function testDetectCamelCaseFlags(args) {
|
||||
const camelCaseFlags = [];
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--')) {
|
||||
const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after =
|
||||
|
||||
// Skip single-word flags - they can't be camelCase
|
||||
if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for camelCase pattern (lowercase followed by uppercase)
|
||||
if (/[a-z][A-Z]/.test(flagName)) {
|
||||
const kebabVersion = toKebabCase(flagName);
|
||||
if (kebabVersion !== flagName) {
|
||||
camelCaseFlags.push({
|
||||
original: flagName,
|
||||
kebabCase: kebabVersion
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return camelCaseFlags;
|
||||
const camelCaseFlags = [];
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith('--')) {
|
||||
const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after =
|
||||
|
||||
// Skip single-word flags - they can't be camelCase
|
||||
if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for camelCase pattern (lowercase followed by uppercase)
|
||||
if (/[a-z][A-Z]/.test(flagName)) {
|
||||
const kebabVersion = toKebabCase(flagName);
|
||||
if (kebabVersion !== flagName) {
|
||||
camelCaseFlags.push({
|
||||
original: flagName,
|
||||
kebabCase: kebabVersion
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return camelCaseFlags;
|
||||
}
|
||||
|
||||
describe('Kebab Case Validation', () => {
|
||||
describe('toKebabCase', () => {
|
||||
test('should convert camelCase to kebab-case', () => {
|
||||
expect(toKebabCase('promptText')).toBe('prompt-text');
|
||||
expect(toKebabCase('userID')).toBe('user-id');
|
||||
expect(toKebabCase('numTasks')).toBe('num-tasks');
|
||||
});
|
||||
|
||||
test('should handle already kebab-case strings', () => {
|
||||
expect(toKebabCase('already-kebab-case')).toBe('already-kebab-case');
|
||||
expect(toKebabCase('kebab-case')).toBe('kebab-case');
|
||||
});
|
||||
|
||||
test('should handle single words', () => {
|
||||
expect(toKebabCase('single')).toBe('single');
|
||||
expect(toKebabCase('file')).toBe('file');
|
||||
});
|
||||
});
|
||||
describe('toKebabCase', () => {
|
||||
test('should convert camelCase to kebab-case', () => {
|
||||
expect(toKebabCase('promptText')).toBe('prompt-text');
|
||||
expect(toKebabCase('userID')).toBe('user-id');
|
||||
expect(toKebabCase('numTasks')).toBe('num-tasks');
|
||||
});
|
||||
|
||||
describe('detectCamelCaseFlags', () => {
|
||||
test('should properly detect camelCase flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--promptText=test', '--userID=123'];
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(2);
|
||||
expect(flags).toContainEqual({
|
||||
original: 'promptText',
|
||||
kebabCase: 'prompt-text'
|
||||
});
|
||||
expect(flags).toContainEqual({
|
||||
original: 'userID',
|
||||
kebabCase: 'user-id'
|
||||
});
|
||||
});
|
||||
|
||||
test('should not flag kebab-case or lowercase flags', () => {
|
||||
const args = ['node', 'task-master', 'add-task', '--prompt=test', '--user-id=123'];
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should not flag any single-word flags regardless of case', () => {
|
||||
const args = [
|
||||
'node',
|
||||
'task-master',
|
||||
'add-task',
|
||||
'--prompt=test', // lowercase
|
||||
'--PROMPT=test', // uppercase
|
||||
'--Prompt=test', // mixed case
|
||||
'--file=test', // lowercase
|
||||
'--FILE=test', // uppercase
|
||||
'--File=test' // mixed case
|
||||
];
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
test('should handle already kebab-case strings', () => {
|
||||
expect(toKebabCase('already-kebab-case')).toBe('already-kebab-case');
|
||||
expect(toKebabCase('kebab-case')).toBe('kebab-case');
|
||||
});
|
||||
|
||||
test('should handle mixed case flags correctly', () => {
|
||||
const args = [
|
||||
'node',
|
||||
'task-master',
|
||||
'add-task',
|
||||
'--prompt=test', // single word, should pass
|
||||
'--promptText=test', // camelCase, should flag
|
||||
'--prompt-text=test', // kebab-case, should pass
|
||||
'--ID=123', // single word, should pass
|
||||
'--userId=123', // camelCase, should flag
|
||||
'--user-id=123' // kebab-case, should pass
|
||||
];
|
||||
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(2);
|
||||
expect(flags).toContainEqual({
|
||||
original: 'promptText',
|
||||
kebabCase: 'prompt-text'
|
||||
});
|
||||
expect(flags).toContainEqual({
|
||||
original: 'userId',
|
||||
kebabCase: 'user-id'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
test('should handle single words', () => {
|
||||
expect(toKebabCase('single')).toBe('single');
|
||||
expect(toKebabCase('file')).toBe('file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectCamelCaseFlags', () => {
|
||||
test('should properly detect camelCase flags', () => {
|
||||
const args = [
|
||||
'node',
|
||||
'task-master',
|
||||
'add-task',
|
||||
'--promptText=test',
|
||||
'--userID=123'
|
||||
];
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(2);
|
||||
expect(flags).toContainEqual({
|
||||
original: 'promptText',
|
||||
kebabCase: 'prompt-text'
|
||||
});
|
||||
expect(flags).toContainEqual({
|
||||
original: 'userID',
|
||||
kebabCase: 'user-id'
|
||||
});
|
||||
});
|
||||
|
||||
test('should not flag kebab-case or lowercase flags', () => {
|
||||
const args = [
|
||||
'node',
|
||||
'task-master',
|
||||
'add-task',
|
||||
'--prompt=test',
|
||||
'--user-id=123'
|
||||
];
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should not flag any single-word flags regardless of case', () => {
|
||||
const args = [
|
||||
'node',
|
||||
'task-master',
|
||||
'add-task',
|
||||
'--prompt=test', // lowercase
|
||||
'--PROMPT=test', // uppercase
|
||||
'--Prompt=test', // mixed case
|
||||
'--file=test', // lowercase
|
||||
'--FILE=test', // uppercase
|
||||
'--File=test' // mixed case
|
||||
];
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should handle mixed case flags correctly', () => {
|
||||
const args = [
|
||||
'node',
|
||||
'task-master',
|
||||
'add-task',
|
||||
'--prompt=test', // single word, should pass
|
||||
'--promptText=test', // camelCase, should flag
|
||||
'--prompt-text=test', // kebab-case, should pass
|
||||
'--ID=123', // single word, should pass
|
||||
'--userId=123', // camelCase, should flag
|
||||
'--user-id=123' // kebab-case, should pass
|
||||
];
|
||||
|
||||
const flags = testDetectCamelCaseFlags(args);
|
||||
|
||||
expect(flags).toHaveLength(2);
|
||||
expect(flags).toContainEqual({
|
||||
original: 'promptText',
|
||||
kebabCase: 'prompt-text'
|
||||
});
|
||||
expect(flags).toContainEqual({
|
||||
original: 'userId',
|
||||
kebabCase: 'user-id'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,45 +6,45 @@ import { findTaskById } from '../../scripts/modules/utils.js';
|
||||
import { sampleTasks, emptySampleTasks } from '../fixtures/sample-tasks.js';
|
||||
|
||||
describe('Task Finder', () => {
|
||||
describe('findTaskById function', () => {
|
||||
test('should find a task by numeric ID', () => {
|
||||
const task = findTaskById(sampleTasks.tasks, 2);
|
||||
expect(task).toBeDefined();
|
||||
expect(task.id).toBe(2);
|
||||
expect(task.title).toBe('Create Core Functionality');
|
||||
});
|
||||
describe('findTaskById function', () => {
|
||||
test('should find a task by numeric ID', () => {
|
||||
const task = findTaskById(sampleTasks.tasks, 2);
|
||||
expect(task).toBeDefined();
|
||||
expect(task.id).toBe(2);
|
||||
expect(task.title).toBe('Create Core Functionality');
|
||||
});
|
||||
|
||||
test('should find a task by string ID', () => {
|
||||
const task = findTaskById(sampleTasks.tasks, '2');
|
||||
expect(task).toBeDefined();
|
||||
expect(task.id).toBe(2);
|
||||
});
|
||||
test('should find a task by string ID', () => {
|
||||
const task = findTaskById(sampleTasks.tasks, '2');
|
||||
expect(task).toBeDefined();
|
||||
expect(task.id).toBe(2);
|
||||
});
|
||||
|
||||
test('should find a subtask using dot notation', () => {
|
||||
const subtask = findTaskById(sampleTasks.tasks, '3.1');
|
||||
expect(subtask).toBeDefined();
|
||||
expect(subtask.id).toBe(1);
|
||||
expect(subtask.title).toBe('Create Header Component');
|
||||
});
|
||||
test('should find a subtask using dot notation', () => {
|
||||
const subtask = findTaskById(sampleTasks.tasks, '3.1');
|
||||
expect(subtask).toBeDefined();
|
||||
expect(subtask.id).toBe(1);
|
||||
expect(subtask.title).toBe('Create Header Component');
|
||||
});
|
||||
|
||||
test('should return null for non-existent task ID', () => {
|
||||
const task = findTaskById(sampleTasks.tasks, 99);
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
test('should return null for non-existent task ID', () => {
|
||||
const task = findTaskById(sampleTasks.tasks, 99);
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null for non-existent subtask ID', () => {
|
||||
const subtask = findTaskById(sampleTasks.tasks, '3.99');
|
||||
expect(subtask).toBeNull();
|
||||
});
|
||||
test('should return null for non-existent subtask ID', () => {
|
||||
const subtask = findTaskById(sampleTasks.tasks, '3.99');
|
||||
expect(subtask).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null for non-existent parent task ID in subtask notation', () => {
|
||||
const subtask = findTaskById(sampleTasks.tasks, '99.1');
|
||||
expect(subtask).toBeNull();
|
||||
});
|
||||
test('should return null for non-existent parent task ID in subtask notation', () => {
|
||||
const subtask = findTaskById(sampleTasks.tasks, '99.1');
|
||||
expect(subtask).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null when tasks array is empty', () => {
|
||||
const task = findTaskById(emptySampleTasks.tasks, 1);
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('should return null when tasks array is empty', () => {
|
||||
const task = findTaskById(emptySampleTasks.tasks, 1);
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,242 +3,244 @@
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import {
|
||||
getStatusWithColor,
|
||||
formatDependenciesWithStatus,
|
||||
createProgressBar,
|
||||
getComplexityWithColor
|
||||
import {
|
||||
getStatusWithColor,
|
||||
formatDependenciesWithStatus,
|
||||
createProgressBar,
|
||||
getComplexityWithColor
|
||||
} from '../../scripts/modules/ui.js';
|
||||
import { sampleTasks } from '../fixtures/sample-tasks.js';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('chalk', () => {
|
||||
const origChalkFn = text => text;
|
||||
const chalk = origChalkFn;
|
||||
chalk.green = text => text; // Return text as-is for status functions
|
||||
chalk.yellow = text => text;
|
||||
chalk.red = text => text;
|
||||
chalk.cyan = text => text;
|
||||
chalk.blue = text => text;
|
||||
chalk.gray = text => text;
|
||||
chalk.white = text => text;
|
||||
chalk.bold = text => text;
|
||||
chalk.dim = text => text;
|
||||
|
||||
// Add hex and other methods
|
||||
chalk.hex = () => origChalkFn;
|
||||
chalk.rgb = () => origChalkFn;
|
||||
|
||||
return chalk;
|
||||
const origChalkFn = (text) => text;
|
||||
const chalk = origChalkFn;
|
||||
chalk.green = (text) => text; // Return text as-is for status functions
|
||||
chalk.yellow = (text) => text;
|
||||
chalk.red = (text) => text;
|
||||
chalk.cyan = (text) => text;
|
||||
chalk.blue = (text) => text;
|
||||
chalk.gray = (text) => text;
|
||||
chalk.white = (text) => text;
|
||||
chalk.bold = (text) => text;
|
||||
chalk.dim = (text) => text;
|
||||
|
||||
// Add hex and other methods
|
||||
chalk.hex = () => origChalkFn;
|
||||
chalk.rgb = () => origChalkFn;
|
||||
|
||||
return chalk;
|
||||
});
|
||||
|
||||
jest.mock('figlet', () => ({
|
||||
textSync: jest.fn(() => 'Task Master Banner'),
|
||||
textSync: jest.fn(() => 'Task Master Banner')
|
||||
}));
|
||||
|
||||
jest.mock('boxen', () => jest.fn(text => `[boxed: ${text}]`));
|
||||
jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
|
||||
|
||||
jest.mock('ora', () => jest.fn(() => ({
|
||||
start: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
})));
|
||||
jest.mock('ora', () =>
|
||||
jest.fn(() => ({
|
||||
start: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
stop: jest.fn()
|
||||
}))
|
||||
);
|
||||
|
||||
jest.mock('cli-table3', () => jest.fn().mockImplementation(() => ({
|
||||
push: jest.fn(),
|
||||
toString: jest.fn(() => 'Table Content'),
|
||||
})));
|
||||
jest.mock('cli-table3', () =>
|
||||
jest.fn().mockImplementation(() => ({
|
||||
push: jest.fn(),
|
||||
toString: jest.fn(() => 'Table Content')
|
||||
}))
|
||||
);
|
||||
|
||||
jest.mock('gradient-string', () => jest.fn(() => jest.fn(text => text)));
|
||||
jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
|
||||
|
||||
jest.mock('../../scripts/modules/utils.js', () => ({
|
||||
CONFIG: {
|
||||
projectName: 'Test Project',
|
||||
projectVersion: '1.0.0',
|
||||
},
|
||||
log: jest.fn(),
|
||||
findTaskById: jest.fn(),
|
||||
readJSON: jest.fn(),
|
||||
readComplexityReport: jest.fn(),
|
||||
truncate: jest.fn(text => text),
|
||||
CONFIG: {
|
||||
projectName: 'Test Project',
|
||||
projectVersion: '1.0.0'
|
||||
},
|
||||
log: jest.fn(),
|
||||
findTaskById: jest.fn(),
|
||||
readJSON: jest.fn(),
|
||||
readComplexityReport: jest.fn(),
|
||||
truncate: jest.fn((text) => text)
|
||||
}));
|
||||
|
||||
jest.mock('../../scripts/modules/task-manager.js', () => ({
|
||||
findNextTask: jest.fn(),
|
||||
analyzeTaskComplexity: jest.fn(),
|
||||
findNextTask: jest.fn(),
|
||||
analyzeTaskComplexity: jest.fn()
|
||||
}));
|
||||
|
||||
describe('UI Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getStatusWithColor function', () => {
|
||||
test('should return done status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('done');
|
||||
expect(result).toMatch(/done/);
|
||||
expect(result).toContain('✅');
|
||||
});
|
||||
describe('getStatusWithColor function', () => {
|
||||
test('should return done status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('done');
|
||||
expect(result).toMatch(/done/);
|
||||
expect(result).toContain('✅');
|
||||
});
|
||||
|
||||
test('should return pending status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('pending');
|
||||
expect(result).toMatch(/pending/);
|
||||
expect(result).toContain('⏱️');
|
||||
});
|
||||
test('should return pending status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('pending');
|
||||
expect(result).toMatch(/pending/);
|
||||
expect(result).toContain('⏱️');
|
||||
});
|
||||
|
||||
test('should return deferred status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('deferred');
|
||||
expect(result).toMatch(/deferred/);
|
||||
expect(result).toContain('⏱️');
|
||||
});
|
||||
test('should return deferred status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('deferred');
|
||||
expect(result).toMatch(/deferred/);
|
||||
expect(result).toContain('⏱️');
|
||||
});
|
||||
|
||||
test('should return in-progress status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('in-progress');
|
||||
expect(result).toMatch(/in-progress/);
|
||||
expect(result).toContain('🔄');
|
||||
});
|
||||
test('should return in-progress status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('in-progress');
|
||||
expect(result).toMatch(/in-progress/);
|
||||
expect(result).toContain('🔄');
|
||||
});
|
||||
|
||||
test('should return unknown status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('unknown');
|
||||
expect(result).toMatch(/unknown/);
|
||||
expect(result).toContain('❌');
|
||||
});
|
||||
|
||||
test('should use simple icons when forTable is true', () => {
|
||||
const doneResult = getStatusWithColor('done', true);
|
||||
expect(doneResult).toMatch(/done/);
|
||||
expect(doneResult).toContain('✓');
|
||||
|
||||
const pendingResult = getStatusWithColor('pending', true);
|
||||
expect(pendingResult).toMatch(/pending/);
|
||||
expect(pendingResult).toContain('○');
|
||||
|
||||
const inProgressResult = getStatusWithColor('in-progress', true);
|
||||
expect(inProgressResult).toMatch(/in-progress/);
|
||||
expect(inProgressResult).toContain('►');
|
||||
|
||||
const deferredResult = getStatusWithColor('deferred', true);
|
||||
expect(deferredResult).toMatch(/deferred/);
|
||||
expect(deferredResult).toContain('x');
|
||||
});
|
||||
});
|
||||
test('should return unknown status with emoji for console output', () => {
|
||||
const result = getStatusWithColor('unknown');
|
||||
expect(result).toMatch(/unknown/);
|
||||
expect(result).toContain('❌');
|
||||
});
|
||||
|
||||
describe('formatDependenciesWithStatus function', () => {
|
||||
test('should format dependencies as plain IDs when forConsole is false (default)', () => {
|
||||
const dependencies = [1, 2, 3];
|
||||
const allTasks = [
|
||||
{ id: 1, status: 'done' },
|
||||
{ id: 2, status: 'pending' },
|
||||
{ id: 3, status: 'deferred' }
|
||||
];
|
||||
test('should use simple icons when forTable is true', () => {
|
||||
const doneResult = getStatusWithColor('done', true);
|
||||
expect(doneResult).toMatch(/done/);
|
||||
expect(doneResult).toContain('✓');
|
||||
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks);
|
||||
|
||||
// With recent changes, we expect just plain IDs when forConsole is false
|
||||
expect(result).toBe('1, 2, 3');
|
||||
});
|
||||
const pendingResult = getStatusWithColor('pending', true);
|
||||
expect(pendingResult).toMatch(/pending/);
|
||||
expect(pendingResult).toContain('○');
|
||||
|
||||
test('should format dependencies with status indicators when forConsole is true', () => {
|
||||
const dependencies = [1, 2, 3];
|
||||
const allTasks = [
|
||||
{ id: 1, status: 'done' },
|
||||
{ id: 2, status: 'pending' },
|
||||
{ id: 3, status: 'deferred' }
|
||||
];
|
||||
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks, true);
|
||||
|
||||
// We can't test for exact color formatting due to our chalk mocks
|
||||
// Instead, test that the result contains all the expected IDs
|
||||
expect(result).toContain('1');
|
||||
expect(result).toContain('2');
|
||||
expect(result).toContain('3');
|
||||
|
||||
// Test that it's a comma-separated list
|
||||
expect(result.split(', ').length).toBe(3);
|
||||
});
|
||||
const inProgressResult = getStatusWithColor('in-progress', true);
|
||||
expect(inProgressResult).toMatch(/in-progress/);
|
||||
expect(inProgressResult).toContain('►');
|
||||
|
||||
test('should return "None" for empty dependencies', () => {
|
||||
const result = formatDependenciesWithStatus([], []);
|
||||
expect(result).toBe('None');
|
||||
});
|
||||
const deferredResult = getStatusWithColor('deferred', true);
|
||||
expect(deferredResult).toMatch(/deferred/);
|
||||
expect(deferredResult).toContain('x');
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle missing tasks in the task list', () => {
|
||||
const dependencies = [1, 999];
|
||||
const allTasks = [
|
||||
{ id: 1, status: 'done' }
|
||||
];
|
||||
describe('formatDependenciesWithStatus function', () => {
|
||||
test('should format dependencies as plain IDs when forConsole is false (default)', () => {
|
||||
const dependencies = [1, 2, 3];
|
||||
const allTasks = [
|
||||
{ id: 1, status: 'done' },
|
||||
{ id: 2, status: 'pending' },
|
||||
{ id: 3, status: 'deferred' }
|
||||
];
|
||||
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks);
|
||||
expect(result).toBe('1, 999 (Not found)');
|
||||
});
|
||||
});
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks);
|
||||
|
||||
describe('createProgressBar function', () => {
|
||||
test('should create a progress bar with the correct percentage', () => {
|
||||
const result = createProgressBar(50, 10, {
|
||||
'pending': 20,
|
||||
'in-progress': 15,
|
||||
'blocked': 5
|
||||
});
|
||||
expect(result).toContain('50%');
|
||||
});
|
||||
// With recent changes, we expect just plain IDs when forConsole is false
|
||||
expect(result).toBe('1, 2, 3');
|
||||
});
|
||||
|
||||
test('should handle 0% progress', () => {
|
||||
const result = createProgressBar(0, 10);
|
||||
expect(result).toContain('0%');
|
||||
});
|
||||
test('should format dependencies with status indicators when forConsole is true', () => {
|
||||
const dependencies = [1, 2, 3];
|
||||
const allTasks = [
|
||||
{ id: 1, status: 'done' },
|
||||
{ id: 2, status: 'pending' },
|
||||
{ id: 3, status: 'deferred' }
|
||||
];
|
||||
|
||||
test('should handle 100% progress', () => {
|
||||
const result = createProgressBar(100, 10);
|
||||
expect(result).toContain('100%');
|
||||
});
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks, true);
|
||||
|
||||
test('should handle invalid percentages by clamping', () => {
|
||||
const result1 = createProgressBar(0, 10);
|
||||
expect(result1).toContain('0%');
|
||||
|
||||
const result2 = createProgressBar(100, 10);
|
||||
expect(result2).toContain('100%');
|
||||
});
|
||||
// We can't test for exact color formatting due to our chalk mocks
|
||||
// Instead, test that the result contains all the expected IDs
|
||||
expect(result).toContain('1');
|
||||
expect(result).toContain('2');
|
||||
expect(result).toContain('3');
|
||||
|
||||
test('should support status breakdown in the progress bar', () => {
|
||||
const result = createProgressBar(30, 10, {
|
||||
'pending': 30,
|
||||
'in-progress': 20,
|
||||
'blocked': 10,
|
||||
'deferred': 5,
|
||||
'cancelled': 5
|
||||
});
|
||||
|
||||
expect(result).toContain('40%');
|
||||
});
|
||||
});
|
||||
// Test that it's a comma-separated list
|
||||
expect(result.split(', ').length).toBe(3);
|
||||
});
|
||||
|
||||
describe('getComplexityWithColor function', () => {
|
||||
test('should return high complexity in red', () => {
|
||||
const result = getComplexityWithColor(8);
|
||||
expect(result).toMatch(/8/);
|
||||
expect(result).toContain('🔴');
|
||||
});
|
||||
test('should return "None" for empty dependencies', () => {
|
||||
const result = formatDependenciesWithStatus([], []);
|
||||
expect(result).toBe('None');
|
||||
});
|
||||
|
||||
test('should return medium complexity in yellow', () => {
|
||||
const result = getComplexityWithColor(5);
|
||||
expect(result).toMatch(/5/);
|
||||
expect(result).toContain('🟡');
|
||||
});
|
||||
test('should handle missing tasks in the task list', () => {
|
||||
const dependencies = [1, 999];
|
||||
const allTasks = [{ id: 1, status: 'done' }];
|
||||
|
||||
test('should return low complexity in green', () => {
|
||||
const result = getComplexityWithColor(3);
|
||||
expect(result).toMatch(/3/);
|
||||
expect(result).toContain('🟢');
|
||||
});
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks);
|
||||
expect(result).toBe('1, 999 (Not found)');
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle non-numeric inputs', () => {
|
||||
const result = getComplexityWithColor('high');
|
||||
expect(result).toMatch(/high/);
|
||||
expect(result).toContain('🔴');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('createProgressBar function', () => {
|
||||
test('should create a progress bar with the correct percentage', () => {
|
||||
const result = createProgressBar(50, 10, {
|
||||
pending: 20,
|
||||
'in-progress': 15,
|
||||
blocked: 5
|
||||
});
|
||||
expect(result).toContain('50%');
|
||||
});
|
||||
|
||||
test('should handle 0% progress', () => {
|
||||
const result = createProgressBar(0, 10);
|
||||
expect(result).toContain('0%');
|
||||
});
|
||||
|
||||
test('should handle 100% progress', () => {
|
||||
const result = createProgressBar(100, 10);
|
||||
expect(result).toContain('100%');
|
||||
});
|
||||
|
||||
test('should handle invalid percentages by clamping', () => {
|
||||
const result1 = createProgressBar(0, 10);
|
||||
expect(result1).toContain('0%');
|
||||
|
||||
const result2 = createProgressBar(100, 10);
|
||||
expect(result2).toContain('100%');
|
||||
});
|
||||
|
||||
test('should support status breakdown in the progress bar', () => {
|
||||
const result = createProgressBar(30, 10, {
|
||||
pending: 30,
|
||||
'in-progress': 20,
|
||||
blocked: 10,
|
||||
deferred: 5,
|
||||
cancelled: 5
|
||||
});
|
||||
|
||||
expect(result).toContain('40%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getComplexityWithColor function', () => {
|
||||
test('should return high complexity in red', () => {
|
||||
const result = getComplexityWithColor(8);
|
||||
expect(result).toMatch(/8/);
|
||||
expect(result).toContain('🔴');
|
||||
});
|
||||
|
||||
test('should return medium complexity in yellow', () => {
|
||||
const result = getComplexityWithColor(5);
|
||||
expect(result).toMatch(/5/);
|
||||
expect(result).toContain('🟡');
|
||||
});
|
||||
|
||||
test('should return low complexity in green', () => {
|
||||
const result = getComplexityWithColor(3);
|
||||
expect(result).toMatch(/3/);
|
||||
expect(result).toContain('🟢');
|
||||
});
|
||||
|
||||
test('should handle non-numeric inputs', () => {
|
||||
const result = getComplexityWithColor('high');
|
||||
expect(result).toMatch(/high/);
|
||||
expect(result).toContain('🔴');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user