mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-11-16 18:14:43 +00:00
* feat: add support for claude code context - code context for: - add-task - update-subtask - update-task - update * feat: fix CI and format + refactor * chore: format * chore: fix test * feat: add gemini-cli support for codebase context * feat: add google cli integration and fix tests * chore: apply requested coderabbit changes * chore: bump gemini cli package
472 lines
13 KiB
JavaScript
472 lines
13 KiB
JavaScript
// In tests/unit/parse-prd.test.js
|
|
// Testing parse-prd.js file extension compatibility with real files
|
|
|
|
import { jest } from '@jest/globals';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import os from 'os';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// Mock the AI services to avoid real API calls
|
|
jest.unstable_mockModule(
|
|
'../../scripts/modules/ai-services-unified.js',
|
|
() => ({
|
|
streamTextService: jest.fn(),
|
|
generateObjectService: jest.fn(),
|
|
streamObjectService: jest.fn().mockImplementation(async () => {
|
|
return {
|
|
get partialObjectStream() {
|
|
return (async function* () {
|
|
yield { tasks: [] };
|
|
yield { tasks: [{ id: 1, title: 'Test Task', priority: 'high' }] };
|
|
})();
|
|
},
|
|
object: Promise.resolve({
|
|
tasks: [{ id: 1, title: 'Test Task', priority: 'high' }]
|
|
})
|
|
};
|
|
})
|
|
})
|
|
);
|
|
|
|
// Mock all config-manager exports comprehensively
|
|
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
|
getDebugFlag: jest.fn(() => false),
|
|
getDefaultPriority: jest.fn(() => 'medium'),
|
|
getMainModelId: jest.fn(() => 'test-model'),
|
|
getResearchModelId: jest.fn(() => 'test-research-model'),
|
|
getParametersForRole: jest.fn(() => ({ maxTokens: 1000, temperature: 0.7 })),
|
|
getMainProvider: jest.fn(() => 'anthropic'),
|
|
getResearchProvider: jest.fn(() => 'perplexity'),
|
|
getFallbackProvider: jest.fn(() => 'anthropic'),
|
|
getResponseLanguage: jest.fn(() => 'English'),
|
|
getDefaultNumTasks: jest.fn(() => 10),
|
|
getDefaultSubtasks: jest.fn(() => 5),
|
|
getLogLevel: jest.fn(() => 'info'),
|
|
getConfig: jest.fn(() => ({})),
|
|
getAllProviders: jest.fn(() => ['anthropic', 'perplexity']),
|
|
MODEL_MAP: {},
|
|
VALID_PROVIDERS: ['anthropic', 'perplexity'],
|
|
validateProvider: jest.fn(() => true),
|
|
validateProviderModelCombination: jest.fn(() => true),
|
|
isApiKeySet: jest.fn(() => true),
|
|
hasCodebaseAnalysis: jest.fn(() => false)
|
|
}));
|
|
|
|
// Mock utils comprehensively to prevent CLI behavior
|
|
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
|
|
log: jest.fn(),
|
|
writeJSON: jest.fn(),
|
|
enableSilentMode: jest.fn(),
|
|
disableSilentMode: jest.fn(),
|
|
isSilentMode: jest.fn(() => false),
|
|
getCurrentTag: jest.fn(() => 'master'),
|
|
ensureTagMetadata: jest.fn(),
|
|
readJSON: jest.fn(() => ({ master: { tasks: [] } })),
|
|
findProjectRoot: jest.fn(() => '/tmp/test'),
|
|
resolveEnvVariable: jest.fn(() => 'mock-key'),
|
|
findTaskById: jest.fn(() => null),
|
|
findTaskByPattern: jest.fn(() => []),
|
|
validateTaskId: jest.fn(() => true),
|
|
createTask: jest.fn(() => ({ id: 1, title: 'Mock Task' })),
|
|
sortByDependencies: jest.fn((tasks) => tasks),
|
|
isEmpty: jest.fn(() => false),
|
|
truncate: jest.fn((text) => text),
|
|
slugify: jest.fn((text) => text.toLowerCase()),
|
|
getTagFromPath: jest.fn(() => 'master'),
|
|
isValidTag: jest.fn(() => true),
|
|
migrateToTaggedFormat: jest.fn(() => ({ master: { tasks: [] } })),
|
|
performCompleteTagMigration: jest.fn(),
|
|
resolveCurrentTag: jest.fn(() => 'master'),
|
|
getDefaultTag: jest.fn(() => 'master'),
|
|
performMigrationIfNeeded: jest.fn()
|
|
}));
|
|
|
|
// Mock prompt manager
|
|
jest.unstable_mockModule('../../scripts/modules/prompt-manager.js', () => ({
|
|
getPromptManager: jest.fn(() => ({
|
|
loadPrompt: jest.fn(() => ({
|
|
systemPrompt: 'Test system prompt',
|
|
userPrompt: 'Test user prompt'
|
|
}))
|
|
}))
|
|
}));
|
|
|
|
// Mock progress/UI components to prevent real CLI UI
|
|
jest.unstable_mockModule('../../src/progress/parse-prd-tracker.js', () => ({
|
|
createParsePrdTracker: jest.fn(() => ({
|
|
start: jest.fn(),
|
|
stop: jest.fn(),
|
|
cleanup: jest.fn(),
|
|
addTaskLine: jest.fn(),
|
|
updateTokens: jest.fn(),
|
|
complete: jest.fn(),
|
|
getSummary: jest.fn().mockReturnValue({
|
|
taskPriorities: { high: 0, medium: 0, low: 0 },
|
|
elapsedTime: 0,
|
|
actionVerb: 'generated'
|
|
})
|
|
}))
|
|
}));
|
|
|
|
jest.unstable_mockModule('../../src/ui/parse-prd.js', () => ({
|
|
displayParsePrdStart: jest.fn(),
|
|
displayParsePrdSummary: jest.fn()
|
|
}));
|
|
|
|
jest.unstable_mockModule('../../scripts/modules/ui.js', () => ({
|
|
displayAiUsageSummary: jest.fn()
|
|
}));
|
|
|
|
// Mock task generation to prevent file operations
|
|
jest.unstable_mockModule(
|
|
'../../scripts/modules/task-manager/generate-task-files.js',
|
|
() => ({
|
|
default: jest.fn()
|
|
})
|
|
);
|
|
|
|
// Mock stream parser
|
|
jest.unstable_mockModule('../../src/utils/stream-parser.js', () => {
|
|
// Define mock StreamingError class
|
|
class StreamingError extends Error {
|
|
constructor(message, code) {
|
|
super(message);
|
|
this.name = 'StreamingError';
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
// Define mock error codes
|
|
const STREAMING_ERROR_CODES = {
|
|
NOT_ASYNC_ITERABLE: 'STREAMING_NOT_SUPPORTED',
|
|
STREAM_PROCESSING_FAILED: 'STREAM_PROCESSING_FAILED',
|
|
STREAM_NOT_ITERABLE: 'STREAM_NOT_ITERABLE'
|
|
};
|
|
|
|
return {
|
|
parseStream: jest.fn(),
|
|
StreamingError,
|
|
STREAMING_ERROR_CODES
|
|
};
|
|
});
|
|
|
|
// Mock other potential UI elements
|
|
jest.unstable_mockModule('ora', () => ({
|
|
default: jest.fn(() => ({
|
|
start: jest.fn(),
|
|
stop: jest.fn(),
|
|
succeed: jest.fn(),
|
|
fail: jest.fn()
|
|
}))
|
|
}));
|
|
|
|
jest.unstable_mockModule('chalk', () => ({
|
|
default: {
|
|
red: jest.fn((text) => text),
|
|
green: jest.fn((text) => text),
|
|
blue: jest.fn((text) => text),
|
|
yellow: jest.fn((text) => text),
|
|
cyan: jest.fn((text) => text),
|
|
white: {
|
|
bold: jest.fn((text) => text)
|
|
}
|
|
},
|
|
red: jest.fn((text) => text),
|
|
green: jest.fn((text) => text),
|
|
blue: jest.fn((text) => text),
|
|
yellow: jest.fn((text) => text),
|
|
cyan: jest.fn((text) => text),
|
|
white: {
|
|
bold: jest.fn((text) => text)
|
|
}
|
|
}));
|
|
|
|
// Mock boxen
|
|
jest.unstable_mockModule('boxen', () => ({
|
|
default: jest.fn((content) => content)
|
|
}));
|
|
|
|
// Mock constants
|
|
jest.unstable_mockModule('../../src/constants/task-priority.js', () => ({
|
|
DEFAULT_TASK_PRIORITY: 'medium',
|
|
TASK_PRIORITY_OPTIONS: ['low', 'medium', 'high']
|
|
}));
|
|
|
|
// Mock UI indicators
|
|
jest.unstable_mockModule('../../src/ui/indicators.js', () => ({
|
|
getPriorityIndicators: jest.fn(() => ({
|
|
high: '🔴',
|
|
medium: '🟡',
|
|
low: '🟢'
|
|
}))
|
|
}));
|
|
|
|
// Import modules after mocking
|
|
const { generateObjectService } = await import(
|
|
'../../scripts/modules/ai-services-unified.js'
|
|
);
|
|
const parsePRD = (
|
|
await import('../../scripts/modules/task-manager/parse-prd/parse-prd.js')
|
|
).default;
|
|
|
|
describe('parse-prd file extension compatibility', () => {
|
|
let tempDir;
|
|
let testFiles;
|
|
|
|
const mockTasksResponse = {
|
|
tasks: [
|
|
{
|
|
id: 1,
|
|
title: 'Test Task 1',
|
|
description: 'First test task',
|
|
status: 'pending',
|
|
dependencies: [],
|
|
priority: 'high',
|
|
details: 'Implementation details for task 1',
|
|
testStrategy: 'Unit tests for task 1'
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Test Task 2',
|
|
description: 'Second test task',
|
|
status: 'pending',
|
|
dependencies: [1],
|
|
priority: 'medium',
|
|
details: 'Implementation details for task 2',
|
|
testStrategy: 'Integration tests for task 2'
|
|
}
|
|
],
|
|
metadata: {
|
|
projectName: 'Test Project',
|
|
totalTasks: 2,
|
|
sourceFile: 'test-prd',
|
|
generatedAt: new Date().toISOString()
|
|
}
|
|
};
|
|
|
|
const samplePRDContent = `# Test Project PRD
|
|
|
|
## Overview
|
|
Build a simple task management application.
|
|
|
|
## Features
|
|
1. Create and manage tasks
|
|
2. Set task priorities
|
|
3. Track task dependencies
|
|
|
|
## Technical Requirements
|
|
- React frontend
|
|
- Node.js backend
|
|
- PostgreSQL database
|
|
|
|
## Success Criteria
|
|
- Users can create tasks successfully
|
|
- Task dependencies work correctly`;
|
|
|
|
beforeAll(() => {
|
|
// Create temporary directory for test files
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parse-prd-test-'));
|
|
|
|
// Create test files with different extensions
|
|
testFiles = {
|
|
txt: path.join(tempDir, 'test-prd.txt'),
|
|
md: path.join(tempDir, 'test-prd.md'),
|
|
rst: path.join(tempDir, 'test-prd.rst'),
|
|
noExt: path.join(tempDir, 'test-prd')
|
|
};
|
|
|
|
// Write the same content to all test files
|
|
Object.values(testFiles).forEach((filePath) => {
|
|
fs.writeFileSync(filePath, samplePRDContent);
|
|
});
|
|
|
|
// Mock process.exit to prevent actual exit
|
|
jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
|
|
// Mock console methods to prevent output
|
|
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Clean up temporary directory
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
|
|
// Restore mocks
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
|
|
// Mock successful AI response
|
|
generateObjectService.mockResolvedValue({
|
|
mainResult: { object: mockTasksResponse },
|
|
telemetryData: {
|
|
timestamp: new Date().toISOString(),
|
|
userId: 'test-user',
|
|
commandName: 'parse-prd',
|
|
modelUsed: 'test-model',
|
|
providerName: 'test-provider',
|
|
inputTokens: 100,
|
|
outputTokens: 200,
|
|
totalTokens: 300,
|
|
totalCost: 0.01,
|
|
currency: 'USD'
|
|
}
|
|
});
|
|
});
|
|
|
|
test('should accept and parse .txt files', async () => {
|
|
const outputPath = path.join(tempDir, 'tasks-txt.json');
|
|
|
|
const result = await parsePRD(testFiles.txt, outputPath, 2, {
|
|
force: true,
|
|
mcpLog: {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
success: jest.fn()
|
|
},
|
|
projectRoot: tempDir
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.tasksPath).toBe(outputPath);
|
|
expect(fs.existsSync(outputPath)).toBe(true);
|
|
|
|
// Verify the content was parsed correctly
|
|
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
|
expect(tasksData.master.tasks).toHaveLength(2);
|
|
expect(tasksData.master.tasks[0].title).toBe('Test Task 1');
|
|
});
|
|
|
|
test('should accept and parse .md files', async () => {
|
|
const outputPath = path.join(tempDir, 'tasks-md.json');
|
|
|
|
const result = await parsePRD(testFiles.md, outputPath, 2, {
|
|
force: true,
|
|
mcpLog: {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
success: jest.fn()
|
|
},
|
|
projectRoot: tempDir
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.tasksPath).toBe(outputPath);
|
|
expect(fs.existsSync(outputPath)).toBe(true);
|
|
|
|
// Verify the content was parsed correctly
|
|
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
|
expect(tasksData.master.tasks).toHaveLength(2);
|
|
});
|
|
|
|
test('should accept and parse files with other text extensions', async () => {
|
|
const outputPath = path.join(tempDir, 'tasks-rst.json');
|
|
|
|
const result = await parsePRD(testFiles.rst, outputPath, 2, {
|
|
force: true,
|
|
mcpLog: {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
success: jest.fn()
|
|
},
|
|
projectRoot: tempDir
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.tasksPath).toBe(outputPath);
|
|
expect(fs.existsSync(outputPath)).toBe(true);
|
|
});
|
|
|
|
test('should accept and parse files with no extension', async () => {
|
|
const outputPath = path.join(tempDir, 'tasks-noext.json');
|
|
|
|
const result = await parsePRD(testFiles.noExt, outputPath, 2, {
|
|
force: true,
|
|
mcpLog: {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
success: jest.fn()
|
|
},
|
|
projectRoot: tempDir
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.tasksPath).toBe(outputPath);
|
|
expect(fs.existsSync(outputPath)).toBe(true);
|
|
});
|
|
|
|
test('should produce identical results regardless of file extension', async () => {
|
|
const outputs = {};
|
|
|
|
// Parse each file type with a unique project root to avoid ID conflicts
|
|
for (const [ext, filePath] of Object.entries(testFiles)) {
|
|
// Create a unique subdirectory for each test to isolate them
|
|
const testSubDir = path.join(tempDir, `test-${ext}`);
|
|
fs.mkdirSync(testSubDir, { recursive: true });
|
|
|
|
const outputPath = path.join(testSubDir, `tasks.json`);
|
|
|
|
await parsePRD(filePath, outputPath, 2, {
|
|
force: true,
|
|
mcpLog: {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
success: jest.fn()
|
|
},
|
|
projectRoot: testSubDir
|
|
});
|
|
|
|
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
|
outputs[ext] = tasksData;
|
|
}
|
|
|
|
// Compare all outputs - they should be identical (except metadata timestamps)
|
|
const baseOutput = outputs.txt;
|
|
Object.values(outputs).forEach((output) => {
|
|
expect(output.master.tasks).toEqual(baseOutput.master.tasks);
|
|
expect(output.master.metadata.projectName).toEqual(
|
|
baseOutput.master.metadata.projectName
|
|
);
|
|
expect(output.master.metadata.totalTasks).toEqual(
|
|
baseOutput.master.metadata.totalTasks
|
|
);
|
|
});
|
|
});
|
|
|
|
test('should handle non-existent files gracefully', async () => {
|
|
const nonExistentFile = path.join(tempDir, 'does-not-exist.txt');
|
|
const outputPath = path.join(tempDir, 'tasks-error.json');
|
|
|
|
await expect(
|
|
parsePRD(nonExistentFile, outputPath, 2, {
|
|
force: true,
|
|
mcpLog: {
|
|
info: jest.fn(),
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
debug: jest.fn(),
|
|
success: jest.fn()
|
|
},
|
|
projectRoot: tempDir
|
|
})
|
|
).rejects.toThrow();
|
|
});
|
|
});
|