mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
fix: preserve subtask metadata when AI returns modified subtasks
- Add subtask metadata preservation in FileStorage.updateTask - Add subtask metadata preservation in legacy update-task-by-id.js - Add test for subtask metadata preservation during AI updates Fixes Cursor Bugbot review comment about subtask metadata loss
This commit is contained in:
@@ -381,9 +381,28 @@ export class FileStorage implements IStorage {
|
||||
throw new Error(`Task ${taskId} not found`);
|
||||
}
|
||||
|
||||
const existingTask = tasks[taskIndex];
|
||||
|
||||
// Preserve subtask metadata when subtasks are updated
|
||||
// AI operations don't include metadata in returned subtasks
|
||||
let mergedSubtasks = updates.subtasks;
|
||||
if (updates.subtasks && existingTask.subtasks) {
|
||||
mergedSubtasks = updates.subtasks.map((updatedSubtask) => {
|
||||
const originalSubtask = existingTask.subtasks?.find(
|
||||
(st) => st.id === updatedSubtask.id
|
||||
);
|
||||
// Preserve original subtask's metadata if it exists
|
||||
if (originalSubtask?.metadata) {
|
||||
return { ...updatedSubtask, metadata: originalSubtask.metadata };
|
||||
}
|
||||
return updatedSubtask;
|
||||
});
|
||||
}
|
||||
|
||||
tasks[taskIndex] = {
|
||||
...tasks[taskIndex],
|
||||
...existingTask,
|
||||
...updates,
|
||||
...(mergedSubtasks && { subtasks: mergedSubtasks }),
|
||||
id: String(taskId) // Keep consistent with normalizeTaskIds
|
||||
};
|
||||
await this.saveTasks(tasks, tag);
|
||||
|
||||
@@ -272,6 +272,90 @@ describe('AI Operation Metadata Preservation - Integration Tests', () => {
|
||||
subtaskMeta: 'subtask-value'
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve subtask metadata when AI returns modified subtasks', async () => {
|
||||
// This tests the scenario where update-task AI returns subtasks
|
||||
// (full update mode, not append mode) - subtask metadata must be preserved
|
||||
const tasks: Task[] = [
|
||||
createTask('1', {
|
||||
metadata: { parentMeta: 'parent-value' },
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
parentId: '1',
|
||||
title: 'Original subtask 1',
|
||||
description: 'Has metadata',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
metadata: { ticket: 'JIRA-100', sprint: 'S1' }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
parentId: '1',
|
||||
title: 'Original subtask 2',
|
||||
description: 'Also has metadata',
|
||||
status: 'in-progress',
|
||||
priority: 'high',
|
||||
dependencies: [1],
|
||||
details: '',
|
||||
testStrategy: '',
|
||||
metadata: { ticket: 'JIRA-101', reviewed: true }
|
||||
}
|
||||
]
|
||||
})
|
||||
];
|
||||
await storage.saveTasks(tasks);
|
||||
|
||||
// Simulate AI returning modified subtasks (AI doesn't include metadata)
|
||||
const aiModifiedSubtasks = [
|
||||
{
|
||||
id: 1,
|
||||
parentId: '1',
|
||||
title: 'AI updated subtask 1', // Title changed by AI
|
||||
description: 'AI updated description',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
details: 'AI added details',
|
||||
testStrategy: ''
|
||||
// No metadata - AI schema excludes it
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
parentId: '1',
|
||||
title: 'AI updated subtask 2',
|
||||
description: 'AI updated this too',
|
||||
status: 'in-progress',
|
||||
priority: 'high',
|
||||
dependencies: [1],
|
||||
details: 'More AI details',
|
||||
testStrategy: ''
|
||||
// No metadata - AI schema excludes it
|
||||
}
|
||||
];
|
||||
|
||||
// Update task with AI-modified subtasks
|
||||
await storage.updateTask('1', {
|
||||
title: 'AI Updated Task',
|
||||
subtasks: aiModifiedSubtasks
|
||||
});
|
||||
|
||||
const loadedTasks = await storage.loadTasks();
|
||||
// Parent metadata preserved
|
||||
expect(loadedTasks[0].metadata).toEqual({ parentMeta: 'parent-value' });
|
||||
// Subtask metadata should be preserved from originals
|
||||
expect(loadedTasks[0].subtasks[0].metadata).toEqual({
|
||||
ticket: 'JIRA-100',
|
||||
sprint: 'S1'
|
||||
});
|
||||
expect(loadedTasks[0].subtasks[1].metadata).toEqual({
|
||||
ticket: 'JIRA-101',
|
||||
reviewed: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse-prd operation simulation', () => {
|
||||
|
||||
@@ -455,6 +455,10 @@ async function updateTaskById(
|
||||
if (updatedTask.subtasks && Array.isArray(updatedTask.subtasks)) {
|
||||
let currentSubtaskId = 1;
|
||||
updatedTask.subtasks = updatedTask.subtasks.map((subtask) => {
|
||||
// Find original subtask to preserve its metadata
|
||||
const originalSubtask = taskToUpdate.subtasks?.find(
|
||||
(st) => st.id === subtask.id || st.id === currentSubtaskId
|
||||
);
|
||||
// Fix AI-generated subtask IDs that might be strings or use parent ID as prefix
|
||||
const correctedSubtask = {
|
||||
...subtask,
|
||||
@@ -472,8 +476,11 @@ async function updateTaskById(
|
||||
)
|
||||
: [],
|
||||
status: subtask.status || 'pending',
|
||||
testStrategy: subtask.testStrategy ?? null
|
||||
};
|
||||
testStrategy: subtask.testStrategy ?? null,
|
||||
// Preserve subtask metadata from original (AI schema excludes metadata)
|
||||
...(originalSubtask?.metadata && {
|
||||
metadata: originalSubtask.metadata
|
||||
}) };
|
||||
currentSubtaskId++;
|
||||
return correctedSubtask;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user