fix(gsd): tolerate corrupt task arrays (#4056)

This commit is contained in:
mastertyko 2026-04-13 18:09:51 +02:00 committed by GitHub
parent e6110976e7
commit 3c44e3d4e2
2 changed files with 78 additions and 3 deletions

View file

@ -1564,6 +1564,23 @@ export interface TaskRow {
sequence: number;
}
function parseTaskArrayColumn(raw: unknown): string[] {
if (typeof raw !== "string" || raw.trim() === "") return [];
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed.map((value) => String(value));
if (parsed === null || parsed === undefined || parsed === "") return [];
return [String(parsed)];
} catch {
// Older/corrupt rows may contain comma-separated strings instead of JSON.
return raw
.split(",")
.map((value) => value.trim())
.filter(Boolean);
}
}
function rowToTask(row: Record<string, unknown>): TaskRow {
const parseTaskArray = (value: unknown): string[] => {
if (Array.isArray(value)) {
@ -1603,8 +1620,8 @@ function rowToTask(row: Record<string, unknown>): TaskRow {
blocker_discovered: (row["blocker_discovered"] as number) === 1,
deviations: row["deviations"] as string,
known_issues: row["known_issues"] as string,
key_files: JSON.parse((row["key_files"] as string) || "[]"),
key_decisions: JSON.parse((row["key_decisions"] as string) || "[]"),
key_files: parseTaskArrayColumn(row["key_files"]),
key_decisions: parseTaskArrayColumn(row["key_decisions"]),
full_summary_md: row["full_summary_md"] as string,
description: (row["description"] as string) ?? "",
estimate: (row["estimate"] as string) ?? "",

View file

@ -15,10 +15,14 @@ import {
getRequirementById,
getActiveDecisions,
getActiveRequirements,
getTask,
transaction,
_getAdapter,
_resetProvider,
insertMilestone,
insertSlice,
insertTask,
getTask,
getSliceTasks,
} from '../gsd-db.ts';
// ═══════════════════════════════════════════════════════════════════════════
@ -460,6 +464,60 @@ describe('gsd-db', () => {
assert.ok(!wasDbOpenAttempted(), 'wasDbOpenAttempted should reset after closeDatabase');
});
test('gsd-db: rowToTask tolerates corrupt comma-separated task arrays', () => {
openDatabase(':memory:');
insertMilestone({ id: 'M001', status: 'active' });
insertSlice({ milestoneId: 'M001', id: 'S01', status: 'active' });
insertTask({
milestoneId: 'M001',
sliceId: 'S01',
id: 'T01',
title: 'Recover corrupt arrays',
planning: {
description: 'desc',
estimate: 'small',
files: ['src/original.ts'],
verify: 'npm test',
inputs: ['docs/original.md'],
expectedOutput: ['dist/original.md'],
observabilityImpact: '',
},
});
const adapter = _getAdapter()!;
adapter.prepare(
`UPDATE tasks
SET files = ?, inputs = ?, expected_output = ?, key_files = ?, key_decisions = ?
WHERE milestone_id = ? AND slice_id = ? AND id = ?`,
).run(
'src-erf/Models/foo.cs, src-erf/Models/bar.cs',
'docs/input-a.md, docs/input-b.md',
'dist/out-a.md, dist/out-b.md',
'src/resources/extensions/gsd/gsd-db.ts, src/resources/extensions/gsd/state.ts',
'"decision-1"',
'M001',
'S01',
'T01',
);
const task = getTask('M001', 'S01', 'T01');
assert.ok(task, 'getTask should still return the corrupt row');
assert.deepStrictEqual(task!.files, ['src-erf/Models/foo.cs', 'src-erf/Models/bar.cs']);
assert.deepStrictEqual(task!.inputs, ['docs/input-a.md', 'docs/input-b.md']);
assert.deepStrictEqual(task!.expected_output, ['dist/out-a.md', 'dist/out-b.md']);
assert.deepStrictEqual(
task!.key_files,
['src/resources/extensions/gsd/gsd-db.ts', 'src/resources/extensions/gsd/state.ts'],
);
assert.deepStrictEqual(task!.key_decisions, ['decision-1']);
const sliceTasks = getSliceTasks('M001', 'S01');
assert.equal(sliceTasks.length, 1, 'getSliceTasks should also survive corrupt rows');
assert.deepStrictEqual(sliceTasks[0]!.files, task!.files);
closeDatabase();
});
// ─── Final Report ──────────────────────────────────────────────────────────
});