chore: add prettier config and prettify
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
|
||||
```
|
||||
```
|
||||
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
||||
130
tests/fixtures/sample-tasks.js
vendored
130
tests/fixtures/sample-tasks.js
vendored
@@ -3,70 +3,72 @@
|
||||
*/
|
||||
|
||||
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"
|
||||
},
|
||||
{
|
||||
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'
|
||||
},
|
||||
{
|
||||
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: []
|
||||
};
|
||||
|
||||
@@ -17,10 +17,10 @@ import { listTasksDirect } from '../../../mcp-server/src/core/task-master-core.j
|
||||
|
||||
// Mock logger
|
||||
const mockLogger = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn()
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn()
|
||||
};
|
||||
|
||||
// Test file paths
|
||||
@@ -28,164 +28,164 @@ const testProjectRoot = path.join(__dirname, '../../fixture');
|
||||
const testTasksPath = path.join(testProjectRoot, 'test-tasks.json');
|
||||
|
||||
describe('MCP Server Direct Functions', () => {
|
||||
// Create test data before tests
|
||||
beforeAll(() => {
|
||||
// Create test directory if it doesn't exist
|
||||
if (!fs.existsSync(testProjectRoot)) {
|
||||
fs.mkdirSync(testProjectRoot, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a sample tasks.json file for testing
|
||||
const sampleTasks = {
|
||||
meta: {
|
||||
projectName: 'Test Project',
|
||||
version: '1.0.0'
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task 1',
|
||||
description: 'First task',
|
||||
status: 'done',
|
||||
dependencies: [],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Task 2',
|
||||
description: 'Second task',
|
||||
status: 'in-progress',
|
||||
dependencies: [1],
|
||||
priority: 'medium',
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Subtask 2.1',
|
||||
description: 'First subtask',
|
||||
status: 'done'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Subtask 2.2',
|
||||
description: 'Second subtask',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Task 3',
|
||||
description: 'Third task',
|
||||
status: 'pending',
|
||||
dependencies: [1, 2],
|
||||
priority: 'low'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fs.writeFileSync(testTasksPath, JSON.stringify(sampleTasks, null, 2));
|
||||
});
|
||||
|
||||
// Clean up after tests
|
||||
afterAll(() => {
|
||||
// Remove test tasks file
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
|
||||
// Try to remove the directory (will only work if empty)
|
||||
try {
|
||||
fs.rmdirSync(testProjectRoot);
|
||||
} catch (error) {
|
||||
// Ignore errors if the directory isn't empty
|
||||
}
|
||||
});
|
||||
|
||||
// Reset mocks before each test
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('listTasksDirect', () => {
|
||||
test('should return all tasks when no filter is provided', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: testTasksPath
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.tasks.length).toBe(3);
|
||||
expect(result.data.stats.total).toBe(3);
|
||||
expect(result.data.stats.completed).toBe(1);
|
||||
expect(result.data.stats.inProgress).toBe(1);
|
||||
expect(result.data.stats.pending).toBe(1);
|
||||
expect(mockLogger.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should filter tasks by status', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: testTasksPath,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.tasks.length).toBe(1);
|
||||
expect(result.data.tasks[0].id).toBe(3);
|
||||
expect(result.data.filter).toBe('pending');
|
||||
});
|
||||
|
||||
test('should include subtasks when requested', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: testTasksPath,
|
||||
withSubtasks: true
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Verify subtasks are included
|
||||
const taskWithSubtasks = result.data.tasks.find(t => t.id === 2);
|
||||
expect(taskWithSubtasks.subtasks).toBeDefined();
|
||||
expect(taskWithSubtasks.subtasks.length).toBe(2);
|
||||
|
||||
// Verify subtask details
|
||||
expect(taskWithSubtasks.subtasks[0].id).toBe(1);
|
||||
expect(taskWithSubtasks.subtasks[0].title).toBe('Subtask 2.1');
|
||||
expect(taskWithSubtasks.subtasks[0].status).toBe('done');
|
||||
});
|
||||
|
||||
test('should handle errors gracefully', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: 'non-existent-file.json'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error.code).toBeDefined();
|
||||
expect(result.error.message).toBeDefined();
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
// Create test data before tests
|
||||
beforeAll(() => {
|
||||
// Create test directory if it doesn't exist
|
||||
if (!fs.existsSync(testProjectRoot)) {
|
||||
fs.mkdirSync(testProjectRoot, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a sample tasks.json file for testing
|
||||
const sampleTasks = {
|
||||
meta: {
|
||||
projectName: 'Test Project',
|
||||
version: '1.0.0'
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Task 1',
|
||||
description: 'First task',
|
||||
status: 'done',
|
||||
dependencies: [],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Task 2',
|
||||
description: 'Second task',
|
||||
status: 'in-progress',
|
||||
dependencies: [1],
|
||||
priority: 'medium',
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Subtask 2.1',
|
||||
description: 'First subtask',
|
||||
status: 'done'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Subtask 2.2',
|
||||
description: 'Second subtask',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Task 3',
|
||||
description: 'Third task',
|
||||
status: 'pending',
|
||||
dependencies: [1, 2],
|
||||
priority: 'low'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fs.writeFileSync(testTasksPath, JSON.stringify(sampleTasks, null, 2));
|
||||
});
|
||||
|
||||
// Clean up after tests
|
||||
afterAll(() => {
|
||||
// Remove test tasks file
|
||||
if (fs.existsSync(testTasksPath)) {
|
||||
fs.unlinkSync(testTasksPath);
|
||||
}
|
||||
|
||||
// Try to remove the directory (will only work if empty)
|
||||
try {
|
||||
fs.rmdirSync(testProjectRoot);
|
||||
} catch (error) {
|
||||
// Ignore errors if the directory isn't empty
|
||||
}
|
||||
});
|
||||
|
||||
// Reset mocks before each test
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('listTasksDirect', () => {
|
||||
test('should return all tasks when no filter is provided', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: testTasksPath
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.tasks.length).toBe(3);
|
||||
expect(result.data.stats.total).toBe(3);
|
||||
expect(result.data.stats.completed).toBe(1);
|
||||
expect(result.data.stats.inProgress).toBe(1);
|
||||
expect(result.data.stats.pending).toBe(1);
|
||||
expect(mockLogger.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should filter tasks by status', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: testTasksPath,
|
||||
status: 'pending'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data.tasks.length).toBe(1);
|
||||
expect(result.data.tasks[0].id).toBe(3);
|
||||
expect(result.data.filter).toBe('pending');
|
||||
});
|
||||
|
||||
test('should include subtasks when requested', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: testTasksPath,
|
||||
withSubtasks: true
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Verify subtasks are included
|
||||
const taskWithSubtasks = result.data.tasks.find((t) => t.id === 2);
|
||||
expect(taskWithSubtasks.subtasks).toBeDefined();
|
||||
expect(taskWithSubtasks.subtasks.length).toBe(2);
|
||||
|
||||
// Verify subtask details
|
||||
expect(taskWithSubtasks.subtasks[0].id).toBe(1);
|
||||
expect(taskWithSubtasks.subtasks[0].title).toBe('Subtask 2.1');
|
||||
expect(taskWithSubtasks.subtasks[0].status).toBe('done');
|
||||
});
|
||||
|
||||
test('should handle errors gracefully', async () => {
|
||||
// Arrange
|
||||
const args = {
|
||||
projectRoot: testProjectRoot,
|
||||
file: 'non-existent-file.json'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await listTasksDirect(args, mockLogger);
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error.code).toBeDefined();
|
||||
expect(result.error.message).toBeDefined();
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,226 +3,228 @@
|
||||
*/
|
||||
|
||||
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);
|
||||
expect(result).toBe('█████░░░░░ 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).toBe('░░░░░░░░░░ 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).toBe('██████████ 100%');
|
||||
});
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks, true);
|
||||
|
||||
test('should handle invalid percentages by clamping', () => {
|
||||
const result1 = createProgressBar(0, 10); // -10 should clamp to 0
|
||||
expect(result1).toBe('░░░░░░░░░░ 0%');
|
||||
|
||||
const result2 = createProgressBar(100, 10); // 150 should clamp to 100
|
||||
expect(result2).toBe('██████████ 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');
|
||||
|
||||
describe('getComplexityWithColor function', () => {
|
||||
test('should return high complexity in red', () => {
|
||||
const result = getComplexityWithColor(8);
|
||||
expect(result).toMatch(/8/);
|
||||
expect(result).toContain('🔴');
|
||||
});
|
||||
// Test that it's a comma-separated list
|
||||
expect(result.split(', ').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should return medium complexity in yellow', () => {
|
||||
const result = getComplexityWithColor(5);
|
||||
expect(result).toMatch(/5/);
|
||||
expect(result).toContain('🟡');
|
||||
});
|
||||
test('should return "None" for empty dependencies', () => {
|
||||
const result = formatDependenciesWithStatus([], []);
|
||||
expect(result).toBe('None');
|
||||
});
|
||||
|
||||
test('should return low complexity in green', () => {
|
||||
const result = getComplexityWithColor(3);
|
||||
expect(result).toMatch(/3/);
|
||||
expect(result).toContain('🟢');
|
||||
});
|
||||
test('should handle missing tasks in the task list', () => {
|
||||
const dependencies = [1, 999];
|
||||
const allTasks = [{ id: 1, status: 'done' }];
|
||||
|
||||
test('should handle non-numeric inputs', () => {
|
||||
const result = getComplexityWithColor('high');
|
||||
expect(result).toMatch(/high/);
|
||||
expect(result).toContain('🔴');
|
||||
});
|
||||
});
|
||||
});
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks);
|
||||
expect(result).toBe('1, 999 (Not found)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProgressBar function', () => {
|
||||
test('should create a progress bar with the correct percentage', () => {
|
||||
const result = createProgressBar(50, 10);
|
||||
expect(result).toBe('█████░░░░░ 50%');
|
||||
});
|
||||
|
||||
test('should handle 0% progress', () => {
|
||||
const result = createProgressBar(0, 10);
|
||||
expect(result).toBe('░░░░░░░░░░ 0%');
|
||||
});
|
||||
|
||||
test('should handle 100% progress', () => {
|
||||
const result = createProgressBar(100, 10);
|
||||
expect(result).toBe('██████████ 100%');
|
||||
});
|
||||
|
||||
test('should handle invalid percentages by clamping', () => {
|
||||
const result1 = createProgressBar(0, 10); // -10 should clamp to 0
|
||||
expect(result1).toBe('░░░░░░░░░░ 0%');
|
||||
|
||||
const result2 = createProgressBar(100, 10); // 150 should clamp to 100
|
||||
expect(result2).toBe('██████████ 100%');
|
||||
});
|
||||
});
|
||||
|
||||
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