chore: fix use-tag e2e test

This commit is contained in:
Ralph Khreish
2025-07-12 00:38:49 +03:00
parent e580e5d7c0
commit 3204582885
7 changed files with 187 additions and 173 deletions

View File

@@ -20,6 +20,8 @@ export default {
testTimeout: 180000, // 3 minutes default (AI operations can be slow) testTimeout: 180000, // 3 minutes default (AI operations can be slow)
maxWorkers: 1, // Run E2E tests sequentially to avoid conflicts maxWorkers: 1, // Run E2E tests sequentially to avoid conflicts
verbose: true, verbose: true,
// Suppress console output for cleaner test results
silent: false,
setupFilesAfterEnv: ['<rootDir>/tests/e2e/setup/jest-setup.js'], setupFilesAfterEnv: ['<rootDir>/tests/e2e/setup/jest-setup.js'],
globalSetup: '<rootDir>/tests/e2e/setup/global-setup.js', globalSetup: '<rootDir>/tests/e2e/setup/global-setup.js',
globalTeardown: '<rootDir>/tests/e2e/setup/global-teardown.js', globalTeardown: '<rootDir>/tests/e2e/setup/global-teardown.js',
@@ -30,38 +32,40 @@ export default {
], ],
coverageDirectory: '<rootDir>/coverage-e2e', coverageDirectory: '<rootDir>/coverage-e2e',
// Custom reporters for better E2E test output // Custom reporters for better E2E test output
// Transform configuration to match unit tests
transform: {},
transformIgnorePatterns: ['/node_modules/'],
// Module configuration
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1'
},
moduleDirectories: ['node_modules', '<rootDir>'],
// Reporters configuration
reporters: [ reporters: [
'default', 'default',
[ [
'jest-stare', 'jest-html-reporters',
{ {
resultDir: 'test-results', publicPath: './test-results',
reportTitle: 'Task Master E2E Test Report', filename: 'index.html',
additionalResultsProcessors: ['jest-junit'], pageTitle: 'Task Master E2E Test Report',
coverageLink: '../coverage-e2e/lcov-report/index.html', expand: true,
jestStareConfigJson: 'jest-stare.config.json', openReport: false,
jestGlobalConfigJson: 'jest.e2e.config.json', hideIcon: false,
report: true, includeFailureMsg: true,
reportSummary: true, enableMergeData: true,
reportHeadline: 'Task Master E2E Test Results', dataMergeLevel: 1,
reportDescriptionHeadline: 'Comprehensive E2E test suite for all CLI commands', inlineSource: false,
disableCharts: false, customInfos: [
resultJson: 'jest-results.json', {
resultHtml: 'index.html' title: 'Environment',
} value: 'E2E Testing'
], },
[ {
'jest-junit', title: 'Test Type',
{ value: 'CLI Commands'
outputDirectory: '<rootDir>/test-results', }
outputName: 'e2e-junit.xml', ]
classNameTemplate: '{classname} - {title}',
titleTemplate: '{classname} - {title}',
ancestorSeparator: ' ',
suiteNameTemplate: '{filepath}',
includeConsoleOutput: true,
includeShortConsoleOutput: true,
reportTestSuiteErrors: true
} }
] ]
], ],

47
package-lock.json generated
View File

@@ -70,7 +70,6 @@
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-node": "^29.7.0", "jest-environment-node": "^29.7.0",
"jest-html-reporters": "^3.1.7", "jest-html-reporters": "^3.1.7",
"jest-junit": "^16.0.0",
"mcp-jest": "^1.0.10", "mcp-jest": "^1.0.10",
"mock-fs": "^5.5.0", "mock-fs": "^5.5.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
@@ -9806,32 +9805,6 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/jest-junit": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz",
"integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"mkdirp": "^1.0.4",
"strip-ansi": "^6.0.1",
"uuid": "^8.3.2",
"xml": "^1.0.1"
},
"engines": {
"node": ">=10.12.0"
}
},
"node_modules/jest-junit/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/jest-leak-detector": { "node_modules/jest-leak-detector": {
"version": "29.7.0", "version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
@@ -11131,19 +11104,6 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/mock-fs": { "node_modules/mock-fs": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.5.0.tgz",
@@ -13791,13 +13751,6 @@
} }
} }
}, },
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"dev": true,
"license": "MIT"
},
"node_modules/xsschema": { "node_modules/xsschema": {
"version": "0.3.0-beta.8", "version": "0.3.0-beta.8",
"resolved": "https://registry.npmjs.org/xsschema/-/xsschema-0.3.0-beta.8.tgz", "resolved": "https://registry.npmjs.org/xsschema/-/xsschema-0.3.0-beta.8.tgz",

View File

@@ -133,7 +133,6 @@
"ink": "^5.0.1", "ink": "^5.0.1",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-node": "^29.7.0", "jest-environment-node": "^29.7.0",
"jest-junit": "^16.0.0",
"jest-html-reporters": "^3.1.7", "jest-html-reporters": "^3.1.7",
"mcp-jest": "^1.0.10", "mcp-jest": "^1.0.10",
"mock-fs": "^5.5.0", "mock-fs": "^5.5.0",

View File

@@ -3,17 +3,27 @@
* Runs once before all test suites * Runs once before all test suites
*/ */
const { execSync } = require('child_process'); import { execSync } from 'child_process';
const { existsSync } = require('fs'); import { existsSync } from 'fs';
const { join } = require('path'); import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
module.exports = async () => { const __filename = fileURLToPath(import.meta.url);
console.log('\n🚀 Setting up E2E test environment...\n'); const __dirname = dirname(__filename);
export default async () => {
// Silent mode for cleaner output
if (!process.env.JEST_SILENT_REPORTER) {
console.log('\n🚀 Setting up E2E test environment...\n');
}
try { try {
// Ensure task-master is linked globally // Ensure task-master is linked globally
const projectRoot = join(__dirname, '../../..'); const projectRoot = join(__dirname, '../../..');
console.log('📦 Linking task-master globally...'); if (!process.env.JEST_SILENT_REPORTER) {
console.log('📦 Linking task-master globally...');
}
execSync('npm link', { execSync('npm link', {
cwd: projectRoot, cwd: projectRoot,
stdio: 'inherit' stdio: 'inherit'
@@ -26,13 +36,17 @@ module.exports = async () => {
'⚠️ Warning: .env file not found. Some tests may fail without API keys.' '⚠️ Warning: .env file not found. Some tests may fail without API keys.'
); );
} else { } else {
console.log('✅ .env file found'); if (!process.env.JEST_SILENT_REPORTER) {
console.log('✅ .env file found');
}
} }
// Verify task-master command is available // Verify task-master command is available
try { try {
execSync('task-master --version', { stdio: 'pipe' }); execSync('task-master --version', { stdio: 'pipe' });
console.log('✅ task-master command is available\n'); if (!process.env.JEST_SILENT_REPORTER) {
console.log('✅ task-master command is available\n');
}
} catch (error) { } catch (error) {
throw new Error( throw new Error(
'task-master command not found. Please ensure npm link succeeded.' 'task-master command not found. Please ensure npm link succeeded.'

View File

@@ -3,8 +3,11 @@
* Runs once after all test suites * Runs once after all test suites
*/ */
module.exports = async () => { export default async () => {
console.log('\n🧹 Cleaning up E2E test environment...\n'); // Silent mode for cleaner output
if (!process.env.JEST_SILENT_REPORTER) {
console.log('\n🧹 Cleaning up E2E test environment...\n');
}
// Any global cleanup needed // Any global cleanup needed
// Note: Individual test directories are cleaned up in afterEach hooks // Note: Individual test directories are cleaned up in afterEach hooks

View File

@@ -1,131 +1,169 @@
const path = require('path'); import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
const fs = require('fs'); import { mkdtempSync, existsSync, readFileSync, rmSync, writeFileSync } from 'fs';
const { import { join } from 'path';
setupTestEnvironment, import { tmpdir } from 'os';
cleanupTestEnvironment,
runCommand
} = require('../../helpers/testHelpers');
describe('use-tag command', () => { describe('use-tag command', () => {
let testDir; let testDir;
let tasksPath; let helpers;
beforeEach(async () => { beforeEach(async () => {
const setup = await setupTestEnvironment(); // Create test directory
testDir = setup.testDir; testDir = mkdtempSync(join(tmpdir(), 'task-master-use-tag-'));
tasksPath = setup.tasksPath;
// Initialize test helpers
const context = global.createTestContext('use-tag');
helpers = context.helpers;
// Copy .env file if it exists
const mainEnvPath = join(process.cwd(), '.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);
// Create tasks file path
const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
// Create a test project with multiple tags // Create a test project with multiple tags
const tasksData = { const tasksData = {
tasks: [ master: {
{ tasks: [
id: 1, {
description: 'Task in master', id: 1,
status: 'pending', description: 'Task in master',
tags: ['master'] status: 'pending',
}, tags: ['master']
{ },
id: 2, {
description: 'Task in feature', id: 3,
status: 'pending', description: 'Task in both',
tags: ['feature'] status: 'pending',
}, tags: ['master', 'feature']
{ }
id: 3, ]
description: 'Task in both', },
status: 'pending', feature: {
tags: ['master', 'feature'] tasks: [
} {
], id: 2,
tags: { description: 'Task in feature',
master: { status: 'pending',
name: 'master', tags: ['feature']
description: 'Main development branch' },
}, {
feature: { id: 3,
name: 'feature', description: 'Task in both',
description: 'Feature branch' status: 'pending',
}, tags: ['master', 'feature']
release: { }
name: 'release', ]
description: 'Release branch' },
} release: {
tasks: []
}, },
activeTag: 'master',
metadata: { metadata: {
nextId: 4 nextId: 4,
activeTag: 'master'
} }
}; };
fs.writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2)); writeFileSync(tasksPath, JSON.stringify(tasksData, null, 2));
}); });
afterEach(async () => { afterEach(async () => {
await cleanupTestEnvironment(testDir); // Clean up test directory
if (testDir && existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
}); });
test('should switch to an existing tag', async () => { it('should switch to an existing tag', async () => {
const result = await runCommand(['use-tag', 'feature'], testDir); const result = await helpers.taskMaster('use-tag', ['feature'], {
cwd: testDir
});
expect(result.code).toBe(0); expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Successfully switched to tag: feature'); expect(result.stdout).toContain('Successfully switched to tag "feature"');
// Verify the active tag was updated // Verify the active tag was updated in state.json
const updatedData = JSON.parse(fs.readFileSync(tasksPath, 'utf8')); const statePath = join(testDir, '.taskmaster/state.json');
expect(updatedData.activeTag).toBe('feature'); const stateData = JSON.parse(readFileSync(statePath, 'utf8'));
expect(stateData.currentTag).toBe('feature');
}); });
test('should show error when switching to non-existent tag', async () => { it('should show error when switching to non-existent tag', async () => {
const result = await runCommand(['use-tag', 'nonexistent'], testDir); const result = await helpers.taskMaster('use-tag', ['nonexistent'], {
cwd: testDir
});
expect(result.code).toBe(1); expect(result).toHaveExitCode(1);
expect(result.stderr).toContain('Tag "nonexistent" does not exist'); expect(result.stderr).toContain('Tag "nonexistent" does not exist');
}); });
test('should switch from feature tag back to master', async () => { it('should switch from feature tag back to master', async () => {
// First switch to feature // First switch to feature
await runCommand(['use-tag', 'feature'], testDir); await helpers.taskMaster('use-tag', ['feature'], { cwd: testDir });
// Then switch back to master // Then switch back to master
const result = await runCommand(['use-tag', 'master'], testDir); const result = await helpers.taskMaster('use-tag', ['master'], {
cwd: testDir
});
expect(result.code).toBe(0); expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Successfully switched to tag: master'); expect(result.stdout).toContain('Successfully switched to tag "master"');
const updatedData = JSON.parse(fs.readFileSync(tasksPath, 'utf8')); // Verify the active tag was updated in state.json
expect(updatedData.activeTag).toBe('master'); const statePath = join(testDir, '.taskmaster/state.json');
const stateData = JSON.parse(readFileSync(statePath, 'utf8'));
expect(stateData.currentTag).toBe('master');
}); });
test('should handle switching to the same tag gracefully', async () => { it('should handle switching to the same tag gracefully', async () => {
const result = await runCommand(['use-tag', 'master'], testDir); const result = await helpers.taskMaster('use-tag', ['master'], {
cwd: testDir
});
expect(result.code).toBe(0); expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Already on tag: master'); expect(result.stdout).toContain('Successfully switched to tag "master"');
}); });
test('should work with custom tasks file path', async () => { it('should work with custom tasks file path', async () => {
const customTasksPath = path.join(testDir, 'custom-tasks.json'); const tasksPath = join(testDir, '.taskmaster/tasks/tasks.json');
fs.copyFileSync(tasksPath, customTasksPath); const customTasksPath = join(testDir, 'custom-tasks.json');
const content = readFileSync(tasksPath, 'utf8');
writeFileSync(customTasksPath, content);
const result = await runCommand( const result = await helpers.taskMaster(
['use-tag', 'feature', '-f', customTasksPath], 'use-tag',
testDir ['feature', '-f', customTasksPath],
{ cwd: testDir }
); );
expect(result.code).toBe(0); expect(result).toHaveExitCode(0);
expect(result.stdout).toContain('Successfully switched to tag: feature'); expect(result.stdout).toContain('Successfully switched to tag "feature"');
const updatedData = JSON.parse(fs.readFileSync(customTasksPath, 'utf8')); // Verify the active tag was updated in state.json
expect(updatedData.activeTag).toBe('feature'); const statePath = join(testDir, '.taskmaster/state.json');
const stateData = JSON.parse(readFileSync(statePath, 'utf8'));
expect(stateData.currentTag).toBe('feature');
}); });
test('should fail when tasks file does not exist', async () => { it('should fail when tasks file does not exist', async () => {
const nonExistentPath = path.join(testDir, 'nonexistent.json'); const nonExistentPath = join(testDir, 'nonexistent.json');
const result = await runCommand( const result = await helpers.taskMaster(
['use-tag', 'feature', '-f', nonExistentPath], 'use-tag',
testDir ['feature', '-f', nonExistentPath],
{ cwd: testDir }
); );
expect(result.code).toBe(1); expect(result).toHaveExitCode(1);
expect(result.stderr).toContain('Tasks file not found'); expect(result.stderr).toContain('does not exist');
}); });
}); });

View File

@@ -58,7 +58,10 @@ export class TestLogger {
break; break;
} }
console.log(coloredMessage); // Only output to console if debugging or it's an error
if ((process.env.DEBUG_TESTS || level === 'ERROR') && !process.env.JEST_SILENT_MODE) {
console.log(coloredMessage);
}
// Write to file if immediate flush requested // Write to file if immediate flush requested
if (options.flush) { if (options.flush) {