mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-29 22:02:04 +00:00
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
268 lines
8.2 KiB
TypeScript
268 lines
8.2 KiB
TypeScript
/**
|
|
* @fileoverview Tests for project root finder utilities
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import path from 'node:path';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import {
|
|
findProjectRoot,
|
|
normalizeProjectRoot
|
|
} from './project-root-finder.js';
|
|
|
|
describe('findProjectRoot', () => {
|
|
let tempDir: string;
|
|
let originalCwd: string;
|
|
|
|
beforeEach(() => {
|
|
// Save original working directory
|
|
originalCwd = process.cwd();
|
|
// Create a temporary directory for testing
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore original working directory
|
|
process.chdir(originalCwd);
|
|
// Clean up temp directory
|
|
if (fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('Task Master marker detection', () => {
|
|
it('should find .taskmaster directory in current directory', () => {
|
|
const taskmasterDir = path.join(tempDir, '.taskmaster');
|
|
fs.mkdirSync(taskmasterDir);
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
|
|
it('should find .taskmaster directory in parent directory', () => {
|
|
const parentDir = tempDir;
|
|
const childDir = path.join(tempDir, 'child');
|
|
const taskmasterDir = path.join(parentDir, '.taskmaster');
|
|
|
|
fs.mkdirSync(taskmasterDir);
|
|
fs.mkdirSync(childDir);
|
|
|
|
const result = findProjectRoot(childDir);
|
|
expect(result).toBe(parentDir);
|
|
});
|
|
|
|
it('should find .taskmaster/config.json marker', () => {
|
|
const configDir = path.join(tempDir, '.taskmaster');
|
|
fs.mkdirSync(configDir);
|
|
fs.writeFileSync(path.join(configDir, 'config.json'), '{}');
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
|
|
it('should find .taskmaster/tasks/tasks.json marker', () => {
|
|
const tasksDir = path.join(tempDir, '.taskmaster', 'tasks');
|
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
fs.writeFileSync(path.join(tasksDir, 'tasks.json'), '{}');
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
|
|
it('should find .taskmasterconfig (legacy) marker', () => {
|
|
fs.writeFileSync(path.join(tempDir, '.taskmasterconfig'), '{}');
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
});
|
|
|
|
describe('Monorepo behavior - Task Master markers take precedence', () => {
|
|
it('should find .taskmaster in parent when starting from apps subdirectory', () => {
|
|
// Simulate exact user scenario:
|
|
// /project/.taskmaster exists
|
|
// Starting from /project/apps
|
|
const projectRoot = tempDir;
|
|
const appsDir = path.join(tempDir, 'apps');
|
|
const taskmasterDir = path.join(projectRoot, '.taskmaster');
|
|
|
|
fs.mkdirSync(taskmasterDir);
|
|
fs.mkdirSync(appsDir);
|
|
|
|
// When called from apps directory
|
|
const result = findProjectRoot(appsDir);
|
|
// Should return project root (one level up)
|
|
expect(result).toBe(projectRoot);
|
|
});
|
|
|
|
it('should prioritize .taskmaster in parent over .git in child', () => {
|
|
// Create structure: /parent/.taskmaster and /parent/child/.git
|
|
const parentDir = tempDir;
|
|
const childDir = path.join(tempDir, 'child');
|
|
const gitDir = path.join(childDir, '.git');
|
|
const taskmasterDir = path.join(parentDir, '.taskmaster');
|
|
|
|
fs.mkdirSync(taskmasterDir);
|
|
fs.mkdirSync(childDir);
|
|
fs.mkdirSync(gitDir);
|
|
|
|
// When called from child directory
|
|
const result = findProjectRoot(childDir);
|
|
// Should return parent (with .taskmaster), not child (with .git)
|
|
expect(result).toBe(parentDir);
|
|
});
|
|
|
|
it('should prioritize .taskmaster in grandparent over package.json in child', () => {
|
|
// Create structure: /grandparent/.taskmaster and /grandparent/parent/child/package.json
|
|
const grandparentDir = tempDir;
|
|
const parentDir = path.join(tempDir, 'parent');
|
|
const childDir = path.join(parentDir, 'child');
|
|
const taskmasterDir = path.join(grandparentDir, '.taskmaster');
|
|
|
|
fs.mkdirSync(taskmasterDir);
|
|
fs.mkdirSync(parentDir);
|
|
fs.mkdirSync(childDir);
|
|
fs.writeFileSync(path.join(childDir, 'package.json'), '{}');
|
|
|
|
const result = findProjectRoot(childDir);
|
|
expect(result).toBe(grandparentDir);
|
|
});
|
|
|
|
it('should prioritize .taskmaster over multiple other project markers', () => {
|
|
// Create structure with many markers
|
|
const parentDir = tempDir;
|
|
const childDir = path.join(tempDir, 'packages', 'my-package');
|
|
const taskmasterDir = path.join(parentDir, '.taskmaster');
|
|
|
|
fs.mkdirSync(taskmasterDir);
|
|
fs.mkdirSync(childDir, { recursive: true });
|
|
|
|
// Add multiple other project markers in child
|
|
fs.mkdirSync(path.join(childDir, '.git'));
|
|
fs.writeFileSync(path.join(childDir, 'package.json'), '{}');
|
|
fs.writeFileSync(path.join(childDir, 'go.mod'), '');
|
|
fs.writeFileSync(path.join(childDir, 'Cargo.toml'), '');
|
|
|
|
const result = findProjectRoot(childDir);
|
|
// Should still return parent with .taskmaster
|
|
expect(result).toBe(parentDir);
|
|
});
|
|
});
|
|
|
|
describe('Other project marker detection (when no Task Master markers)', () => {
|
|
it('should find .git directory', () => {
|
|
const gitDir = path.join(tempDir, '.git');
|
|
fs.mkdirSync(gitDir);
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
|
|
it('should find package.json', () => {
|
|
fs.writeFileSync(path.join(tempDir, 'package.json'), '{}');
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
|
|
it('should find go.mod', () => {
|
|
fs.writeFileSync(path.join(tempDir, 'go.mod'), '');
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
|
|
it('should find Cargo.toml (Rust)', () => {
|
|
fs.writeFileSync(path.join(tempDir, 'Cargo.toml'), '');
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
|
|
it('should find pyproject.toml (Python)', () => {
|
|
fs.writeFileSync(path.join(tempDir, 'pyproject.toml'), '');
|
|
|
|
const result = findProjectRoot(tempDir);
|
|
expect(result).toBe(tempDir);
|
|
});
|
|
});
|
|
|
|
describe('Edge cases', () => {
|
|
it('should return current directory if no markers found', () => {
|
|
const result = findProjectRoot(tempDir);
|
|
// Should fall back to process.cwd()
|
|
expect(result).toBe(process.cwd());
|
|
});
|
|
|
|
it('should handle permission errors gracefully', () => {
|
|
// This test is hard to implement portably, but the function should handle it
|
|
const result = findProjectRoot(tempDir);
|
|
expect(typeof result).toBe('string');
|
|
});
|
|
|
|
it('should not traverse more than 50 levels', () => {
|
|
// Create a deep directory structure
|
|
let deepDir = tempDir;
|
|
for (let i = 0; i < 60; i++) {
|
|
deepDir = path.join(deepDir, `level${i}`);
|
|
}
|
|
// Don't actually create it, just test the function doesn't hang
|
|
const result = findProjectRoot(deepDir);
|
|
expect(typeof result).toBe('string');
|
|
});
|
|
|
|
it('should handle being called from filesystem root', () => {
|
|
const rootDir = path.parse(tempDir).root;
|
|
const result = findProjectRoot(rootDir);
|
|
expect(typeof result).toBe('string');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('normalizeProjectRoot', () => {
|
|
it('should remove .taskmaster from path', () => {
|
|
const result = normalizeProjectRoot('/project/.taskmaster');
|
|
expect(result).toBe('/project');
|
|
});
|
|
|
|
it('should remove .taskmaster/subdirectory from path', () => {
|
|
const result = normalizeProjectRoot('/project/.taskmaster/tasks');
|
|
expect(result).toBe('/project');
|
|
});
|
|
|
|
it('should return unchanged path if no .taskmaster', () => {
|
|
const result = normalizeProjectRoot('/project/src');
|
|
expect(result).toBe('/project/src');
|
|
});
|
|
|
|
it('should handle paths with native separators', () => {
|
|
// Use native path separators for the test
|
|
const testPath = ['project', '.taskmaster', 'tasks'].join(path.sep);
|
|
const expectedPath = 'project';
|
|
const result = normalizeProjectRoot(testPath);
|
|
expect(result).toBe(expectedPath);
|
|
});
|
|
|
|
it('should handle empty string', () => {
|
|
const result = normalizeProjectRoot('');
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should handle null', () => {
|
|
const result = normalizeProjectRoot(null);
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should handle undefined', () => {
|
|
const result = normalizeProjectRoot(undefined);
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
it('should handle root .taskmaster', () => {
|
|
const sep = path.sep;
|
|
const result = normalizeProjectRoot(`${sep}.taskmaster`);
|
|
expect(result).toBe(sep);
|
|
});
|
|
});
|