Merge pull request #3671 from Tibsfox/fix/phantom-milestone-ghost-detection
fix(gsd): detect phantom milestones from abandoned generate_id
This commit is contained in:
commit
498a7a0aac
3 changed files with 71 additions and 5 deletions
|
|
@ -79,10 +79,20 @@ import {
|
|||
* as ghosts, causing auto-mode to skip them entirely.
|
||||
*/
|
||||
export function isGhostMilestone(basePath: string, mid: string): boolean {
|
||||
// If the milestone has a DB row, it's a known milestone — not a ghost.
|
||||
// If the milestone has a DB row, it's usually a known milestone — not a ghost.
|
||||
// Exception: a "queued" row with no disk artifacts is a phantom from
|
||||
// gsd_milestone_generate_id that was never planned (#3645).
|
||||
if (isDbAvailable()) {
|
||||
const dbRow = getMilestone(mid);
|
||||
if (dbRow) return false;
|
||||
if (dbRow) {
|
||||
if (dbRow.status === 'queued') {
|
||||
const hasContent = resolveMilestoneFile(basePath, mid, "CONTEXT")
|
||||
|| resolveMilestoneFile(basePath, mid, "ROADMAP")
|
||||
|| resolveMilestoneFile(basePath, mid, "SUMMARY");
|
||||
return !hasContent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If a worktree exists for this milestone, it was legitimately created.
|
||||
|
|
|
|||
|
|
@ -1098,16 +1098,17 @@ describe('derive-state-db', async () => {
|
|||
// M001: complete milestone with summary
|
||||
writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
|
||||
|
||||
// M002: queued milestone — directory exists, no content files, but has DB row
|
||||
// M002: queued milestone — directory exists with CONTEXT file and DB row
|
||||
mkdirSync(join(base, '.gsd', 'milestones', 'M002', 'slices'), { recursive: true });
|
||||
writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002 Context\n\nPlanned milestone.');
|
||||
|
||||
// DB has both M001 complete and M002 queued
|
||||
openDatabase(':memory:');
|
||||
insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
|
||||
insertMilestone({ id: 'M002', title: 'Second', status: 'queued' });
|
||||
|
||||
// isGhostMilestone should NOT treat M002 as ghost when DB row exists
|
||||
assert.ok(!isGhostMilestone(base, 'M002'), 'ghost-dbrow: M002 with DB row is NOT a ghost');
|
||||
// isGhostMilestone should NOT treat M002 as ghost when DB row + content files exist
|
||||
assert.ok(!isGhostMilestone(base, 'M002'), 'ghost-dbrow: M002 with DB row and content is NOT a ghost');
|
||||
|
||||
invalidateStateCache();
|
||||
const dbState = await deriveStateFromDb(base);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Regression test for #3671 — isGhostMilestone detects phantom queued rows
|
||||
*
|
||||
* gsd_milestone_generate_id inserts a DB row with status "queued" as a side
|
||||
* effect. If the milestone is never planned, isGhostMilestone previously
|
||||
* returned false for any milestone with a DB row, blocking the state machine.
|
||||
*
|
||||
* The fix makes isGhostMilestone treat a "queued" DB row with no disk
|
||||
* artifacts (CONTEXT, ROADMAP, SUMMARY) as a ghost.
|
||||
*
|
||||
* This structural test verifies the dbRow.status === 'queued' guard exists.
|
||||
*/
|
||||
|
||||
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, '..', 'state.ts'), 'utf-8');
|
||||
|
||||
describe('isGhostMilestone phantom queued detection (#3671)', () => {
|
||||
test('isGhostMilestone function exists', () => {
|
||||
assert.match(source, /export function isGhostMilestone\(/,
|
||||
'isGhostMilestone should be exported');
|
||||
});
|
||||
|
||||
test('checks dbRow.status === queued', () => {
|
||||
assert.match(source, /dbRow\.status\s*===\s*['"]queued['"]/,
|
||||
'isGhostMilestone should check dbRow.status === "queued"');
|
||||
});
|
||||
|
||||
test('checks for CONTEXT disk artifact', () => {
|
||||
assert.match(source, /resolveMilestoneFile\(basePath,\s*mid,\s*["']CONTEXT["']\)/,
|
||||
'should check for CONTEXT file');
|
||||
});
|
||||
|
||||
test('checks for ROADMAP disk artifact', () => {
|
||||
assert.match(source, /resolveMilestoneFile\(basePath,\s*mid,\s*["']ROADMAP["']\)/,
|
||||
'should check for ROADMAP file');
|
||||
});
|
||||
|
||||
test('checks for SUMMARY disk artifact', () => {
|
||||
assert.match(source, /resolveMilestoneFile\(basePath,\s*mid,\s*["']SUMMARY["']\)/,
|
||||
'should check for SUMMARY file');
|
||||
});
|
||||
|
||||
test('returns !hasContent for queued rows (ghost if no artifacts)', () => {
|
||||
assert.match(source, /return !hasContent/,
|
||||
'should return !hasContent for queued phantom milestones');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue