fix: use tag-specific complexity reports (#857)

* fix(expand-task): Use tag-specific complexity reports

- Add getTagAwareFilePath utility function to resolve tag-specific file paths
- Update expandTask to use tag-aware complexity report paths
- Fix issue where expand-task always used default complexity report
- Add comprehensive tests for getTagAwareFilePath utility
- Ensure proper handling of file extensions and directory structures

Fixes #850: Expanding tasks not using tag-specific complexity reports

The expandTask function now correctly uses complexity reports specific
to the current tag context (e.g., task-complexity-report_feature-branch.json)
instead of always using the default task-complexity-report.json file.

This enables proper task expansion behavior when working with multiple
tag contexts, ensuring complexity analysis is tag-specific and accurate.

* chore: Add changeset for tag-specific complexity reports fix

* test(expand-task): Add tests for tag-specific complexity report integration

- Introduced a new test suite for verifying the integration of tag-specific complexity reports in the expandTask function.
- Added a test case to ensure the correct complexity report is used when available for a specific tag.
- Mocked file system interactions to simulate the presence of tag-specific complexity reports.

This enhances the test coverage for task expansion behavior, ensuring it accurately reflects the complexity analysis based on the current tag context.

* refactor(task-manager): unify and simplify tag-aware file path logic and tests

- Reformatted imports and cleaned up comments in test files for readability
- Centralized mocks: moved getTagAwareFilePath & slugifyTagForFilePath
  mocks to setup.js for consistency and maintainability
- Simplified utils/getTagAwareFilePath: replaced manual parsing with
  path.parse() & path.format(); improved extension handling
- Enhanced test mocks for path.parse, path.format & reset path.join
  in beforeEach to avoid interference
- All tests now pass consistently; no change in functionality
This commit is contained in:
Parthy
2025-07-02 12:52:45 +02:00
committed by GitHub
parent f38abd6843
commit 598e687067
8 changed files with 334 additions and 5 deletions

View File

@@ -2,6 +2,10 @@
* Tests for the analyze-task-complexity.js module
*/
import { jest } from '@jest/globals';
import {
createGetTagAwareFilePathMock,
createSlugifyTagForFilePathMock
} from './setup.js';
// Mock the dependencies before importing the module under test
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
@@ -32,6 +36,8 @@ jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
ensureTagMetadata: jest.fn((tagObj) => tagObj),
getCurrentTag: jest.fn(() => 'master'),
flattenTasksWithSubtasks: jest.fn((tasks) => tasks),
getTagAwareFilePath: createGetTagAwareFilePathMock(),
slugifyTagForFilePath: createSlugifyTagForFilePathMock(),
markMigrationForNotice: jest.fn(),
performCompleteTagMigration: jest.fn(),
setTasksForTag: jest.fn(),

View File

@@ -3,6 +3,10 @@
*/
import { jest } from '@jest/globals';
import fs from 'fs';
import {
createGetTagAwareFilePathMock,
createSlugifyTagForFilePathMock
} from './setup.js';
// Mock the dependencies before importing the module under test
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
@@ -36,6 +40,8 @@ jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
}
return allTasks;
}),
getTagAwareFilePath: createGetTagAwareFilePathMock(),
slugifyTagForFilePath: createSlugifyTagForFilePathMock(),
readComplexityReport: jest.fn(),
markMigrationForNotice: jest.fn(),
performCompleteTagMigration: jest.fn(),
@@ -649,6 +655,61 @@ describe('expandTask', () => {
});
});
describe('Complexity Report Integration (Tag-Specific)', () => {
test('should use tag-specific complexity report when available', async () => {
// Arrange
const tasksPath = 'tasks/tasks.json';
const taskId = '1'; // Task in feature-branch
const context = {
mcpLog: createMcpLogMock(),
projectRoot: '/mock/project/root',
tag: 'feature-branch'
};
// Stub fs.existsSync to simulate complexity report exists for this tag
const existsSpy = jest
.spyOn(fs, 'existsSync')
.mockImplementation((filepath) =>
filepath.endsWith('task-complexity-report_feature-branch.json')
);
// Stub readJSON to return complexity report when reading the report path
readJSON.mockImplementation((filepath, projectRootParam, tagParam) => {
if (filepath.includes('task-complexity-report_feature-branch.json')) {
return {
complexityAnalysis: [
{
taskId: 1,
complexityScore: 8,
recommendedSubtasks: 5,
reasoning: 'Needs five detailed steps',
expansionPrompt: 'Please break this task into 5 parts'
}
]
};
}
// Default tasks data for tasks.json
const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
const selectedTag = tagParam || 'master';
return {
...sampleTasksCopy[selectedTag],
tag: selectedTag,
_rawTaggedData: sampleTasksCopy
};
});
// Act
await expandTask(tasksPath, taskId, undefined, false, '', context, false);
// Assert - generateTextService called with systemPrompt for 5 subtasks
const callArg = generateTextService.mock.calls[0][0];
expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks');
// Clean up stub
existsSpy.mockRestore();
});
});
describe('Error Handling', () => {
test('should handle non-existent task ID', async () => {
// Arrange

View File

@@ -119,3 +119,45 @@ export const setupCommonMocks = () => {
// Helper to create a deep copy of objects to avoid test pollution
export const cloneData = (data) => JSON.parse(JSON.stringify(data));
/**
* Shared mock implementation for getTagAwareFilePath that matches the actual implementation
* This ensures consistent behavior across all test files, particularly regarding projectRoot handling.
*
* The key difference from previous inconsistent implementations was that some tests were not
* properly handling the projectRoot parameter, leading to different behaviors between test files.
*
* @param {string} basePath - The base file path
* @param {string|null} tag - The tag name (null, undefined, or 'master' uses base path)
* @param {string} [projectRoot='.'] - The project root directory
* @returns {string} The resolved file path
*/
export const createGetTagAwareFilePathMock = () => {
return jest.fn((basePath, tag, projectRoot = '.') => {
// Handle projectRoot consistently - this was the key fix
const fullPath = projectRoot ? `${projectRoot}/${basePath}` : basePath;
if (!tag || tag === 'master') {
return fullPath;
}
// Mock the slugification behavior (matches actual implementation)
const slugifiedTag = tag.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
const idx = fullPath.lastIndexOf('.');
return `${fullPath.slice(0, idx)}_${slugifiedTag}${fullPath.slice(idx)}`;
});
};
/**
* Shared mock implementation for slugifyTagForFilePath that matches the actual implementation
* @param {string} tagName - The tag name to slugify
* @returns {string} Slugified tag name safe for filesystem use
*/
export const createSlugifyTagForFilePathMock = () => {
return jest.fn((tagName) => {
if (!tagName || typeof tagName !== 'string') {
return 'unknown-tag';
}
return tagName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
});
};

View File

@@ -0,0 +1,83 @@
/**
* Test for getTagAwareFilePath utility function
* Tests the fix for Issue #850
*/
import { getTagAwareFilePath } from '../../../../scripts/modules/utils.js';
import path from 'path';
describe('getTagAwareFilePath utility function', () => {
const projectRoot = '/test/project';
const basePath = '.taskmaster/reports/task-complexity-report.json';
it('should return base path for master tag', () => {
const result = getTagAwareFilePath(basePath, 'master', projectRoot);
const expected = path.join(projectRoot, basePath);
expect(result).toBe(expected);
});
it('should return base path for null tag', () => {
const result = getTagAwareFilePath(basePath, null, projectRoot);
const expected = path.join(projectRoot, basePath);
expect(result).toBe(expected);
});
it('should return base path for undefined tag', () => {
const result = getTagAwareFilePath(basePath, undefined, projectRoot);
const expected = path.join(projectRoot, basePath);
expect(result).toBe(expected);
});
it('should return tag-specific path for non-master tag', () => {
const tag = 'feature-branch';
const result = getTagAwareFilePath(basePath, tag, projectRoot);
const expected = path.join(
projectRoot,
'.taskmaster/reports/task-complexity-report_feature-branch.json'
);
expect(result).toBe(expected);
});
it('should handle different file extensions', () => {
const csvBasePath = '.taskmaster/reports/export.csv';
const tag = 'dev-branch';
const result = getTagAwareFilePath(csvBasePath, tag, projectRoot);
const expected = path.join(
projectRoot,
'.taskmaster/reports/export_dev-branch.csv'
);
expect(result).toBe(expected);
});
it('should handle paths without extensions', () => {
const noExtPath = '.taskmaster/reports/summary';
const tag = 'test-tag';
const result = getTagAwareFilePath(noExtPath, tag, projectRoot);
// Since there's no extension, it should append the tag
const expected = path.join(
projectRoot,
'.taskmaster/reports/summary_test-tag'
);
expect(result).toBe(expected);
});
it('should use default project root when not provided', () => {
const tag = 'feature-tag';
const result = getTagAwareFilePath(basePath, tag);
const expected = path.join(
'.',
'.taskmaster/reports/task-complexity-report_feature-tag.json'
);
expect(result).toBe(expected);
});
it('should handle complex tag names with special characters', () => {
const tag = 'feature-user-auth-v2';
const result = getTagAwareFilePath(basePath, tag, projectRoot);
const expected = path.join(
projectRoot,
'.taskmaster/reports/task-complexity-report_feature-user-auth-v2.json'
);
expect(result).toBe(expected);
});
});