chore: add integration tests to new cli and mcp (#1430)

This commit is contained in:
Ralph Khreish
2025-11-20 19:36:17 +01:00
committed by GitHub
parent 4049f34d5a
commit e66150e91c
22 changed files with 13419 additions and 6988 deletions

View File

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

View File

@@ -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"],

View File

@@ -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"],

View 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: []
});
});
});
});

View File

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

View File

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