Files
claude-task-master/tests/unit/task-master.test.js
Ralph Khreish 21392a1117 fix: more regression bugs (#956)
* fix: more regression bugs

* chore: fix format

* chore: fix unit tests

* chore: fix format
2025-07-11 14:23:54 +02:00

440 lines
13 KiB
JavaScript

/**
* Tests for task-master.js initTaskMaster function
*/
import { jest } from '@jest/globals';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { initTaskMaster, TaskMaster } from '../../src/task-master.js';
import {
TASKMASTER_DIR,
TASKMASTER_TASKS_FILE,
LEGACY_CONFIG_FILE,
TASKMASTER_CONFIG_FILE,
LEGACY_TASKS_FILE
} from '../../src/constants/paths.js';
// Mock the console to prevent noise during tests
jest.spyOn(console, 'error').mockImplementation(() => {});
describe('initTaskMaster', () => {
let tempDir;
let originalCwd;
beforeEach(() => {
// Create a temporary directory for testing
tempDir = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'taskmaster-test-'))
);
originalCwd = process.cwd();
// Clear all mocks
jest.clearAllMocks();
});
afterEach(() => {
// Restore original working directory
process.chdir(originalCwd);
// Clean up temporary directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('Project root detection', () => {
test('should find project root when .taskmaster directory exists', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Change to temp directory
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
expect(taskMaster).toBeInstanceOf(TaskMaster);
});
test('should find project root when legacy config file exists', () => {
// Arrange - Create legacy config file in temp dir
const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
fs.writeFileSync(legacyConfigPath, '{}');
// Change to temp directory
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should find project root from subdirectory', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Create a subdirectory and change to it
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
process.chdir(srcDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should find project root from deeply nested subdirectory', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Create deeply nested subdirectory and change to it
const deepDir = path.join(tempDir, 'src', 'components', 'ui');
fs.mkdirSync(deepDir, { recursive: true });
process.chdir(deepDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should throw error when no project markers found', () => {
// Arrange - Empty temp directory, no project markers
process.chdir(tempDir);
// Act & Assert
expect(() => {
initTaskMaster({});
}).toThrow(
'Unable to find project root. No project markers found. Run "init" command first.'
);
});
});
describe('Project root override validation', () => {
test('should accept valid project root override with .taskmaster directory', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Act
const taskMaster = initTaskMaster({ projectRoot: tempDir });
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should accept valid project root override with legacy config', () => {
// Arrange - Create legacy config file in temp dir
const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
fs.writeFileSync(legacyConfigPath, '{}');
// Act
const taskMaster = initTaskMaster({ projectRoot: tempDir });
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should throw error when project root override does not exist', () => {
// Arrange - Non-existent path
const nonExistentPath = path.join(tempDir, 'does-not-exist');
// Act & Assert
expect(() => {
initTaskMaster({ projectRoot: nonExistentPath });
}).toThrow(
`Project root override path does not exist: ${nonExistentPath}`
);
});
test('should throw error when project root override has no project markers', () => {
// Arrange - Empty temp directory (no project markers)
// Act & Assert
expect(() => {
initTaskMaster({ projectRoot: tempDir });
}).toThrow(
`Project root override is not a valid taskmaster project: ${tempDir}`
);
});
test('should resolve relative project root override', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Create subdirectory and change to it
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
process.chdir(srcDir);
// Act - Use relative path '../' to go back to project root
const taskMaster = initTaskMaster({ projectRoot: '../' });
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
});
describe('Path resolution with boolean logic', () => {
let taskMasterDir, tasksPath, configPath, statePath;
beforeEach(() => {
// Setup a valid project structure
taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
fs.mkdirSync(path.dirname(tasksPath), { recursive: true });
fs.writeFileSync(tasksPath, '[]');
configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE);
fs.writeFileSync(configPath, '{}');
statePath = path.join(taskMasterDir, 'state.json');
fs.writeFileSync(statePath, '{}');
process.chdir(tempDir);
});
test('should return paths when required (true) and files exist', () => {
// Act
const taskMaster = initTaskMaster({
tasksPath: true,
configPath: true,
statePath: true
});
// Assert
expect(taskMaster.getTasksPath()).toBe(tasksPath);
expect(taskMaster.getConfigPath()).toBe(configPath);
expect(taskMaster.getStatePath()).toBe(statePath);
});
test('should throw error when required (true) files do not exist', () => {
// Arrange - Remove tasks file
fs.unlinkSync(tasksPath);
// Act & Assert
expect(() => {
initTaskMaster({ tasksPath: true });
}).toThrow(
'Required tasks file not found. Searched: .taskmaster/tasks/tasks.json, tasks/tasks.json'
);
});
test('should return null when optional (false/undefined) files do not exist', () => {
// Arrange - Remove tasks file
fs.unlinkSync(tasksPath);
// Act
const taskMaster = initTaskMaster({
tasksPath: false
});
// Assert
expect(taskMaster.getTasksPath()).toBeNull();
});
test('should return default paths when optional files not specified in overrides', () => {
// Arrange - Remove all optional files
fs.unlinkSync(tasksPath);
fs.unlinkSync(configPath);
fs.unlinkSync(statePath);
// Act - Don't specify any optional paths
const taskMaster = initTaskMaster({});
// Assert - Should return absolute paths with default locations
expect(taskMaster.getTasksPath()).toBe(
path.join(tempDir, TASKMASTER_TASKS_FILE)
);
expect(taskMaster.getConfigPath()).toBe(
path.join(tempDir, TASKMASTER_CONFIG_FILE)
);
expect(taskMaster.getStatePath()).toBe(
path.join(tempDir, TASKMASTER_DIR, 'state.json')
);
});
});
describe('String path overrides', () => {
let taskMasterDir;
beforeEach(() => {
taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
process.chdir(tempDir);
});
test('should accept valid absolute path override', () => {
// Arrange - Create custom tasks file
const customTasksPath = path.join(tempDir, 'custom-tasks.json');
fs.writeFileSync(customTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({
tasksPath: customTasksPath
});
// Assert
expect(taskMaster.getTasksPath()).toBe(customTasksPath);
});
test('should accept valid relative path override', () => {
// Arrange - Create custom tasks file
const customTasksPath = path.join(tempDir, 'custom-tasks.json');
fs.writeFileSync(customTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({
tasksPath: './custom-tasks.json'
});
// Assert
expect(taskMaster.getTasksPath()).toBe(customTasksPath);
});
test('should throw error when string path override does not exist', () => {
// Arrange - Non-existent file path
const nonExistentPath = path.join(tempDir, 'does-not-exist.json');
// Act & Assert
expect(() => {
initTaskMaster({ tasksPath: nonExistentPath });
}).toThrow(`tasks file override path does not exist: ${nonExistentPath}`);
});
});
describe('Legacy file support', () => {
beforeEach(() => {
// Setup basic project structure
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
process.chdir(tempDir);
});
test('should find legacy tasks file when new format does not exist', () => {
// Arrange - Create legacy tasks file
const legacyTasksDir = path.join(tempDir, 'tasks');
fs.mkdirSync(legacyTasksDir, { recursive: true });
const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE);
fs.writeFileSync(legacyTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({ tasksPath: true });
// Assert
expect(taskMaster.getTasksPath()).toBe(legacyTasksPath);
});
test('should prefer new format over legacy when both exist', () => {
// Arrange - Create both new and legacy files
const newTasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
fs.mkdirSync(path.dirname(newTasksPath), { recursive: true });
fs.writeFileSync(newTasksPath, '[]');
const legacyTasksDir = path.join(tempDir, 'tasks');
fs.mkdirSync(legacyTasksDir, { recursive: true });
const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE);
fs.writeFileSync(legacyTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({ tasksPath: true });
// Assert
expect(taskMaster.getTasksPath()).toBe(newTasksPath);
});
test('should find legacy config file when new format does not exist', () => {
// Arrange - Create legacy config file
const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
fs.writeFileSync(legacyConfigPath, '{}');
// Act
const taskMaster = initTaskMaster({ configPath: true });
// Assert
expect(taskMaster.getConfigPath()).toBe(legacyConfigPath);
});
});
describe('TaskMaster class methods', () => {
test('should return all paths via getAllPaths method', () => {
// Arrange - Setup project with all files
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
const tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
fs.mkdirSync(path.dirname(tasksPath), { recursive: true });
fs.writeFileSync(tasksPath, '[]');
const configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE);
fs.writeFileSync(configPath, '{}');
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({
tasksPath: true,
configPath: true
});
// Assert
const allPaths = taskMaster.getAllPaths();
expect(allPaths).toEqual(
expect.objectContaining({
projectRoot: tempDir,
taskMasterDir: taskMasterDir,
tasksPath: tasksPath,
configPath: configPath
})
);
// Verify paths object is frozen
expect(() => {
allPaths.projectRoot = '/different/path';
}).toThrow();
});
test('should return correct individual paths', () => {
// Arrange
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
expect(taskMaster.getTaskMasterDir()).toBe(taskMasterDir);
// Default paths are always set for tasks, config, and state
expect(taskMaster.getTasksPath()).toBe(
path.join(tempDir, TASKMASTER_TASKS_FILE)
);
expect(taskMaster.getConfigPath()).toBe(
path.join(tempDir, TASKMASTER_CONFIG_FILE)
);
expect(taskMaster.getStatePath()).toBe(
path.join(taskMasterDir, 'state.json')
);
// PRD and complexity report paths are undefined when not provided
expect(taskMaster.getPrdPath()).toBeUndefined();
expect(taskMaster.getComplexityReportPath()).toBeUndefined();
});
});
});