fix merge conflicts to prep for merge with branch next

- Enhance E2E testing and LLM analysis report and:
  - Add --analyze-log flag to run_e2e.sh to re-run LLM analysis on existing logs.
  - Add test:e2e and analyze-log scripts to package.json for easier execution.

- Correct display errors and dependency validation output:
  - Update chalk usage in add-task.js to use bracket notation (chalk[color]) compatible with v5, resolving 'chalk.keyword is not a function' error.
  - Modify fix-dependencies command output to show red failure box with issue count instead of green success box when validation fails.

- Refactor interactive model setup:
  - Verify inclusion of 'No change' option during interactive model setup flow (task-master models --setup).

- Update model definitions:
  - Add max_tokens field for gpt-4o in supported-models.json.

- Remove unused scripts:
  - Delete prepare-package.js and rule-transformer.test.js.

Release candidate
This commit is contained in:
Eyal Toledano
2025-04-29 01:54:42 -04:00
48 changed files with 4744 additions and 1237 deletions

View File

@@ -199,16 +199,35 @@ describe('Commands Module', () => {
// Use input option if file argument not provided
const inputFile = file || options.input;
const defaultPrdPath = 'scripts/prd.txt';
const append = options.append || false;
const force = options.force || false;
const outputPath = options.output || 'tasks/tasks.json';
// Mock confirmOverwriteIfNeeded function to test overwrite behavior
const mockConfirmOverwrite = jest.fn().mockResolvedValue(true);
// Helper function to check if tasks.json exists and confirm overwrite
async function confirmOverwriteIfNeeded() {
if (fs.existsSync(outputPath) && !force && !append) {
return mockConfirmOverwrite();
}
return true;
}
// If no input file specified, check for default PRD location
if (!inputFile) {
if (fs.existsSync(defaultPrdPath)) {
console.log(chalk.blue(`Using default PRD file: ${defaultPrdPath}`));
const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
// Check if we need to confirm overwrite
if (!(await confirmOverwriteIfNeeded())) return;
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
await mockParsePRD(defaultPrdPath, outputPath, numTasks);
if (append) {
console.log(chalk.blue('Appending to existing tasks...'));
}
await mockParsePRD(defaultPrdPath, outputPath, numTasks, { append });
return;
}
@@ -221,12 +240,20 @@ describe('Commands Module', () => {
}
const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
// Check if we need to confirm overwrite
if (!(await confirmOverwriteIfNeeded())) return;
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
if (append) {
console.log(chalk.blue('Appending to existing tasks...'));
}
await mockParsePRD(inputFile, outputPath, numTasks);
await mockParsePRD(inputFile, outputPath, numTasks, { append });
// Return mock for testing
return { mockConfirmOverwrite };
}
beforeEach(() => {
@@ -252,7 +279,8 @@ describe('Commands Module', () => {
expect(mockParsePRD).toHaveBeenCalledWith(
'scripts/prd.txt',
'tasks/tasks.json',
10 // Default value from command definition
10, // Default value from command definition
{ append: false }
);
});
@@ -290,7 +318,8 @@ describe('Commands Module', () => {
expect(mockParsePRD).toHaveBeenCalledWith(
testFile,
'tasks/tasks.json',
10
10,
{ append: false }
);
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
});
@@ -313,7 +342,8 @@ describe('Commands Module', () => {
expect(mockParsePRD).toHaveBeenCalledWith(
testFile,
'tasks/tasks.json',
10
10,
{ append: false }
);
expect(mockExistsSync).not.toHaveBeenCalledWith('scripts/prd.txt');
});
@@ -331,7 +361,126 @@ describe('Commands Module', () => {
});
// Assert
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, numTasks);
expect(mockParsePRD).toHaveBeenCalledWith(
testFile,
outputFile,
numTasks,
{ append: false }
);
});
test('should pass append flag to parsePRD when provided', async () => {
// Arrange
const testFile = 'test/prd.txt';
// Act - call the handler directly with append flag
await parsePrdAction(testFile, {
numTasks: '10',
output: 'tasks/tasks.json',
append: true
});
// Assert
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('Appending to existing tasks')
);
expect(mockParsePRD).toHaveBeenCalledWith(
testFile,
'tasks/tasks.json',
10,
{ append: true }
);
});
test('should bypass confirmation when append flag is true and tasks.json exists', async () => {
// Arrange
const testFile = 'test/prd.txt';
const outputFile = 'tasks/tasks.json';
// Mock that tasks.json exists
mockExistsSync.mockImplementation((path) => {
if (path === outputFile) return true;
if (path === testFile) return true;
return false;
});
// Act - call the handler with append flag
const { mockConfirmOverwrite } =
(await parsePrdAction(testFile, {
numTasks: '10',
output: outputFile,
append: true
})) || {};
// Assert - confirm overwrite should not be called with append flag
expect(mockConfirmOverwrite).not.toHaveBeenCalled();
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
append: true
});
// Reset mock implementation
mockExistsSync.mockReset();
});
test('should prompt for confirmation when append flag is false and tasks.json exists', async () => {
// Arrange
const testFile = 'test/prd.txt';
const outputFile = 'tasks/tasks.json';
// Mock that tasks.json exists
mockExistsSync.mockImplementation((path) => {
if (path === outputFile) return true;
if (path === testFile) return true;
return false;
});
// Act - call the handler without append flag
const { mockConfirmOverwrite } =
(await parsePrdAction(testFile, {
numTasks: '10',
output: outputFile
// append: false (default)
})) || {};
// Assert - confirm overwrite should be called without append flag
expect(mockConfirmOverwrite).toHaveBeenCalled();
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
append: false
});
// Reset mock implementation
mockExistsSync.mockReset();
});
test('should bypass confirmation when force flag is true, regardless of append flag', async () => {
// Arrange
const testFile = 'test/prd.txt';
const outputFile = 'tasks/tasks.json';
// Mock that tasks.json exists
mockExistsSync.mockImplementation((path) => {
if (path === outputFile) return true;
if (path === testFile) return true;
return false;
});
// Act - call the handler with force flag
const { mockConfirmOverwrite } =
(await parsePrdAction(testFile, {
numTasks: '10',
output: outputFile,
force: true,
append: false
})) || {};
// Assert - confirm overwrite should not be called with force flag
expect(mockConfirmOverwrite).not.toHaveBeenCalled();
expect(mockParsePRD).toHaveBeenCalledWith(testFile, outputFile, 10, {
append: false
});
// Reset mock implementation
mockExistsSync.mockReset();
});
});

View File

@@ -0,0 +1,182 @@
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Mock external modules
jest.mock('child_process', () => ({
execSync: jest.fn()
}));
// Mock console methods
jest.mock('console', () => ({
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
clear: jest.fn()
}));
describe('Roo Integration', () => {
let tempDir;
beforeEach(() => {
jest.clearAllMocks();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
// Spy on fs methods
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
if (filePath.toString().includes('.roomodes')) {
return 'Existing roomodes content';
}
if (filePath.toString().includes('-rules')) {
return 'Existing mode rules content';
}
return '{}';
});
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
});
afterEach(() => {
// Clean up the temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
console.error(`Error cleaning up: ${err.message}`);
}
});
// Test function that simulates the createProjectStructure behavior for Roo files
function mockCreateRooStructure() {
// Create main .roo directory
fs.mkdirSync(path.join(tempDir, '.roo'), { recursive: true });
// Create rules directory
fs.mkdirSync(path.join(tempDir, '.roo', 'rules'), { recursive: true });
// Create mode-specific rule directories
const rooModes = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];
for (const mode of rooModes) {
fs.mkdirSync(path.join(tempDir, '.roo', `rules-${mode}`), {
recursive: true
});
fs.writeFileSync(
path.join(tempDir, '.roo', `rules-${mode}`, `${mode}-rules`),
`Content for ${mode} rules`
);
}
// Create additional directories
fs.mkdirSync(path.join(tempDir, '.roo', 'config'), { recursive: true });
fs.mkdirSync(path.join(tempDir, '.roo', 'templates'), { recursive: true });
fs.mkdirSync(path.join(tempDir, '.roo', 'logs'), { recursive: true });
// Copy .roomodes file
fs.writeFileSync(path.join(tempDir, '.roomodes'), 'Roomodes file content');
}
test('creates all required .roo directories', () => {
// Act
mockCreateRooStructure();
// Assert
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.roo'), {
recursive: true
});
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules'),
{ recursive: true }
);
// Verify all mode directories are created
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-architect'),
{ recursive: true }
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-ask'),
{ recursive: true }
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-boomerang'),
{ recursive: true }
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-code'),
{ recursive: true }
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-debug'),
{ recursive: true }
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-test'),
{ recursive: true }
);
});
test('creates rule files for all modes', () => {
// Act
mockCreateRooStructure();
// Assert - check all rule files are created
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-architect', 'architect-rules'),
expect.any(String)
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-ask', 'ask-rules'),
expect.any(String)
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-boomerang', 'boomerang-rules'),
expect.any(String)
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-code', 'code-rules'),
expect.any(String)
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-debug', 'debug-rules'),
expect.any(String)
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'rules-test', 'test-rules'),
expect.any(String)
);
});
test('creates .roomodes file in project root', () => {
// Act
mockCreateRooStructure();
// Assert
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(tempDir, '.roomodes'),
expect.any(String)
);
});
test('creates additional required Roo directories', () => {
// Act
mockCreateRooStructure();
// Assert
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'config'),
{ recursive: true }
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'templates'),
{ recursive: true }
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.roo', 'logs'),
{ recursive: true }
);
});
});

View File

@@ -0,0 +1,113 @@
import { expect } from 'chai';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { convertCursorRuleToRooRule } from '../modules/rule-transformer.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
describe('Rule Transformer', () => {
const testDir = path.join(__dirname, 'temp-test-dir');
before(() => {
// Create test directory
if (!fs.existsSync(testDir)) {
fs.mkdirSync(testDir, { recursive: true });
}
});
after(() => {
// Clean up test directory
if (fs.existsSync(testDir)) {
fs.rmSync(testDir, { recursive: true, force: true });
}
});
it('should correctly convert basic terms', () => {
// Create a test Cursor rule file with basic terms
const testCursorRule = path.join(testDir, 'basic-terms.mdc');
const testContent = `---
description: Test Cursor rule for basic terms
globs: **/*
alwaysApply: true
---
This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
Also has references to .mdc files.`;
fs.writeFileSync(testCursorRule, testContent);
// Convert it
const testRooRule = path.join(testDir, 'basic-terms.md');
convertCursorRuleToRooRule(testCursorRule, testRooRule);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
// Verify transformations
expect(convertedContent).to.include('Roo Code');
expect(convertedContent).to.include('roocode.com');
expect(convertedContent).to.include('.md');
expect(convertedContent).to.not.include('cursor.so');
expect(convertedContent).to.not.include('Cursor rule');
});
it('should correctly convert tool references', () => {
// Create a test Cursor rule file with tool references
const testCursorRule = path.join(testDir, 'tool-refs.mdc');
const testContent = `---
description: Test Cursor rule for tool references
globs: **/*
alwaysApply: true
---
- Use the search tool to find code
- The edit_file tool lets you modify files
- run_command executes terminal commands
- use_mcp connects to external services`;
fs.writeFileSync(testCursorRule, testContent);
// Convert it
const testRooRule = path.join(testDir, 'tool-refs.md');
convertCursorRuleToRooRule(testCursorRule, testRooRule);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
// Verify transformations
expect(convertedContent).to.include('search_files tool');
expect(convertedContent).to.include('apply_diff tool');
expect(convertedContent).to.include('execute_command');
expect(convertedContent).to.include('use_mcp_tool');
});
it('should correctly update file references', () => {
// Create a test Cursor rule file with file references
const testCursorRule = path.join(testDir, 'file-refs.mdc');
const testContent = `---
description: Test Cursor rule for file references
globs: **/*
alwaysApply: true
---
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
fs.writeFileSync(testCursorRule, testContent);
// Convert it
const testRooRule = path.join(testDir, 'file-refs.md');
convertCursorRuleToRooRule(testCursorRule, testRooRule);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
// Verify transformations
expect(convertedContent).to.include('(mdc:.roo/rules/dev_workflow.md)');
expect(convertedContent).to.include('(mdc:.roo/rules/taskmaster.md)');
expect(convertedContent).to.not.include('(mdc:.cursor/rules/');
});
});

View File

@@ -134,33 +134,59 @@ jest.mock('../../scripts/modules/task-manager.js', () => {
});
// Create a simplified version of parsePRD for testing
const testParsePRD = async (prdPath, outputPath, numTasks) => {
const testParsePRD = async (prdPath, outputPath, numTasks, options = {}) => {
const { append = false } = options;
try {
// Handle existing tasks when append flag is true
let existingTasks = { tasks: [] };
let lastTaskId = 0;
// Check if the output file already exists
if (mockExistsSync(outputPath)) {
const confirmOverwrite = await mockPromptYesNo(
`Warning: ${outputPath} already exists. Overwrite?`,
false
);
if (append) {
// Simulate reading existing tasks.json
existingTasks = {
tasks: [
{ id: 1, title: 'Existing Task 1', status: 'done' },
{ id: 2, title: 'Existing Task 2', status: 'pending' }
]
};
lastTaskId = 2; // Highest existing ID
} else {
const confirmOverwrite = await mockPromptYesNo(
`Warning: ${outputPath} already exists. Overwrite?`,
false
);
if (!confirmOverwrite) {
console.log(`Operation cancelled. ${outputPath} was not modified.`);
return null;
if (!confirmOverwrite) {
console.log(`Operation cancelled. ${outputPath} was not modified.`);
return null;
}
}
}
const prdContent = mockReadFileSync(prdPath, 'utf8');
const tasks = await mockCallClaude(prdContent, prdPath, numTasks);
// Modify mockCallClaude to accept lastTaskId parameter
let newTasks = await mockCallClaude(prdContent, prdPath, numTasks);
// Merge tasks if appending
const tasksData = append
? {
...existingTasks,
tasks: [...existingTasks.tasks, ...newTasks.tasks]
}
: newTasks;
const dir = mockDirname(outputPath);
if (!mockExistsSync(dir)) {
mockMkdirSync(dir, { recursive: true });
}
mockWriteJSON(outputPath, tasks);
mockWriteJSON(outputPath, tasksData);
await mockGenerateTaskFiles(outputPath, dir);
return tasks;
return tasksData;
} catch (error) {
console.error(`Error parsing PRD: ${error.message}`);
process.exit(1);
@@ -628,6 +654,27 @@ describe('Task Manager Module', () => {
// Mock the sample PRD content
const samplePRDContent = '# Sample PRD for Testing';
// Mock existing tasks for append test
const existingTasks = {
tasks: [
{ id: 1, title: 'Existing Task 1', status: 'done' },
{ id: 2, title: 'Existing Task 2', status: 'pending' }
]
};
// Mock new tasks with continuing IDs for append test
const newTasksWithContinuedIds = {
tasks: [
{ id: 3, title: 'New Task 3' },
{ id: 4, title: 'New Task 4' }
]
};
// Mock merged tasks for append test
const mergedTasks = {
tasks: [...existingTasks.tasks, ...newTasksWithContinuedIds.tasks]
};
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
@@ -811,6 +858,66 @@ describe('Task Manager Module', () => {
sampleClaudeResponse
);
});
test('should append new tasks when append option is true', async () => {
// Setup mocks to simulate tasks.json already exists
mockExistsSync.mockImplementation((path) => {
if (path === 'tasks/tasks.json') return true; // Output file exists
if (path === 'tasks') return true; // Directory exists
return false;
});
// Mock for reading existing tasks
mockReadJSON.mockReturnValue(existingTasks);
// mockReadJSON = jest.fn().mockReturnValue(existingTasks);
// Mock callClaude to return new tasks with continuing IDs
mockCallClaude.mockResolvedValueOnce(newTasksWithContinuedIds);
// Call the function with append option
const result = await testParsePRD(
'path/to/prd.txt',
'tasks/tasks.json',
2,
{ append: true }
);
// Verify prompt was NOT called (no confirmation needed for append)
expect(mockPromptYesNo).not.toHaveBeenCalled();
// Verify the file was written with merged tasks
expect(mockWriteJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1 }),
expect.objectContaining({ id: 2 }),
expect.objectContaining({ id: 3 }),
expect.objectContaining({ id: 4 })
])
})
);
// Verify the result contains merged tasks
expect(result.tasks.length).toBe(4);
});
test('should skip prompt and not overwrite when append is true', async () => {
// Setup mocks to simulate tasks.json already exists
mockExistsSync.mockImplementation((path) => {
if (path === 'tasks/tasks.json') return true; // Output file exists
if (path === 'tasks') return true; // Directory exists
return false;
});
// Call the function with append option
await testParsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
append: true
});
// Verify prompt was NOT called with append flag
expect(mockPromptYesNo).not.toHaveBeenCalled();
});
});
describe.skip('updateTasks function', () => {