Files
claude-task-master/tests/e2e/tests/commands/sync-readme.test.js
2025-07-19 00:18:37 +03:00

739 lines
21 KiB
JavaScript

/**
* Comprehensive E2E tests for sync-readme command
* Tests README.md synchronization with task list
*/
const {
mkdtempSync,
existsSync,
readFileSync,
rmSync,
writeFileSync,
mkdirSync
} = require('fs');
const { join } = require('path');
const { tmpdir } = require('os');
const path = require('path');
describe('sync-readme command', () => {
let testDir;
let helpers;
beforeEach(async () => {
// Create test directory
testDir = mkdtempSync(join(tmpdir(), 'task-master-sync-readme-'));
// Initialize test helpers
const context = global.createTestContext('sync-readme');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(__dirname, '../../../../.env');
const testEnvPath = join(testDir, '.env');
if (existsSync(mainEnvPath)) {
const envContent = readFileSync(mainEnvPath, 'utf8');
writeFileSync(testEnvPath, envContent);
}
// Initialize task-master project
const initResult = await helpers.taskMaster('init', ['-y'], {
cwd: testDir
});
expect(initResult).toHaveExitCode(0);
// Ensure tasks.json exists (bug workaround)
const tasksJsonPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (!existsSync(tasksJsonPath)) {
mkdirSync(join(testDir, '.taskmaster/tasks'), { recursive: true });
writeFileSync(tasksJsonPath, JSON.stringify({ master: { tasks: [] } }));
}
});
afterEach(() => {
// Clean up test directory
if (testDir && existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
describe('Creating README.md', () => {
it('should create README.md when it does not exist', async () => {
// Add a test task
await helpers.taskMaster(
'add-task',
['--title', 'Test task', '--description', 'Task for README sync'],
{ cwd: testDir }
);
// Run sync-readme
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Successfully synced tasks to README.md');
// Verify README.md was created
const readmePath = join(testDir, 'README.md');
expect(existsSync(readmePath)).toBe(true);
// Verify content
const readmeContent = readFileSync(readmePath, 'utf8');
expect(readmeContent).toContain('Test task');
expect(readmeContent).toContain('<!-- TASKMASTER_EXPORT_START -->');
expect(readmeContent).toContain('<!-- TASKMASTER_EXPORT_END -->');
expect(readmeContent).toContain('Taskmaster Export');
expect(readmeContent).toContain('Powered by [Task Master]');
});
it('should create basic README structure with project name', async () => {
// Run sync-readme without any tasks
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain project name from directory
const projectName = path.basename(testDir);
expect(readmeContent).toContain(`# ${projectName}`);
expect(readmeContent).toContain('This project is managed using Task Master');
});
});
describe('Updating existing README.md', () => {
beforeEach(() => {
// Create an existing README with custom content
const readmePath = join(testDir, 'README.md');
writeFileSync(
readmePath,
`# My Project
This is my awesome project.
## Features
- Feature 1
- Feature 2
## Installation
Run npm install
`
);
});
it('should preserve existing README content', async () => {
// Add a task
await helpers.taskMaster(
'add-task',
['--title', 'New feature', '--description', 'Implement feature 3'],
{ cwd: testDir }
);
// Run sync-readme
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Original content should be preserved
expect(readmeContent).toContain('# My Project');
expect(readmeContent).toContain('This is my awesome project');
expect(readmeContent).toContain('## Features');
expect(readmeContent).toContain('- Feature 1');
expect(readmeContent).toContain('## Installation');
// Task list should be appended
expect(readmeContent).toContain('New feature');
expect(readmeContent).toContain('<!-- TASKMASTER_EXPORT_START -->');
expect(readmeContent).toContain('<!-- TASKMASTER_EXPORT_END -->');
});
it('should replace existing task section between markers', async () => {
// Add initial task section to README
const readmePath = join(testDir, 'README.md');
let content = readFileSync(readmePath, 'utf8');
content += `
<!-- TASKMASTER_EXPORT_START -->
Old task content that should be replaced
<!-- TASKMASTER_EXPORT_END -->
`;
writeFileSync(readmePath, content);
// Add new tasks
await helpers.taskMaster(
'add-task',
['--title', 'Task 1', '--description', 'First task'],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-task',
['--title', 'Task 2', '--description', 'Second task'],
{ cwd: testDir }
);
// Run sync-readme
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const updatedContent = readFileSync(readmePath, 'utf8');
// Old content should be replaced
expect(updatedContent).not.toContain('Old task content that should be replaced');
// New tasks should be present
expect(updatedContent).toContain('Task 1');
expect(updatedContent).toContain('Task 2');
// Original content before markers should be preserved
expect(updatedContent).toContain('# My Project');
expect(updatedContent).toContain('This is my awesome project');
});
});
describe('Task list formatting', () => {
beforeEach(async () => {
// Create tasks with different properties
const task1 = await helpers.taskMaster(
'add-task',
[
'--title',
'High priority task',
'--description',
'Urgent task',
'--priority',
'high'
],
{ cwd: testDir }
);
const taskId1 = helpers.extractTaskId(task1.stdout);
const task2 = await helpers.taskMaster(
'add-task',
[
'--title',
'In progress task',
'--description',
'Working on it',
'--priority',
'medium'
],
{ cwd: testDir }
);
const taskId2 = helpers.extractTaskId(task2.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId2, '--status', 'in-progress'],
{ cwd: testDir }
);
const task3 = await helpers.taskMaster(
'add-task',
['--title', 'Completed task', '--description', 'All done'],
{ cwd: testDir }
);
const taskId3 = helpers.extractTaskId(task3.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId3, '--status', 'done'],
{ cwd: testDir }
);
});
it('should format tasks in markdown table', async () => {
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain markdown table headers
expect(readmeContent).toContain('| ID |');
expect(readmeContent).toContain('| Title |');
expect(readmeContent).toContain('| Status |');
expect(readmeContent).toContain('| Priority |');
// Should contain task data
expect(readmeContent).toContain('High priority task');
expect(readmeContent).toContain('high');
expect(readmeContent).toContain('In progress task');
expect(readmeContent).toContain('in-progress');
expect(readmeContent).toContain('Completed task');
expect(readmeContent).toContain('done');
});
it('should include metadata in export header', async () => {
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain export metadata
expect(readmeContent).toContain('Taskmaster Export');
expect(readmeContent).toContain('without subtasks');
expect(readmeContent).toContain('Status filter: none');
expect(readmeContent).toContain('Powered by [Task Master](https://task-master.dev');
// Should contain timestamp
expect(readmeContent).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/);
});
});
describe('Subtasks support', () => {
let parentTaskId;
beforeEach(async () => {
// Create parent task
const parentResult = await helpers.taskMaster(
'add-task',
['--title', 'Main task', '--description', 'Has subtasks'],
{ cwd: testDir }
);
parentTaskId = helpers.extractTaskId(parentResult.stdout);
// Add subtasks
await helpers.taskMaster(
'add-subtask',
[
'--parent',
parentTaskId,
'--title',
'Subtask 1',
'--description',
'First subtask'
],
{ cwd: testDir }
);
await helpers.taskMaster(
'add-subtask',
[
'--parent',
parentTaskId,
'--title',
'Subtask 2',
'--description',
'Second subtask'
],
{ cwd: testDir }
);
});
it('should not include subtasks by default', async () => {
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain parent task
expect(readmeContent).toContain('Main task');
// Should not contain subtasks
expect(readmeContent).not.toContain('Subtask 1');
expect(readmeContent).not.toContain('Subtask 2');
// Metadata should indicate no subtasks
expect(readmeContent).toContain('without subtasks');
});
it('should include subtasks with --with-subtasks flag', async () => {
const result = await helpers.taskMaster('sync-readme', ['--with-subtasks'], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain parent and subtasks
expect(readmeContent).toContain('Main task');
expect(readmeContent).toContain('Subtask 1');
expect(readmeContent).toContain('Subtask 2');
// Should show subtask IDs
expect(readmeContent).toContain(`${parentTaskId}.1`);
expect(readmeContent).toContain(`${parentTaskId}.2`);
// Metadata should indicate subtasks included
expect(readmeContent).toContain('with subtasks');
});
});
describe('Status filtering', () => {
beforeEach(async () => {
// Create tasks with different statuses
await helpers.taskMaster(
'add-task',
['--title', 'Pending task', '--description', 'Not started'],
{ cwd: testDir }
);
const task2 = await helpers.taskMaster(
'add-task',
['--title', 'Active task', '--description', 'In progress'],
{ cwd: testDir }
);
const taskId2 = helpers.extractTaskId(task2.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId2, '--status', 'in-progress'],
{ cwd: testDir }
);
const task3 = await helpers.taskMaster(
'add-task',
['--title', 'Done task', '--description', 'Completed'],
{ cwd: testDir }
);
const taskId3 = helpers.extractTaskId(task3.stdout);
await helpers.taskMaster(
'set-status',
['--id', taskId3, '--status', 'done'],
{ cwd: testDir }
);
});
it('should filter by pending status', async () => {
const result = await helpers.taskMaster(
'sync-readme',
['--status', 'pending'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('status: pending');
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should only contain pending task
expect(readmeContent).toContain('Pending task');
expect(readmeContent).not.toContain('Active task');
expect(readmeContent).not.toContain('Done task');
// Metadata should indicate status filter
expect(readmeContent).toContain('Status filter: pending');
});
it('should filter by done status', async () => {
const result = await helpers.taskMaster(
'sync-readme',
['--status', 'done'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should only contain done task
expect(readmeContent).toContain('Done task');
expect(readmeContent).not.toContain('Pending task');
expect(readmeContent).not.toContain('Active task');
// Metadata should indicate status filter
expect(readmeContent).toContain('Status filter: done');
});
});
describe('Tag support', () => {
beforeEach(async () => {
// Create tasks in master tag
await helpers.taskMaster(
'add-task',
['--title', 'Master task', '--description', 'In master tag'],
{ cwd: testDir }
);
// Create new tag and add tasks
await helpers.taskMaster(
'add-tag',
['feature-branch', '--description', 'Feature work'],
{ cwd: testDir }
);
await helpers.taskMaster('use-tag', ['feature-branch'], { cwd: testDir });
await helpers.taskMaster(
'add-task',
[
'--title',
'Feature task',
'--description',
'In feature tag',
'--tag',
'feature-branch'
],
{ cwd: testDir }
);
});
it('should sync tasks from current active tag', async () => {
// Ensure we're on feature-branch tag
await helpers.taskMaster('use-tag', ['feature-branch'], { cwd: testDir });
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain feature task from active tag
expect(readmeContent).toContain('Feature task');
expect(readmeContent).not.toContain('Master task');
});
it('should sync master tag tasks when on master', async () => {
// Switch back to master tag
await helpers.taskMaster('use-tag', ['master'], { cwd: testDir });
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain master task
expect(readmeContent).toContain('Master task');
expect(readmeContent).not.toContain('Feature task');
});
});
describe('Error handling', () => {
it('should handle missing tasks file gracefully', async () => {
// Remove tasks file
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
if (existsSync(tasksPath)) {
rmSync(tasksPath);
}
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Failed to sync tasks to README');
});
it('should handle invalid tasks file', async () => {
// Create invalid tasks file
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
writeFileSync(tasksPath, '{ invalid json }');
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir,
allowFailure: true
});
expect(result.exitCode).not.toBe(0);
});
it('should handle read-only README file', async () => {
// Skip this test on Windows as chmod doesn't work the same way
if (process.platform === 'win32') {
return;
}
// Create read-only README
const readmePath = join(testDir, 'README.md');
writeFileSync(readmePath, '# Read Only');
// Make file read-only
const fs = require('fs');
fs.chmodSync(readmePath, 0o444);
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir,
allowFailure: true
});
// Restore write permissions for cleanup
fs.chmodSync(readmePath, 0o644);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Failed to sync tasks to README');
});
});
describe('File path handling', () => {
it('should use custom tasks file path', async () => {
// Create custom tasks file
const customPath = join(testDir, 'custom-tasks.json');
writeFileSync(
customPath,
JSON.stringify({
master: {
tasks: [
{
id: 1,
title: 'Custom file task',
description: 'From custom file',
status: 'pending',
priority: 'medium',
dependencies: []
}
]
}
})
);
const result = await helpers.taskMaster(
'sync-readme',
['--file', customPath],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
expect(readmeContent).toContain('Custom file task');
expect(readmeContent).toContain('From custom file');
});
});
describe('Multiple sync operations', () => {
it('should handle multiple sync operations correctly', async () => {
// First sync
await helpers.taskMaster(
'add-task',
['--title', 'Initial task', '--description', 'First sync'],
{ cwd: testDir }
);
await helpers.taskMaster('sync-readme', [], { cwd: testDir });
// Add more tasks
await helpers.taskMaster(
'add-task',
['--title', 'Second task', '--description', 'Second sync'],
{ cwd: testDir }
);
// Second sync
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain both tasks
expect(readmeContent).toContain('Initial task');
expect(readmeContent).toContain('Second task');
// Should only have one set of markers
const startMatches = (readmeContent.match(/<!-- TASKMASTER_EXPORT_START -->/g) || []).length;
const endMatches = (readmeContent.match(/<!-- TASKMASTER_EXPORT_END -->/g) || []).length;
expect(startMatches).toBe(1);
expect(endMatches).toBe(1);
});
});
describe('UTM tracking', () => {
it('should include proper UTM parameters in Task Master link', async () => {
await helpers.taskMaster(
'add-task',
['--title', 'Test task', '--description', 'For UTM test'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
const readmePath = join(testDir, 'README.md');
const readmeContent = readFileSync(readmePath, 'utf8');
// Should contain Task Master link with UTM parameters
expect(readmeContent).toContain('https://task-master.dev?');
expect(readmeContent).toContain('utm_source=github-readme');
expect(readmeContent).toContain('utm_medium=readme-export');
expect(readmeContent).toContain('utm_campaign=');
expect(readmeContent).toContain('utm_content=task-export-link');
// UTM campaign should be based on folder name
const folderName = path.basename(testDir);
const cleanFolderName = folderName
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
expect(readmeContent).toContain(`utm_campaign=${cleanFolderName}`);
});
});
describe('Output formatting', () => {
it('should show export details in console output', async () => {
await helpers.taskMaster(
'add-task',
['--title', 'Test task', '--description', 'For output test'],
{ cwd: testDir }
);
const result = await helpers.taskMaster(
'sync-readme',
['--with-subtasks', '--status', 'pending'],
{ cwd: testDir }
);
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Syncing tasks to README.md');
expect(result.stdout).toContain('(with subtasks)');
expect(result.stdout).toContain('(status: pending)');
expect(result.stdout).toContain('Successfully synced tasks to README.md');
expect(result.stdout).toContain('Export details: with subtasks, status: pending');
expect(result.stdout).toContain('Location:');
expect(result.stdout).toContain('README.md');
});
it('should show proper output without filters', async () => {
await helpers.taskMaster(
'add-task',
['--title', 'Test task', '--description', 'No filters'],
{ cwd: testDir }
);
const result = await helpers.taskMaster('sync-readme', [], {
cwd: testDir
});
expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Syncing tasks to README.md');
expect(result.stdout).not.toContain('(with subtasks)');
expect(result.stdout).not.toContain('(status:');
expect(result.stdout).toContain('Export details: without subtasks');
});
});
});