chore: run npm run format

This commit is contained in:
Ralph Khreish
2025-04-09 00:25:27 +02:00
parent 3148b57f1b
commit c02483bc41
109 changed files with 28144 additions and 24157 deletions

View File

@@ -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
```
```

View File

@@ -1,14 +1,14 @@
{
"tasks": [
{
"id": 1,
"dependencies": [],
"subtasks": [
{
"id": 1,
"dependencies": []
}
]
}
]
}
"tasks": [
{
"id": 1,
"dependencies": [],
"subtasks": [
{
"id": 1,
"dependencies": []
}
]
}
]
}

View File

@@ -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'
}
};

View File

@@ -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

View File

@@ -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()
};
}

View File

@@ -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');
});
});
});

View File

@@ -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

View File

@@ -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
});
});
});

View File

@@ -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'
});
});
});
});

View File

@@ -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

View File

@@ -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