Compare commits
11 Commits
claude/iss
...
fix-task-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b594795238 | ||
|
|
8f97b6aead | ||
|
|
3f31c34f8d | ||
|
|
ef23beac0d | ||
|
|
077cbb99de | ||
|
|
ee2af1760f | ||
|
|
2421c205c4 | ||
|
|
33208388bd | ||
|
|
3fa0c7f7af | ||
|
|
0609b9ae28 | ||
|
|
b79eb4f7a2 |
5
.changeset/eleven-horses-shop.md
Normal file
5
.changeset/eleven-horses-shop.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix for tasks not found when using string IDs
|
||||||
@@ -262,6 +262,43 @@ function hasTaggedStructure(data) {
|
|||||||
return false;
|
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
|
* Reads and parses a JSON file
|
||||||
* @param {string} filepath - Path to the 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...`);
|
console.log(`File is in legacy format, performing migration...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeTaskIds(data.tasks);
|
||||||
|
|
||||||
// This is legacy format - migrate it to tagged format
|
// This is legacy format - migrate it to tagged format
|
||||||
const migratedData = {
|
const migratedData = {
|
||||||
master: {
|
master: {
|
||||||
@@ -401,6 +440,16 @@ function readJSON(filepath, projectRoot = null, tag = null) {
|
|||||||
// Store reference to the raw tagged data for functions that need it
|
// Store reference to the raw tagged data for functions that need it
|
||||||
const originalTaggedData = JSON.parse(JSON.stringify(data));
|
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)
|
// Check and auto-switch git tags if enabled (for existing tagged format)
|
||||||
// This needs to run synchronously BEFORE tag resolution
|
// This needs to run synchronously BEFORE tag resolution
|
||||||
if (projectRoot) {
|
if (projectRoot) {
|
||||||
@@ -448,6 +497,8 @@ function readJSON(filepath, projectRoot = null, tag = null) {
|
|||||||
// Get the data for the resolved tag
|
// Get the data for the resolved tag
|
||||||
const tagData = data[resolvedTag];
|
const tagData = data[resolvedTag];
|
||||||
if (tagData && tagData.tasks) {
|
if (tagData && tagData.tasks) {
|
||||||
|
normalizeTaskIds(tagData.tasks);
|
||||||
|
|
||||||
// Add the _rawTaggedData property and the resolved tag to the returned data
|
// Add the _rawTaggedData property and the resolved tag to the returned data
|
||||||
const result = {
|
const result = {
|
||||||
...tagData,
|
...tagData,
|
||||||
@@ -464,6 +515,8 @@ function readJSON(filepath, projectRoot = null, tag = null) {
|
|||||||
// If the resolved tag doesn't exist, fall back to master
|
// If the resolved tag doesn't exist, fall back to master
|
||||||
const masterData = data.master;
|
const masterData = data.master;
|
||||||
if (masterData && masterData.tasks) {
|
if (masterData && masterData.tasks) {
|
||||||
|
normalizeTaskIds(masterData.tasks);
|
||||||
|
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
console.log(
|
console.log(
|
||||||
`Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks`
|
`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
|
// If anything goes wrong, try to return master or empty
|
||||||
const masterData = data.master;
|
const masterData = data.master;
|
||||||
if (masterData && masterData.tasks) {
|
if (masterData && masterData.tasks) {
|
||||||
|
normalizeTaskIds(masterData.tasks);
|
||||||
return {
|
return {
|
||||||
...masterData,
|
...masterData,
|
||||||
_rawTaggedData: originalTaggedData
|
_rawTaggedData: originalTaggedData
|
||||||
@@ -1412,5 +1466,6 @@ export {
|
|||||||
createStateJson,
|
createStateJson,
|
||||||
markMigrationForNotice,
|
markMigrationForNotice,
|
||||||
flattenTasksWithSubtasks,
|
flattenTasksWithSubtasks,
|
||||||
ensureTagMetadata
|
ensureTagMetadata,
|
||||||
|
normalizeTaskIds
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,82 @@ describe('Task Finder', () => {
|
|||||||
expect(result.originalSubtaskCount).toBeNull();
|
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', () => {
|
test('should find a subtask using dot notation', () => {
|
||||||
const result = findTaskById(sampleTasks.tasks, '3.1');
|
const result = findTaskById(sampleTasks.tasks, '3.1');
|
||||||
expect(result.task).toBeDefined();
|
expect(result.task).toBeDefined();
|
||||||
|
|||||||
Reference in New Issue
Block a user