feat: Enhance Task Master CLI with Testing Framework, Perplexity AI Integration, and Refactored Core Logic
This commit introduces significant enhancements and refactoring to the Task Master CLI, focusing on improved testing, integration with Perplexity AI for research-backed task updates, and core logic refactoring for better maintainability and functionality.
**Testing Infrastructure Setup:**
- Implemented Jest as the primary testing framework, setting up a comprehensive testing environment.
- Added new test scripts to including , , and for streamlined testing workflows.
- Integrated necessary devDependencies for testing, such as , , , , and , to support unit, integration, and end-to-end testing.
**Dependency Updates:**
- Updated and to reflect the latest dependency versions, ensuring project stability and access to the newest features and security patches.
- Upgraded to version 0.9.16 and usage: openai [-h] [-v] [-b API_BASE] [-k API_KEY] [-p PROXY [PROXY ...]]
[-o ORGANIZATION] [-t {openai,azure}]
[--api-version API_VERSION] [--azure-endpoint AZURE_ENDPOINT]
[--azure-ad-token AZURE_AD_TOKEN] [-V]
{api,tools,migrate,grit} ...
positional arguments:
{api,tools,migrate,grit}
api Direct API calls
tools Client side tools for convenience
options:
-h, --help show this help message and exit
-v, --verbose Set verbosity.
-b, --api-base API_BASE
What API base url to use.
-k, --api-key API_KEY
What API key to use.
-p, --proxy PROXY [PROXY ...]
What proxy to use.
-o, --organization ORGANIZATION
Which organization to run as (will use your default
organization if not specified)
-t, --api-type {openai,azure}
The backend API to call, must be `openai` or `azure`
--api-version API_VERSION
The Azure API version, e.g.
'https://learn.microsoft.com/en-us/azure/ai-
services/openai/reference#rest-api-versioning'
--azure-endpoint AZURE_ENDPOINT
The Azure endpoint, e.g.
'https://endpoint.openai.azure.com'
--azure-ad-token AZURE_AD_TOKEN
A token from Azure Active Directory,
https://www.microsoft.com/en-
us/security/business/identity-access/microsoft-entra-
id
-V, --version show program's version number and exit to 4.89.0.
- Added dependency (version 2.3.0) and updated related dependencies to their latest versions.
**Perplexity AI Integration for Research-Backed Updates:**
- Introduced an option to leverage Perplexity AI for task updates, enabling research-backed enhancements to task details.
- Implemented logic to initialize a Perplexity AI client if the environment variable is available.
- Modified the function to accept a parameter, allowing dynamic selection between Perplexity AI and Claude AI for task updates based on API key availability and user preference.
- Enhanced to handle responses from Perplexity AI and update tasks accordingly, including improved error handling and logging for robust operation.
**Core Logic Refactoring and Improvements:**
- Refactored the function to utilize task IDs instead of dependency IDs, ensuring consistency and clarity in dependency management.
- Implemented a new function to rigorously check for both circular dependencies and self-dependencies within tasks, improving task relationship integrity.
- Enhanced UI elements in :
- Refactored to incorporate icons for different task statuses and utilize a object for color mapping, improving visual representation of task status.
- Updated to display colored complexity scores with emojis, providing a more intuitive and visually appealing representation of task complexity.
- Refactored the task data structure creation and validation process:
- Updated the JSON Schema for to reflect a more streamlined and efficient task structure.
- Implemented Task Model Classes for better data modeling and type safety.
- Improved File System Operations for task data management.
- Developed robust Validation Functions and an Error Handling System to ensure data integrity and application stability.
**Testing Guidelines Implementation:**
- Implemented guidelines for writing testable code when developing new features, promoting a test-driven development approach.
- Added testing requirements and best practices for unit, integration, and edge case testing to ensure comprehensive test coverage.
- Updated the development workflow to mandate writing tests before proceeding with configuration and documentation updates, reinforcing the importance of testing throughout the development lifecycle.
This commit collectively enhances the Task Master CLI's reliability, functionality, and developer experience through improved testing practices, AI-powered research capabilities, and a more robust and maintainable codebase.
This commit is contained in:
63
tests/README.md
Normal file
63
tests/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Task Master Test Suite
|
||||
|
||||
This directory contains tests for the Task Master CLI. The tests are organized into different categories to ensure comprehensive test coverage.
|
||||
|
||||
## Test Structure
|
||||
|
||||
- `unit/`: Unit tests for individual functions and components
|
||||
- `integration/`: Integration tests for testing interactions between components
|
||||
- `e2e/`: End-to-end tests for testing complete workflows
|
||||
- `fixtures/`: Test fixtures and sample data
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run all tests:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
To run tests in watch mode (for development):
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
To run tests with coverage reporting:
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## Testing Approach
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Unit tests focus on testing individual functions and components in isolation. These tests should be fast and should mock external dependencies.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Integration tests focus on testing interactions between components. These tests ensure that components work together correctly.
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
End-to-end tests focus on testing complete workflows from a user's perspective. These tests ensure that the CLI works correctly as a whole.
|
||||
|
||||
## Test Fixtures
|
||||
|
||||
Test fixtures provide sample data for tests. Fixtures should be small, focused, and representative of real-world data.
|
||||
|
||||
## Mocking
|
||||
|
||||
For external dependencies like file system operations and API calls, we use mocking to isolate the code being tested.
|
||||
|
||||
- File system operations: Use `mock-fs` to mock the file system
|
||||
- API calls: Use Jest's mocking capabilities to mock API responses
|
||||
|
||||
## Test Coverage
|
||||
|
||||
We aim for at least 80% test coverage for all code paths. Coverage reports can be generated with:
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
72
tests/fixtures/sample-tasks.js
vendored
Normal file
72
tests/fixtures/sample-tasks.js
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Sample tasks data for tests
|
||||
*/
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
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: []
|
||||
};
|
||||
30
tests/setup.js
Normal file
30
tests/setup.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Jest setup file
|
||||
*
|
||||
* This file is run before each test suite to set up the test environment.
|
||||
*/
|
||||
|
||||
// Mock environment variables
|
||||
process.env.MODEL = 'sonar-pro';
|
||||
process.env.MAX_TOKENS = '64000';
|
||||
process.env.TEMPERATURE = '0.4';
|
||||
process.env.DEBUG = 'false';
|
||||
process.env.LOG_LEVEL = 'error'; // Set to error to reduce noise in tests
|
||||
process.env.DEFAULT_SUBTASKS = '3';
|
||||
process.env.DEFAULT_PRIORITY = 'medium';
|
||||
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));
|
||||
|
||||
// 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(),
|
||||
};
|
||||
}
|
||||
288
tests/unit/ai-services.test.js
Normal file
288
tests/unit/ai-services.test.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* AI Services module tests
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { parseSubtasksFromText } from '../../scripts/modules/ai-services.js';
|
||||
|
||||
// Create a mock log function we can check later
|
||||
const mockLog = jest.fn();
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@anthropic-ai/sdk', () => {
|
||||
return {
|
||||
Anthropic: jest.fn().mockImplementation(() => ({
|
||||
messages: {
|
||||
create: jest.fn().mockResolvedValue({
|
||||
content: [{ text: 'AI response' }],
|
||||
}),
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Use jest.fn() directly for OpenAI mock
|
||||
const mockOpenAIInstance = {
|
||||
chat: {
|
||||
completions: {
|
||||
create: jest.fn().mockResolvedValue({
|
||||
choices: [{ message: { content: 'Perplexity response' } }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockOpenAI = jest.fn().mockImplementation(() => mockOpenAIInstance);
|
||||
|
||||
jest.mock('openai', () => {
|
||||
return { default: mockOpenAI };
|
||||
});
|
||||
|
||||
jest.mock('dotenv', () => ({
|
||||
config: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../scripts/modules/utils.js', () => ({
|
||||
CONFIG: {
|
||||
model: 'claude-3-sonnet-20240229',
|
||||
temperature: 0.7,
|
||||
maxTokens: 4000,
|
||||
},
|
||||
log: mockLog,
|
||||
sanitizePrompt: jest.fn(text => text),
|
||||
}));
|
||||
|
||||
jest.mock('../../scripts/modules/ui.js', () => ({
|
||||
startLoadingIndicator: jest.fn().mockReturnValue('mockLoader'),
|
||||
stopLoadingIndicator: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock anthropic global object
|
||||
global.anthropic = {
|
||||
messages: {
|
||||
create: jest.fn().mockResolvedValue({
|
||||
content: [{ text: '[{"id": 1, "title": "Test", "description": "Test", "dependencies": [], "details": "Test"}]' }],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Mock process.env
|
||||
const originalEnv = process.env;
|
||||
|
||||
describe('AI Services Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env = { ...originalEnv };
|
||||
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
|
||||
process.env.PERPLEXITY_API_KEY = 'test-perplexity-key';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('parseSubtasksFromText function', () => {
|
||||
test('should parse subtasks from JSON text', () => {
|
||||
const text = `Here's your list of subtasks:
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Implement database schema",
|
||||
"description": "Design and implement the database schema for user data",
|
||||
"dependencies": [],
|
||||
"details": "Create tables for users, preferences, and settings"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Create API endpoints",
|
||||
"description": "Develop RESTful API endpoints for user operations",
|
||||
"dependencies": [],
|
||||
"details": "Implement CRUD operations for user management"
|
||||
}
|
||||
]
|
||||
|
||||
These subtasks will help you implement the parent task efficiently.`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
id: 1,
|
||||
title: 'Implement database schema',
|
||||
description: 'Design and implement the database schema for user data',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
details: 'Create tables for users, preferences, and settings',
|
||||
parentTaskId: 5
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
id: 2,
|
||||
title: 'Create API endpoints',
|
||||
description: 'Develop RESTful API endpoints for user operations',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
details: 'Implement CRUD operations for user management',
|
||||
parentTaskId: 5
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle subtasks with dependencies', () => {
|
||||
const text = `
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Setup React environment",
|
||||
"description": "Initialize React app with necessary dependencies",
|
||||
"dependencies": [],
|
||||
"details": "Use Create React App or Vite to set up a new project"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Create component structure",
|
||||
"description": "Design and implement component hierarchy",
|
||||
"dependencies": [1],
|
||||
"details": "Organize components by feature and reusability"
|
||||
}
|
||||
]`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].dependencies).toEqual([]);
|
||||
expect(result[1].dependencies).toEqual([1]);
|
||||
});
|
||||
|
||||
test('should handle complex dependency lists', () => {
|
||||
const text = `
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Setup database",
|
||||
"description": "Initialize database structure",
|
||||
"dependencies": [],
|
||||
"details": "Set up PostgreSQL database"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Create models",
|
||||
"description": "Implement data models",
|
||||
"dependencies": [1],
|
||||
"details": "Define Prisma models"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Implement controllers",
|
||||
"description": "Create API controllers",
|
||||
"dependencies": [1, 2],
|
||||
"details": "Build controllers for all endpoints"
|
||||
}
|
||||
]`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 3, 5);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[2].dependencies).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('should create fallback subtasks for empty text', () => {
|
||||
const emptyText = '';
|
||||
|
||||
const result = parseSubtasksFromText(emptyText, 1, 2, 5);
|
||||
|
||||
// Verify fallback subtasks structure
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 1,
|
||||
title: 'Subtask 1',
|
||||
description: 'Auto-generated fallback subtask',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
parentTaskId: 5
|
||||
});
|
||||
expect(result[1]).toMatchObject({
|
||||
id: 2,
|
||||
title: 'Subtask 2',
|
||||
description: 'Auto-generated fallback subtask',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
parentTaskId: 5
|
||||
});
|
||||
});
|
||||
|
||||
test('should normalize subtask IDs', () => {
|
||||
const text = `
|
||||
[
|
||||
{
|
||||
"id": 10,
|
||||
"title": "First task with incorrect ID",
|
||||
"description": "First description",
|
||||
"dependencies": [],
|
||||
"details": "First details"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"title": "Second task with incorrect ID",
|
||||
"description": "Second description",
|
||||
"dependencies": [],
|
||||
"details": "Second details"
|
||||
}
|
||||
]`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe(1); // Should normalize to starting ID
|
||||
expect(result[1].id).toBe(2); // Should normalize to starting ID + 1
|
||||
});
|
||||
|
||||
test('should convert string dependencies to numbers', () => {
|
||||
const text = `
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "First task",
|
||||
"description": "First description",
|
||||
"dependencies": [],
|
||||
"details": "First details"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Second task",
|
||||
"description": "Second description",
|
||||
"dependencies": ["1"],
|
||||
"details": "Second details"
|
||||
}
|
||||
]`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
expect(result[1].dependencies).toEqual([1]);
|
||||
expect(typeof result[1].dependencies[0]).toBe('number');
|
||||
});
|
||||
|
||||
test('should create fallback subtasks for invalid JSON', () => {
|
||||
const text = `This is not valid JSON and cannot be parsed`;
|
||||
|
||||
const result = parseSubtasksFromText(text, 1, 2, 5);
|
||||
|
||||
// Verify fallback subtasks structure
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 1,
|
||||
title: 'Subtask 1',
|
||||
description: 'Auto-generated fallback subtask',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
parentTaskId: 5
|
||||
});
|
||||
expect(result[1]).toMatchObject({
|
||||
id: 2,
|
||||
title: 'Subtask 2',
|
||||
description: 'Auto-generated fallback subtask',
|
||||
status: 'pending',
|
||||
dependencies: [],
|
||||
parentTaskId: 5
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
119
tests/unit/commands.test.js
Normal file
119
tests/unit/commands.test.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Commands module tests
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock modules
|
||||
jest.mock('commander');
|
||||
jest.mock('fs');
|
||||
jest.mock('path');
|
||||
jest.mock('../../scripts/modules/ui.js', () => ({
|
||||
displayBanner: jest.fn(),
|
||||
displayHelp: jest.fn()
|
||||
}));
|
||||
jest.mock('../../scripts/modules/task-manager.js');
|
||||
jest.mock('../../scripts/modules/dependency-manager.js');
|
||||
jest.mock('../../scripts/modules/utils.js', () => ({
|
||||
CONFIG: {
|
||||
projectVersion: '1.5.0'
|
||||
},
|
||||
log: jest.fn()
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
import { setupCLI } from '../../scripts/modules/commands.js';
|
||||
import { program } from 'commander';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Commands Module', () => {
|
||||
// Set up spies on the mocked modules
|
||||
const mockName = jest.spyOn(program, 'name').mockReturnValue(program);
|
||||
const mockDescription = jest.spyOn(program, 'description').mockReturnValue(program);
|
||||
const mockVersion = jest.spyOn(program, 'version').mockReturnValue(program);
|
||||
const mockHelpOption = jest.spyOn(program, 'helpOption').mockReturnValue(program);
|
||||
const mockAddHelpCommand = jest.spyOn(program, 'addHelpCommand').mockReturnValue(program);
|
||||
const mockOn = jest.spyOn(program, 'on').mockReturnValue(program);
|
||||
const mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
|
||||
const mockJoin = jest.spyOn(path, 'join');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('setupCLI function', () => {
|
||||
test('should return Commander program instance', () => {
|
||||
const result = setupCLI();
|
||||
|
||||
// Verify the program was properly configured
|
||||
expect(mockName).toHaveBeenCalledWith('dev');
|
||||
expect(mockDescription).toHaveBeenCalledWith('AI-driven development task management');
|
||||
expect(mockVersion).toHaveBeenCalled();
|
||||
expect(mockHelpOption).toHaveBeenCalledWith('-h, --help', 'Display help');
|
||||
expect(mockAddHelpCommand).toHaveBeenCalledWith(false);
|
||||
expect(mockOn).toHaveBeenCalled();
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should read version from package.json when available', () => {
|
||||
// Setup mock for package.json existence and content
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify({ version: '2.0.0' }));
|
||||
mockJoin.mockReturnValue('/mock/path/package.json');
|
||||
|
||||
// Call the setup function
|
||||
setupCLI();
|
||||
|
||||
// Get the version callback function
|
||||
const versionCallback = mockVersion.mock.calls[0][0];
|
||||
expect(typeof versionCallback).toBe('function');
|
||||
|
||||
// Execute the callback and check the result
|
||||
const result = versionCallback();
|
||||
expect(result).toBe('2.0.0');
|
||||
|
||||
// Verify the correct functions were called
|
||||
expect(mockExistsSync).toHaveBeenCalled();
|
||||
expect(mockReadFileSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use default version when package.json is not available', () => {
|
||||
// Setup mock for package.json absence
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
// Call the setup function
|
||||
setupCLI();
|
||||
|
||||
// Get the version callback function
|
||||
const versionCallback = mockVersion.mock.calls[0][0];
|
||||
expect(typeof versionCallback).toBe('function');
|
||||
|
||||
// Execute the callback and check the result
|
||||
const result = versionCallback();
|
||||
expect(result).toBe('1.5.0'); // Updated to match the actual CONFIG.projectVersion
|
||||
|
||||
expect(mockExistsSync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should use default version when package.json reading throws an error', () => {
|
||||
// Setup mock for package.json reading error
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockImplementation(() => {
|
||||
throw new Error('Read error');
|
||||
});
|
||||
|
||||
// Call the setup function
|
||||
setupCLI();
|
||||
|
||||
// Get the version callback function
|
||||
const versionCallback = mockVersion.mock.calls[0][0];
|
||||
expect(typeof versionCallback).toBe('function');
|
||||
|
||||
// Execute the callback and check the result
|
||||
const result = versionCallback();
|
||||
expect(result).toBe('1.5.0'); // Updated to match the actual CONFIG.projectVersion
|
||||
});
|
||||
});
|
||||
});
|
||||
585
tests/unit/dependency-manager.test.js
Normal file
585
tests/unit/dependency-manager.test.js
Normal file
@@ -0,0 +1,585 @@
|
||||
/**
|
||||
* Dependency Manager module tests
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import {
|
||||
validateTaskDependencies,
|
||||
isCircularDependency,
|
||||
removeDuplicateDependencies,
|
||||
cleanupSubtaskDependencies,
|
||||
ensureAtLeastOneIndependentSubtask,
|
||||
validateAndFixDependencies
|
||||
} from '../../scripts/modules/dependency-manager.js';
|
||||
import * as utils from '../../scripts/modules/utils.js';
|
||||
import { sampleTasks } from '../fixtures/sample-tasks.js';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('path');
|
||||
jest.mock('chalk', () => ({
|
||||
green: jest.fn(text => `<green>${text}</green>`),
|
||||
yellow: jest.fn(text => `<yellow>${text}</yellow>`),
|
||||
red: jest.fn(text => `<red>${text}</red>`),
|
||||
cyan: jest.fn(text => `<cyan>${text}</cyan>`),
|
||||
bold: jest.fn(text => `<bold>${text}</bold>`),
|
||||
}));
|
||||
|
||||
jest.mock('boxen', () => jest.fn(text => `[boxed: ${text}]`));
|
||||
|
||||
jest.mock('@anthropic-ai/sdk', () => ({
|
||||
Anthropic: jest.fn().mockImplementation(() => ({})),
|
||||
}));
|
||||
|
||||
// Mock utils module
|
||||
const mockTaskExists = jest.fn();
|
||||
const mockFormatTaskId = jest.fn();
|
||||
const mockFindCycles = jest.fn();
|
||||
const mockLog = jest.fn();
|
||||
const mockReadJSON = jest.fn();
|
||||
const mockWriteJSON = jest.fn();
|
||||
|
||||
jest.mock('../../scripts/modules/utils.js', () => ({
|
||||
log: mockLog,
|
||||
readJSON: mockReadJSON,
|
||||
writeJSON: mockWriteJSON,
|
||||
taskExists: mockTaskExists,
|
||||
formatTaskId: mockFormatTaskId,
|
||||
findCycles: mockFindCycles
|
||||
}));
|
||||
|
||||
jest.mock('../../scripts/modules/ui.js', () => ({
|
||||
displayBanner: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../scripts/modules/task-manager.js', () => ({
|
||||
generateTaskFiles: jest.fn(),
|
||||
}));
|
||||
|
||||
// Create a path for test files
|
||||
const TEST_TASKS_PATH = 'tests/fixture/test-tasks.json';
|
||||
|
||||
describe('Dependency Manager Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set default implementations
|
||||
mockTaskExists.mockImplementation((tasks, id) => {
|
||||
if (Array.isArray(tasks)) {
|
||||
if (typeof id === 'string' && id.includes('.')) {
|
||||
const [taskId, subtaskId] = id.split('.').map(Number);
|
||||
const task = tasks.find(t => t.id === taskId);
|
||||
return task && task.subtasks && task.subtasks.some(st => st.id === subtaskId);
|
||||
}
|
||||
return tasks.some(task => task.id === (typeof id === 'string' ? parseInt(id, 10) : id));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
mockFormatTaskId.mockImplementation(id => {
|
||||
if (typeof id === 'string' && id.includes('.')) {
|
||||
return id;
|
||||
}
|
||||
return parseInt(id, 10);
|
||||
});
|
||||
|
||||
mockFindCycles.mockImplementation((tasks) => {
|
||||
// Simplified cycle detection for testing
|
||||
const dependencyMap = new Map();
|
||||
|
||||
// Build dependency map
|
||||
tasks.forEach(task => {
|
||||
if (task.dependencies) {
|
||||
dependencyMap.set(task.id, task.dependencies);
|
||||
}
|
||||
});
|
||||
|
||||
const visited = new Set();
|
||||
const recursionStack = new Set();
|
||||
|
||||
function dfs(taskId) {
|
||||
visited.add(taskId);
|
||||
recursionStack.add(taskId);
|
||||
|
||||
const dependencies = dependencyMap.get(taskId) || [];
|
||||
for (const depId of dependencies) {
|
||||
if (!visited.has(depId)) {
|
||||
if (dfs(depId)) return true;
|
||||
} else if (recursionStack.has(depId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.delete(taskId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for cycles starting from each unvisited node
|
||||
for (const taskId of dependencyMap.keys()) {
|
||||
if (!visited.has(taskId)) {
|
||||
if (dfs(taskId)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCircularDependency function', () => {
|
||||
test('should detect a direct circular dependency', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [2] },
|
||||
{ id: 2, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = isCircularDependency(tasks, 1);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should detect an indirect circular dependency', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [2] },
|
||||
{ id: 2, dependencies: [3] },
|
||||
{ id: 3, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = isCircularDependency(tasks, 1);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false for non-circular dependencies', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [2] },
|
||||
{ id: 2, dependencies: [3] },
|
||||
{ id: 3, dependencies: [] }
|
||||
];
|
||||
|
||||
const result = isCircularDependency(tasks, 1);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle a task with no dependencies', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [] },
|
||||
{ id: 2, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = isCircularDependency(tasks, 1);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle a task depending on itself', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = isCircularDependency(tasks, 1);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateTaskDependencies function', () => {
|
||||
test('should detect missing dependencies', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [99] }, // 99 doesn't exist
|
||||
{ id: 2, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = validateTaskDependencies(tasks);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.issues.length).toBeGreaterThan(0);
|
||||
expect(result.issues[0].type).toBe('missing');
|
||||
expect(result.issues[0].taskId).toBe(1);
|
||||
expect(result.issues[0].dependencyId).toBe(99);
|
||||
});
|
||||
|
||||
test('should detect circular dependencies', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [2] },
|
||||
{ id: 2, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = validateTaskDependencies(tasks);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.issues.some(issue => issue.type === 'circular')).toBe(true);
|
||||
});
|
||||
|
||||
test('should detect self-dependencies', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = validateTaskDependencies(tasks);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.issues.some(issue =>
|
||||
issue.type === 'self' && issue.taskId === 1
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return valid for correct dependencies', () => {
|
||||
const tasks = [
|
||||
{ id: 1, dependencies: [] },
|
||||
{ id: 2, dependencies: [1] },
|
||||
{ id: 3, dependencies: [1, 2] }
|
||||
];
|
||||
|
||||
const result = validateTaskDependencies(tasks);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.issues.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should handle tasks with no dependencies property', () => {
|
||||
const tasks = [
|
||||
{ id: 1 }, // Missing dependencies property
|
||||
{ id: 2, dependencies: [1] }
|
||||
];
|
||||
|
||||
const result = validateTaskDependencies(tasks);
|
||||
|
||||
// Should be valid since a missing dependencies property is interpreted as an empty array
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeDuplicateDependencies function', () => {
|
||||
test('should remove duplicate dependencies from tasks', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{ id: 1, dependencies: [2, 2, 3, 3, 3] },
|
||||
{ id: 2, dependencies: [3] },
|
||||
{ id: 3, dependencies: [] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = removeDuplicateDependencies(tasksData);
|
||||
|
||||
expect(result.tasks[0].dependencies).toEqual([2, 3]);
|
||||
expect(result.tasks[1].dependencies).toEqual([3]);
|
||||
expect(result.tasks[2].dependencies).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle empty dependencies array', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{ id: 1, dependencies: [] },
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = removeDuplicateDependencies(tasksData);
|
||||
|
||||
expect(result.tasks[0].dependencies).toEqual([]);
|
||||
expect(result.tasks[1].dependencies).toEqual([1]);
|
||||
});
|
||||
|
||||
test('should handle tasks with no dependencies property', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{ id: 1 }, // No dependencies property
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = removeDuplicateDependencies(tasksData);
|
||||
|
||||
expect(result.tasks[0]).not.toHaveProperty('dependencies');
|
||||
expect(result.tasks[1].dependencies).toEqual([1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupSubtaskDependencies function', () => {
|
||||
test('should remove dependencies to non-existent subtasks', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
dependencies: [],
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: [] },
|
||||
{ id: 2, dependencies: [3] } // Dependency 3 doesn't exist
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
dependencies: ['1.2'], // Valid subtask dependency
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: ['1.1'] } // Valid subtask dependency
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = cleanupSubtaskDependencies(tasksData);
|
||||
|
||||
// Should remove the invalid dependency to subtask 3
|
||||
expect(result.tasks[0].subtasks[1].dependencies).toEqual([]);
|
||||
// Should keep valid dependencies
|
||||
expect(result.tasks[1].dependencies).toEqual(['1.2']);
|
||||
expect(result.tasks[1].subtasks[0].dependencies).toEqual(['1.1']);
|
||||
});
|
||||
|
||||
test('should handle tasks without subtasks', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{ id: 1, dependencies: [] },
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = cleanupSubtaskDependencies(tasksData);
|
||||
|
||||
// Should return the original data unchanged
|
||||
expect(result).toEqual(tasksData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureAtLeastOneIndependentSubtask function', () => {
|
||||
test('should clear dependencies of first subtask if none are independent', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: [2] },
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = ensureAtLeastOneIndependentSubtask(tasksData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual([]);
|
||||
expect(tasksData.tasks[0].subtasks[1].dependencies).toEqual([1]);
|
||||
});
|
||||
|
||||
test('should not modify tasks if at least one subtask is independent', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: [] },
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = ensureAtLeastOneIndependentSubtask(tasksData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual([]);
|
||||
expect(tasksData.tasks[0].subtasks[1].dependencies).toEqual([1]);
|
||||
});
|
||||
|
||||
test('should handle tasks without subtasks', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{ id: 1 },
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = ensureAtLeastOneIndependentSubtask(tasksData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(tasksData).toEqual({
|
||||
tasks: [
|
||||
{ id: 1 },
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle empty subtasks array', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{ id: 1, subtasks: [] }
|
||||
]
|
||||
};
|
||||
|
||||
const result = ensureAtLeastOneIndependentSubtask(tasksData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(tasksData).toEqual({
|
||||
tasks: [
|
||||
{ id: 1, subtasks: [] }
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAndFixDependencies function', () => {
|
||||
test('should fix multiple dependency issues and return true if changes made', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
dependencies: [1, 1, 99], // Self-dependency and duplicate and invalid dependency
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: [2, 2] }, // Duplicate dependencies
|
||||
{ id: 2, dependencies: [1] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
dependencies: [1],
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: [99] } // Invalid dependency
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Mock taskExists for validating dependencies
|
||||
mockTaskExists.mockImplementation((tasks, id) => {
|
||||
// Convert id to string for comparison
|
||||
const idStr = String(id);
|
||||
|
||||
// Handle subtask references (e.g., "1.2")
|
||||
if (idStr.includes('.')) {
|
||||
const [parentId, subtaskId] = idStr.split('.').map(Number);
|
||||
const task = tasks.find(t => t.id === parentId);
|
||||
return task && task.subtasks && task.subtasks.some(st => st.id === subtaskId);
|
||||
}
|
||||
|
||||
// Handle regular task references
|
||||
const taskId = parseInt(idStr, 10);
|
||||
return taskId === 1 || taskId === 2; // Only tasks 1 and 2 exist
|
||||
});
|
||||
|
||||
// Make a copy for verification that original is modified
|
||||
const originalData = JSON.parse(JSON.stringify(tasksData));
|
||||
|
||||
const result = validateAndFixDependencies(tasksData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
// Check that data has been modified
|
||||
expect(tasksData).not.toEqual(originalData);
|
||||
|
||||
// Check specific changes
|
||||
// 1. Self-dependency removed
|
||||
expect(tasksData.tasks[0].dependencies).not.toContain(1);
|
||||
// 2. Invalid dependency removed
|
||||
expect(tasksData.tasks[0].dependencies).not.toContain(99);
|
||||
// 3. Dependencies have been deduplicated
|
||||
if (tasksData.tasks[0].subtasks[0].dependencies.length > 0) {
|
||||
expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual(
|
||||
expect.arrayContaining([])
|
||||
);
|
||||
}
|
||||
// 4. Invalid subtask dependency removed
|
||||
expect(tasksData.tasks[1].subtasks[0].dependencies).toEqual([]);
|
||||
|
||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||
expect(mockWriteJSON).not.toHaveBeenCalledWith('tasks/tasks.json', expect.anything());
|
||||
});
|
||||
|
||||
test('should return false if no changes needed', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
dependencies: [],
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: [] }, // Already has an independent subtask
|
||||
{ id: 2, dependencies: ['1.1'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
dependencies: [1]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Mock taskExists to validate all dependencies as valid
|
||||
mockTaskExists.mockImplementation((tasks, id) => {
|
||||
// Convert id to string for comparison
|
||||
const idStr = String(id);
|
||||
|
||||
// Handle subtask references
|
||||
if (idStr.includes('.')) {
|
||||
const [parentId, subtaskId] = idStr.split('.').map(Number);
|
||||
const task = tasks.find(t => t.id === parentId);
|
||||
return task && task.subtasks && task.subtasks.some(st => st.id === subtaskId);
|
||||
}
|
||||
|
||||
// Handle regular task references
|
||||
const taskId = parseInt(idStr, 10);
|
||||
return taskId === 1 || taskId === 2;
|
||||
});
|
||||
|
||||
const originalData = JSON.parse(JSON.stringify(tasksData));
|
||||
const result = validateAndFixDependencies(tasksData);
|
||||
|
||||
expect(result).toBe(false);
|
||||
// Verify data is unchanged
|
||||
expect(tasksData).toEqual(originalData);
|
||||
|
||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||
expect(mockWriteJSON).not.toHaveBeenCalledWith('tasks/tasks.json', expect.anything());
|
||||
});
|
||||
|
||||
test('should handle invalid input', () => {
|
||||
expect(validateAndFixDependencies(null)).toBe(false);
|
||||
expect(validateAndFixDependencies({})).toBe(false);
|
||||
expect(validateAndFixDependencies({ tasks: null })).toBe(false);
|
||||
expect(validateAndFixDependencies({ tasks: 'not an array' })).toBe(false);
|
||||
|
||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||
expect(mockWriteJSON).not.toHaveBeenCalledWith('tasks/tasks.json', expect.anything());
|
||||
});
|
||||
|
||||
test('should save changes when tasksPath is provided', () => {
|
||||
const tasksData = {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
dependencies: [1, 1], // Self-dependency and duplicate
|
||||
subtasks: [
|
||||
{ id: 1, dependencies: [99] } // Invalid dependency
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Mock taskExists for this specific test
|
||||
mockTaskExists.mockImplementation((tasks, id) => {
|
||||
// Convert id to string for comparison
|
||||
const idStr = String(id);
|
||||
|
||||
// Handle subtask references
|
||||
if (idStr.includes('.')) {
|
||||
const [parentId, subtaskId] = idStr.split('.').map(Number);
|
||||
const task = tasks.find(t => t.id === parentId);
|
||||
return task && task.subtasks && task.subtasks.some(st => st.id === subtaskId);
|
||||
}
|
||||
|
||||
// Handle regular task references
|
||||
const taskId = parseInt(idStr, 10);
|
||||
return taskId === 1; // Only task 1 exists
|
||||
});
|
||||
|
||||
// Copy the original data to verify changes
|
||||
const originalData = JSON.parse(JSON.stringify(tasksData));
|
||||
|
||||
// Call the function with our test path instead of the actual tasks.json
|
||||
const result = validateAndFixDependencies(tasksData, TEST_TASKS_PATH);
|
||||
|
||||
// First verify that the result is true (changes were made)
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Verify the data was modified
|
||||
expect(tasksData).not.toEqual(originalData);
|
||||
|
||||
// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
|
||||
expect(mockWriteJSON).not.toHaveBeenCalledWith('tasks/tasks.json', expect.anything());
|
||||
});
|
||||
});
|
||||
});
|
||||
50
tests/unit/task-finder.test.js
Normal file
50
tests/unit/task-finder.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Task finder tests
|
||||
*/
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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 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 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
153
tests/unit/task-manager.test.js
Normal file
153
tests/unit/task-manager.test.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Task Manager module tests
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { findNextTask } from '../../scripts/modules/task-manager.js';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('fs');
|
||||
jest.mock('path');
|
||||
jest.mock('@anthropic-ai/sdk');
|
||||
jest.mock('cli-table3');
|
||||
jest.mock('../../scripts/modules/ui.js');
|
||||
jest.mock('../../scripts/modules/ai-services.js');
|
||||
jest.mock('../../scripts/modules/dependency-manager.js');
|
||||
jest.mock('../../scripts/modules/utils.js');
|
||||
|
||||
describe('Task Manager Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('findNextTask function', () => {
|
||||
test('should return the highest priority task with all dependencies satisfied', () => {
|
||||
const tasks = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Setup Project',
|
||||
status: 'done',
|
||||
dependencies: [],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Implement Core Features',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Create Documentation',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Deploy Application',
|
||||
status: 'pending',
|
||||
dependencies: [2, 3],
|
||||
priority: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
const nextTask = findNextTask(tasks);
|
||||
|
||||
expect(nextTask).toBeDefined();
|
||||
expect(nextTask.id).toBe(2);
|
||||
expect(nextTask.title).toBe('Implement Core Features');
|
||||
});
|
||||
|
||||
test('should prioritize by priority level when dependencies are equal', () => {
|
||||
const tasks = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Setup Project',
|
||||
status: 'done',
|
||||
dependencies: [],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Low Priority Task',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Medium Priority Task',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'High Priority Task',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
const nextTask = findNextTask(tasks);
|
||||
|
||||
expect(nextTask.id).toBe(4);
|
||||
expect(nextTask.priority).toBe('high');
|
||||
});
|
||||
|
||||
test('should return null when all tasks are completed', () => {
|
||||
const tasks = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Setup Project',
|
||||
status: 'done',
|
||||
dependencies: [],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Implement Features',
|
||||
status: 'done',
|
||||
dependencies: [1],
|
||||
priority: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
const nextTask = findNextTask(tasks);
|
||||
|
||||
expect(nextTask).toBeNull();
|
||||
});
|
||||
|
||||
test('should return null when all pending tasks have unsatisfied dependencies', () => {
|
||||
const tasks = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Setup Project',
|
||||
status: 'pending',
|
||||
dependencies: [2],
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Implement Features',
|
||||
status: 'pending',
|
||||
dependencies: [1],
|
||||
priority: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
const nextTask = findNextTask(tasks);
|
||||
|
||||
expect(nextTask).toBeNull();
|
||||
});
|
||||
|
||||
test('should handle empty tasks array', () => {
|
||||
const nextTask = findNextTask([]);
|
||||
|
||||
expect(nextTask).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
189
tests/unit/ui.test.js
Normal file
189
tests/unit/ui.test.js
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* UI module tests
|
||||
*/
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
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;
|
||||
});
|
||||
|
||||
jest.mock('figlet', () => ({
|
||||
textSync: jest.fn(() => 'Task Master Banner'),
|
||||
}));
|
||||
|
||||
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('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('../../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),
|
||||
}));
|
||||
|
||||
jest.mock('../../scripts/modules/task-manager.js', () => ({
|
||||
findNextTask: jest.fn(),
|
||||
analyzeTaskComplexity: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('UI Module', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getStatusWithColor function', () => {
|
||||
test('should return done status in green', () => {
|
||||
const result = getStatusWithColor('done');
|
||||
expect(result).toMatch(/done/);
|
||||
expect(result).toContain('✅');
|
||||
});
|
||||
|
||||
test('should return pending status in yellow', () => {
|
||||
const result = getStatusWithColor('pending');
|
||||
expect(result).toMatch(/pending/);
|
||||
expect(result).toContain('⏱️');
|
||||
});
|
||||
|
||||
test('should return deferred status in gray', () => {
|
||||
const result = getStatusWithColor('deferred');
|
||||
expect(result).toMatch(/deferred/);
|
||||
expect(result).toContain('⏱️');
|
||||
});
|
||||
|
||||
test('should return in-progress status in cyan', () => {
|
||||
const result = getStatusWithColor('in-progress');
|
||||
expect(result).toMatch(/in-progress/);
|
||||
expect(result).toContain('🔄');
|
||||
});
|
||||
|
||||
test('should return unknown status in red', () => {
|
||||
const result = getStatusWithColor('unknown');
|
||||
expect(result).toMatch(/unknown/);
|
||||
expect(result).toContain('❌');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDependenciesWithStatus function', () => {
|
||||
test('should format dependencies with status indicators', () => {
|
||||
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 (done), ⏱️ 2 (pending), ⏱️ 3 (deferred)');
|
||||
});
|
||||
|
||||
test('should return "None" for empty dependencies', () => {
|
||||
const result = formatDependenciesWithStatus([], []);
|
||||
expect(result).toBe('None');
|
||||
});
|
||||
|
||||
test('should handle missing tasks in the task list', () => {
|
||||
const dependencies = [1, 999];
|
||||
const allTasks = [
|
||||
{ id: 1, status: 'done' }
|
||||
];
|
||||
|
||||
const result = formatDependenciesWithStatus(dependencies, allTasks);
|
||||
expect(result).toBe('✅ 1 (done), 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('🔴');
|
||||
});
|
||||
});
|
||||
});
|
||||
44
tests/unit/utils.test.js
Normal file
44
tests/unit/utils.test.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Utils module tests
|
||||
*/
|
||||
|
||||
import { truncate } from '../../scripts/modules/utils.js';
|
||||
|
||||
describe('Utils Module', () => {
|
||||
describe('truncate function', () => {
|
||||
test('should return the original string if shorter than maxLength', () => {
|
||||
const result = truncate('Hello', 10);
|
||||
expect(result).toBe('Hello');
|
||||
});
|
||||
|
||||
test('should truncate the string and add ellipsis if longer than maxLength', () => {
|
||||
const result = truncate('This is a long string that needs truncation', 20);
|
||||
expect(result).toBe('This is a long st...');
|
||||
});
|
||||
|
||||
test('should handle empty string', () => {
|
||||
const result = truncate('', 10);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('should return null when input is null', () => {
|
||||
const result = truncate(null, 10);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
test('should return undefined when input is undefined', () => {
|
||||
const result = truncate(undefined, 10);
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
|
||||
test('should handle maxLength of 0 or negative', () => {
|
||||
// When maxLength is 0, slice(0, -3) returns 'He'
|
||||
const result1 = truncate('Hello', 0);
|
||||
expect(result1).toBe('He...');
|
||||
|
||||
// When maxLength is negative, slice(0, -8) returns nothing
|
||||
const result2 = truncate('Hello', -5);
|
||||
expect(result2).toBe('...');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user