diff --git a/.changeset/eleven-horses-shop.md b/.changeset/eleven-horses-shop.md new file mode 100644 index 00000000..84b63caf --- /dev/null +++ b/.changeset/eleven-horses-shop.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": patch +--- + +Fix for tasks not found when using string IDs diff --git a/scripts/modules/utils.js b/scripts/modules/utils.js index e8a92dd9..fa6cdc26 100644 --- a/scripts/modules/utils.js +++ b/scripts/modules/utils.js @@ -262,6 +262,43 @@ function hasTaggedStructure(data) { return false; } +/** + * Normalizes task IDs to ensure they are numbers instead of strings + * @param {Array} tasks - Array of tasks to normalize + */ +function normalizeTaskIds(tasks) { + if (!Array.isArray(tasks)) return; + + tasks.forEach((task) => { + // Convert task ID to number with validation + if (task.id !== undefined) { + const parsedId = parseInt(task.id, 10); + if (!isNaN(parsedId) && parsedId > 0) { + task.id = parsedId; + } + } + + // Convert subtask IDs to numbers with validation + if (Array.isArray(task.subtasks)) { + task.subtasks.forEach((subtask) => { + if (subtask.id !== undefined) { + // Check for dot notation (which shouldn't exist in storage) + if (typeof subtask.id === 'string' && subtask.id.includes('.')) { + // Extract the subtask part after the dot + const parts = subtask.id.split('.'); + subtask.id = parseInt(parts[parts.length - 1], 10); + } else { + const parsedSubtaskId = parseInt(subtask.id, 10); + if (!isNaN(parsedSubtaskId) && parsedSubtaskId > 0) { + subtask.id = parsedSubtaskId; + } + } + } + }); + } + }); +} + /** * Reads and parses a JSON file * @param {string} filepath - Path to the JSON file @@ -322,6 +359,8 @@ function readJSON(filepath, projectRoot = null, tag = null) { console.log(`File is in legacy format, performing migration...`); } + normalizeTaskIds(data.tasks); + // This is legacy format - migrate it to tagged format const migratedData = { master: { @@ -401,6 +440,16 @@ function readJSON(filepath, projectRoot = null, tag = null) { // Store reference to the raw tagged data for functions that need it const originalTaggedData = JSON.parse(JSON.stringify(data)); + // Normalize IDs in all tags before storing as originalTaggedData + for (const tagName in originalTaggedData) { + if ( + originalTaggedData[tagName] && + Array.isArray(originalTaggedData[tagName].tasks) + ) { + normalizeTaskIds(originalTaggedData[tagName].tasks); + } + } + // Check and auto-switch git tags if enabled (for existing tagged format) // This needs to run synchronously BEFORE tag resolution if (projectRoot) { @@ -448,6 +497,8 @@ function readJSON(filepath, projectRoot = null, tag = null) { // Get the data for the resolved tag const tagData = data[resolvedTag]; if (tagData && tagData.tasks) { + normalizeTaskIds(tagData.tasks); + // Add the _rawTaggedData property and the resolved tag to the returned data const result = { ...tagData, @@ -464,6 +515,8 @@ function readJSON(filepath, projectRoot = null, tag = null) { // If the resolved tag doesn't exist, fall back to master const masterData = data.master; if (masterData && masterData.tasks) { + normalizeTaskIds(masterData.tasks); + if (isDebug) { console.log( `Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks` @@ -493,6 +546,7 @@ function readJSON(filepath, projectRoot = null, tag = null) { // If anything goes wrong, try to return master or empty const masterData = data.master; if (masterData && masterData.tasks) { + normalizeTaskIds(masterData.tasks); return { ...masterData, _rawTaggedData: originalTaggedData @@ -1412,5 +1466,6 @@ export { createStateJson, markMigrationForNotice, flattenTasksWithSubtasks, - ensureTagMetadata + ensureTagMetadata, + normalizeTaskIds }; diff --git a/tests/unit/task-finder.test.js b/tests/unit/task-finder.test.js index b480a2a2..92e24c26 100644 --- a/tests/unit/task-finder.test.js +++ b/tests/unit/task-finder.test.js @@ -23,6 +23,82 @@ describe('Task Finder', () => { expect(result.originalSubtaskCount).toBeNull(); }); + test('should find tasks when JSON contains string IDs (normalized to numbers)', () => { + // Simulate tasks loaded from JSON with string IDs and mixed subtask notations + const tasksWithStringIds = [ + { id: '1', title: 'First Task' }, + { + id: '2', + title: 'Second Task', + subtasks: [ + { id: '1', title: 'Subtask One' }, + { id: '2.2', title: 'Subtask Two (with dotted notation)' } // Testing dotted notation + ] + }, + { + id: '5', + title: 'Fifth Task', + subtasks: [ + { id: '5.1', title: 'Subtask with dotted ID' }, // Should normalize to 1 + { id: '3', title: 'Subtask with simple ID' } // Should stay as 3 + ] + } + ]; + + // The readJSON function should normalize these IDs to numbers + // For this test, we'll manually normalize them to simulate what happens + tasksWithStringIds.forEach((task) => { + task.id = parseInt(task.id, 10); + if (task.subtasks) { + task.subtasks.forEach((subtask) => { + // Handle dotted notation like "5.1" -> extract the subtask part + if (typeof subtask.id === 'string' && subtask.id.includes('.')) { + const parts = subtask.id.split('.'); + subtask.id = parseInt(parts[parts.length - 1], 10); + } else { + subtask.id = parseInt(subtask.id, 10); + } + }); + } + }); + + // Test finding tasks by numeric ID + const result1 = findTaskById(tasksWithStringIds, 5); + expect(result1.task).toBeDefined(); + expect(result1.task.id).toBe(5); + expect(result1.task.title).toBe('Fifth Task'); + + // Test finding tasks by string ID + const result2 = findTaskById(tasksWithStringIds, '5'); + expect(result2.task).toBeDefined(); + expect(result2.task.id).toBe(5); + + // Test finding subtasks with normalized IDs + const result3 = findTaskById(tasksWithStringIds, '2.1'); + expect(result3.task).toBeDefined(); + expect(result3.task.id).toBe(1); + expect(result3.task.title).toBe('Subtask One'); + expect(result3.task.isSubtask).toBe(true); + + // Test subtask that was originally "2.2" (should be normalized to 2) + const result4 = findTaskById(tasksWithStringIds, '2.2'); + expect(result4.task).toBeDefined(); + expect(result4.task.id).toBe(2); + expect(result4.task.title).toBe('Subtask Two (with dotted notation)'); + + // Test subtask that was originally "5.1" (should be normalized to 1) + const result5 = findTaskById(tasksWithStringIds, '5.1'); + expect(result5.task).toBeDefined(); + expect(result5.task.id).toBe(1); + expect(result5.task.title).toBe('Subtask with dotted ID'); + + // Test subtask that was originally "3" (should stay as 3) + const result6 = findTaskById(tasksWithStringIds, '5.3'); + expect(result6.task).toBeDefined(); + expect(result6.task.id).toBe(3); + expect(result6.task.title).toBe('Subtask with simple ID'); + }); + test('should find a subtask using dot notation', () => { const result = findTaskById(sampleTasks.tasks, '3.1'); expect(result.task).toBeDefined();