mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
chore: add integration tests to new cli and mcp (#1430)
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.6",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"files": ["src", "README.md"],
|
||||
"keywords": ["temporary", "bridge", "migration"],
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^4.0.10",
|
||||
"strip-literal": "3.1.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.10"
|
||||
},
|
||||
"files": ["src", "README.md", "CHANGELOG.md"],
|
||||
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],
|
||||
|
||||
385
packages/tm-core/src/modules/tasks/entities/task.entity.spec.ts
Normal file
385
packages/tm-core/src/modules/tasks/entities/task.entity.spec.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* @fileoverview Unit tests for TaskEntity validation
|
||||
* Tests that validation errors are properly thrown with correct error codes
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TaskEntity } from './task.entity.js';
|
||||
import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js';
|
||||
import type { Task } from '../../../common/types/index.js';
|
||||
|
||||
describe('TaskEntity', () => {
|
||||
describe('validation', () => {
|
||||
it('should create a valid task entity', () => {
|
||||
const validTask: Task = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A valid test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: 'Some details',
|
||||
testStrategy: 'Unit tests',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = new TaskEntity(validTask);
|
||||
|
||||
expect(entity.id).toBe('1');
|
||||
expect(entity.title).toBe('Test Task');
|
||||
expect(entity.description).toBe('A valid test task');
|
||||
expect(entity.status).toBe('pending');
|
||||
expect(entity.priority).toBe('high');
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when id is missing', () => {
|
||||
const invalidTask = {
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as any;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task ID is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when title is missing', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: '',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task title is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when description is missing', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: '',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task description is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when title is only whitespace', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: ' ',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task title is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw VALIDATION_ERROR when description is only whitespace', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: ' ',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => new TaskEntity(invalidTask)).toThrow(TaskMasterError);
|
||||
|
||||
try {
|
||||
new TaskEntity(invalidTask);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TaskMasterError);
|
||||
expect(error.code).toBe(ERROR_CODES.VALIDATION_ERROR);
|
||||
expect(error.message).toContain('Task description is required');
|
||||
}
|
||||
});
|
||||
|
||||
it('should convert numeric id to string', () => {
|
||||
const taskWithNumericId = {
|
||||
id: 123,
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as any;
|
||||
|
||||
const entity = new TaskEntity(taskWithNumericId);
|
||||
|
||||
expect(entity.id).toBe('123');
|
||||
expect(typeof entity.id).toBe('string');
|
||||
});
|
||||
|
||||
it('should convert dependency ids to strings', () => {
|
||||
const taskWithNumericDeps = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [1, 2, '3'] as any,
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = new TaskEntity(taskWithNumericDeps);
|
||||
|
||||
expect(entity.dependencies).toEqual(['1', '2', '3']);
|
||||
entity.dependencies.forEach((dep) => {
|
||||
expect(typeof dep).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize subtask ids to strings for parent and numbers for subtask', () => {
|
||||
const taskWithSubtasks = {
|
||||
id: '1',
|
||||
title: 'Parent Task',
|
||||
description: 'A parent task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: [
|
||||
{
|
||||
id: '1' as any,
|
||||
parentId: '1',
|
||||
title: 'Subtask 1',
|
||||
description: 'First subtask',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: ''
|
||||
},
|
||||
{
|
||||
id: 2 as any,
|
||||
parentId: 1 as any,
|
||||
title: 'Subtask 2',
|
||||
description: 'Second subtask',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: ''
|
||||
}
|
||||
]
|
||||
} as Task;
|
||||
|
||||
const entity = new TaskEntity(taskWithSubtasks);
|
||||
|
||||
expect(entity.subtasks[0].id).toBe(1);
|
||||
expect(typeof entity.subtasks[0].id).toBe('number');
|
||||
expect(entity.subtasks[0].parentId).toBe('1');
|
||||
expect(typeof entity.subtasks[0].parentId).toBe('string');
|
||||
|
||||
expect(entity.subtasks[1].id).toBe(2);
|
||||
expect(typeof entity.subtasks[1].id).toBe('number');
|
||||
expect(entity.subtasks[1].parentId).toBe('1');
|
||||
expect(typeof entity.subtasks[1].parentId).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromObject', () => {
|
||||
it('should create TaskEntity from plain object', () => {
|
||||
const plainTask: Task = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = TaskEntity.fromObject(plainTask);
|
||||
|
||||
expect(entity).toBeInstanceOf(TaskEntity);
|
||||
expect(entity.id).toBe('1');
|
||||
expect(entity.title).toBe('Test Task');
|
||||
});
|
||||
|
||||
it('should throw validation error for invalid object', () => {
|
||||
const invalidTask = {
|
||||
id: '1',
|
||||
title: '',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
} as Task;
|
||||
|
||||
expect(() => TaskEntity.fromObject(invalidTask)).toThrow(TaskMasterError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromArray', () => {
|
||||
it('should create array of TaskEntities from plain objects', () => {
|
||||
const plainTasks: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Task 1',
|
||||
description: 'First task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Task 2',
|
||||
description: 'Second task',
|
||||
status: 'in-progress',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
];
|
||||
|
||||
const entities = TaskEntity.fromArray(plainTasks);
|
||||
|
||||
expect(entities).toHaveLength(2);
|
||||
expect(entities[0]).toBeInstanceOf(TaskEntity);
|
||||
expect(entities[1]).toBeInstanceOf(TaskEntity);
|
||||
expect(entities[0].id).toBe('1');
|
||||
expect(entities[1].id).toBe('2');
|
||||
});
|
||||
|
||||
it('should throw validation error if any task is invalid', () => {
|
||||
const tasksWithInvalid: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Valid Task',
|
||||
description: 'First task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Invalid Task',
|
||||
description: '', // Invalid - missing description
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
subtasks: []
|
||||
}
|
||||
];
|
||||
|
||||
expect(() => TaskEntity.fromArray(tasksWithInvalid)).toThrow(
|
||||
TaskMasterError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSON', () => {
|
||||
it('should convert TaskEntity to plain object', () => {
|
||||
const taskData: Task = {
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: ['2', '3'],
|
||||
details: 'Some details',
|
||||
testStrategy: 'Unit tests',
|
||||
subtasks: []
|
||||
};
|
||||
|
||||
const entity = new TaskEntity(taskData);
|
||||
const json = entity.toJSON();
|
||||
|
||||
expect(json).toEqual({
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'A test task',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
dependencies: ['2', '3'],
|
||||
details: 'Some details',
|
||||
testStrategy: 'Unit tests',
|
||||
subtasks: []
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -170,16 +170,13 @@ export class TaskService {
|
||||
storageType
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's a user-facing error (like NO_BRIEF_SELECTED), don't log it as an internal error
|
||||
if (
|
||||
error instanceof TaskMasterError &&
|
||||
error.is(ERROR_CODES.NO_BRIEF_SELECTED)
|
||||
) {
|
||||
// Just re-throw user-facing errors without wrapping
|
||||
// Re-throw all TaskMasterErrors without wrapping
|
||||
// These errors are already user-friendly and have appropriate error codes
|
||||
if (error instanceof TaskMasterError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Log internal errors
|
||||
// Only wrap unknown errors
|
||||
this.logger.error('Failed to get task list', error);
|
||||
throw new TaskMasterError(
|
||||
'Failed to get task list',
|
||||
|
||||
@@ -1,60 +1,57 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig, mergeConfig } from 'vitest/config';
|
||||
import rootConfig from '../../vitest.config';
|
||||
|
||||
// __dirname in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: [
|
||||
'tests/**/*.test.ts',
|
||||
'tests/**/*.spec.ts',
|
||||
'tests/{unit,integration,e2e}/**/*.{test,spec}.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.spec.ts'
|
||||
],
|
||||
exclude: ['node_modules', 'dist', '.git', '.cache'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'tests/',
|
||||
'**/*.test.ts',
|
||||
'**/*.spec.ts',
|
||||
'**/*.d.ts',
|
||||
'**/mocks/**',
|
||||
'**/fixtures/**',
|
||||
'vitest.config.ts',
|
||||
'src/index.ts'
|
||||
/**
|
||||
* Core package Vitest configuration
|
||||
* Extends root config with core-specific settings including:
|
||||
* - Path aliases for cleaner imports
|
||||
* - Test setup file
|
||||
* - Higher coverage thresholds (80%)
|
||||
*/
|
||||
export default mergeConfig(
|
||||
rootConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
// Core-specific test patterns
|
||||
include: [
|
||||
'tests/**/*.test.ts',
|
||||
'tests/**/*.spec.ts',
|
||||
'tests/{unit,integration,e2e}/**/*.{test,spec}.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.spec.ts'
|
||||
],
|
||||
thresholds: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
|
||||
// Core-specific setup
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
|
||||
// Higher coverage thresholds for core package
|
||||
coverage: {
|
||||
thresholds: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80
|
||||
}
|
||||
}
|
||||
},
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
testTimeout: 10000,
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
mockReset: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@/types': path.resolve(__dirname, './src/types'),
|
||||
'@/providers': path.resolve(__dirname, './src/providers'),
|
||||
'@/storage': path.resolve(__dirname, './src/storage'),
|
||||
'@/parser': path.resolve(__dirname, './src/parser'),
|
||||
'@/utils': path.resolve(__dirname, './src/utils'),
|
||||
'@/errors': path.resolve(__dirname, './src/errors')
|
||||
|
||||
// Path aliases for cleaner imports
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@/types': path.resolve(__dirname, './src/types'),
|
||||
'@/providers': path.resolve(__dirname, './src/providers'),
|
||||
'@/storage': path.resolve(__dirname, './src/storage'),
|
||||
'@/parser': path.resolve(__dirname, './src/parser'),
|
||||
'@/utils': path.resolve(__dirname, './src/utils'),
|
||||
'@/errors': path.resolve(__dirname, './src/errors')
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user