Merge pull request #3673 from Tibsfox/fix/auto-remediate-stale-slice-status

fix(gsd): auto-remediate stale slice DB status when SUMMARY exists
This commit is contained in:
Jeremy McSpadden 2026-04-07 07:07:33 -05:00 committed by GitHub
commit 0d3789eee5
3 changed files with 69 additions and 7 deletions

View file

@ -39,7 +39,7 @@ import {
} from "./auto-recovery.js";
import { regenerateIfMissing } from "./workflow-projections.js";
import { syncStateToProjectRoot } from "./auto-worktree.js";
import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, _getAdapter } from "./gsd-db.js";
import { isDbAvailable, getTask, getSlice, getMilestone, updateTaskStatus, updateSliceStatus, _getAdapter } from "./gsd-db.js";
import { renderPlanCheckboxes } from "./markdown-renderer.js";
import { consumeSignal } from "./session-status-io.js";
import {
@ -161,7 +161,14 @@ export function detectRogueFileWrites(
const dbRow = getSlice(mid, sid);
if (!dbRow || dbRow.status !== "complete") {
rogues.push({ path: summaryPath, unitType, unitId });
// Auto-remediate: SUMMARY exists on disk but DB is stale — sync DB to
// match filesystem instead of reporting as rogue (#3633).
try {
updateSliceStatus(mid, sid, "complete", new Date().toISOString());
} catch {
// If DB update fails, fall back to rogue detection so the issue is visible
rogues.push({ path: summaryPath, unitType, unitId });
}
}
} else if (unitType === "plan-milestone") {
if (!mid) return [];

View file

@ -0,0 +1,56 @@
/**
* Regression test for #3673 auto-remediate stale slice DB status
*
* When complete-slice fails after writing SUMMARY.md but before calling
* updateSliceStatus(), the DB stays stale and the post-unit check
* previously reported this as a "rogue" artifact, causing infinite
* re-dispatch. The fix calls updateSliceStatus() to sync the DB.
*
* This structural test verifies updateSliceStatus is imported and called
* in the complete-slice branch of auto-post-unit.ts.
*/
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const source = readFileSync(join(__dirname, '..', 'auto-post-unit.ts'), 'utf-8');
describe('auto-remediate stale slice status (#3673)', () => {
test('updateSliceStatus is imported from gsd-db', () => {
assert.match(source, /import\s*\{[^}]*updateSliceStatus[^}]*\}\s*from\s*["']\.\/gsd-db/,
'updateSliceStatus should be imported from gsd-db');
});
test('updateSliceStatus is called with "complete" status', () => {
assert.match(source, /updateSliceStatus\(mid,\s*sid,\s*["']complete["']/,
'updateSliceStatus should be called with "complete" status');
});
test('remediation is wrapped in try-catch for fallback to rogue detection', () => {
// The updateSliceStatus call should be in a try block with a catch
// that falls back to rogues.push
const updateIdx = source.indexOf('updateSliceStatus(mid, sid');
assert.ok(updateIdx > 0, 'updateSliceStatus call should exist');
// Find surrounding try-catch
const before = source.slice(Math.max(0, updateIdx - 200), updateIdx);
assert.match(before, /try\s*\{/,
'updateSliceStatus should be inside a try block');
const after = source.slice(updateIdx, updateIdx + 300);
assert.match(after, /catch/,
'try block should have a catch for fallback');
});
test('rogue detection still exists as fallback', () => {
// rogues.push should appear in the catch block
assert.match(source, /rogues\.push\(\{.*path:\s*summaryPath/,
'rogues.push fallback should still exist');
});
});

View file

@ -149,7 +149,7 @@ test("rogue detection: DB not available → returns empty array (graceful degrad
}
});
test("rogue detection: slice summary on disk, no DB row → detected as rogue", () => {
test("rogue detection: slice summary on disk, no DB row → auto-remediated (not rogue)", () => {
const basePath = createTmpBase();
const dbPath = join(basePath, ".gsd", "gsd.db");
mkdirSync(join(basePath, ".gsd"), { recursive: true });
@ -160,11 +160,10 @@ test("rogue detection: slice summary on disk, no DB row → detected as rogue",
const summaryPath = createSliceSummaryOnDisk(basePath, "M001", "S01");
assert.ok(existsSync(summaryPath), "Slice summary file should exist on disk");
// Fix #3633: stale slice DB status is auto-remediated via updateSliceStatus()
// instead of being reported as rogue, so rogues array should be empty.
const rogues = detectRogueFileWrites("complete-slice", "M001/S01", basePath);
assert.equal(rogues.length, 1, "Should detect one rogue slice file");
assert.equal(rogues[0].path, summaryPath);
assert.equal(rogues[0].unitType, "complete-slice");
assert.equal(rogues[0].unitId, "M001/S01");
assert.equal(rogues.length, 0, "Should auto-remediate stale slice, not report as rogue");
} finally {
closeDatabase();
rmSync(basePath, { recursive: true, force: true });