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:
Cedric Hurst
2025-12-31 01:48:06 -06:00
parent bf19b0c2c3
commit a5f0a01b06
3 changed files with 113 additions and 3 deletions

View File

@@ -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);

View File

@@ -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', () => {

View File

@@ -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;
});