fix(gsd): discard milestone DB and worktree state (#4065)
This commit is contained in:
parent
cf34383104
commit
e6110976e7
3 changed files with 116 additions and 1 deletions
|
|
@ -2200,6 +2200,39 @@ export function deleteSlice(milestoneId: string, sliceId: string): void {
|
|||
});
|
||||
}
|
||||
|
||||
export function deleteMilestone(milestoneId: string): void {
|
||||
if (!currentDb) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
||||
transaction(() => {
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM verification_evidence WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM quality_gates WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM tasks WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM slice_dependencies WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM slices WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM replan_history WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM assessments WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM artifacts WHERE milestone_id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
currentDb!.prepare(
|
||||
`DELETE FROM milestones WHERE id = :mid`,
|
||||
).run({ ":mid": milestoneId });
|
||||
});
|
||||
}
|
||||
|
||||
export function updateSliceFields(milestoneId: string, sliceId: string, fields: {
|
||||
title?: string;
|
||||
risk?: string;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import {
|
|||
} from "./paths.js";
|
||||
import { invalidateAllCaches } from "./cache.js";
|
||||
import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
||||
import { getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
||||
import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
||||
import { removeWorktree } from "./worktree-manager.js";
|
||||
import { logWarning } from "./workflow-logger.js";
|
||||
|
||||
// ─── Park ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -110,6 +111,15 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
|
|||
const mDir = resolveMilestonePath(basePath, milestoneId);
|
||||
if (!mDir || !existsSync(mDir)) return false;
|
||||
|
||||
try {
|
||||
removeWorktree(basePath, milestoneId, {
|
||||
branch: `milestone/${milestoneId}`,
|
||||
deleteBranch: true,
|
||||
});
|
||||
} catch (err) {
|
||||
logWarning("engine", `discardMilestone worktree cleanup failed for ${milestoneId}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
rmSync(mDir, { recursive: true, force: true });
|
||||
|
||||
// Prune from queue order if present
|
||||
|
|
@ -118,6 +128,14 @@ export function discardMilestone(basePath: string, milestoneId: string): boolean
|
|||
saveQueueOrder(basePath, order.filter(id => id !== milestoneId));
|
||||
}
|
||||
|
||||
if (isDbAvailable()) {
|
||||
try {
|
||||
deleteMilestone(milestoneId);
|
||||
} catch (err) {
|
||||
logWarning("engine", `discardMilestone DB cleanup failed for ${milestoneId}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
invalidateAllCaches();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,22 @@ import assert from 'node:assert/strict';
|
|||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
import { deriveState, invalidateStateCache, getActiveMilestoneId } from '../state.ts';
|
||||
import { clearPathCache } from '../paths.ts';
|
||||
import { parkMilestone, unparkMilestone, discardMilestone, isParked, getParkedReason } from '../milestone-actions.ts';
|
||||
import {
|
||||
closeDatabase,
|
||||
getMilestone,
|
||||
getMilestoneSlices,
|
||||
getSliceTasks,
|
||||
insertMilestone,
|
||||
insertSlice,
|
||||
insertTask,
|
||||
openDatabase,
|
||||
} from "../gsd-db.ts";
|
||||
import { createWorktree } from "../worktree-manager.ts";
|
||||
|
||||
|
||||
|
||||
|
|
@ -60,9 +72,29 @@ function createMilestone(base: string, mid: string, opts?: { withRoadmap?: boole
|
|||
}
|
||||
|
||||
function cleanup(base: string): void {
|
||||
try {
|
||||
closeDatabase();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function run(cmd: string, cwd: string): string {
|
||||
return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
|
||||
}
|
||||
|
||||
function initGitRepo(base: string): void {
|
||||
writeFileSync(join(base, "README.md"), "# test\n", "utf-8");
|
||||
writeFileSync(join(base, ".gsd", "STATE.md"), "# State\n", "utf-8");
|
||||
run("git init", base);
|
||||
run("git config user.email test@test.com", base);
|
||||
run("git config user.name Test", base);
|
||||
run("git add .", base);
|
||||
run('git commit -m "init"', base);
|
||||
run("git branch -M main", base);
|
||||
}
|
||||
|
||||
function clearCaches(): void {
|
||||
clearPathCache();
|
||||
invalidateStateCache();
|
||||
|
|
@ -294,6 +326,38 @@ test('discardMilestone updates queue order', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('discardMilestone removes DB rows, worktree, and milestone branch', () => {
|
||||
const base = createFixtureBase();
|
||||
try {
|
||||
createMilestone(base, 'M001', { withRoadmap: true });
|
||||
initGitRepo(base);
|
||||
clearCaches();
|
||||
|
||||
assert.ok(openDatabase(join(base, '.gsd', 'gsd.db')), 'database opens');
|
||||
insertMilestone({ id: 'M001', title: 'Discard me', status: 'active' });
|
||||
insertSlice({ milestoneId: 'M001', id: 'S01', title: 'Only slice', status: 'pending' });
|
||||
insertTask({ milestoneId: 'M001', sliceId: 'S01', id: 'T01', title: 'Only task', status: 'pending' });
|
||||
|
||||
const wt = createWorktree(base, 'M001', { branch: 'milestone/M001' });
|
||||
assert.ok(existsSync(wt.path), 'worktree exists before discard');
|
||||
assert.ok(run('git branch', base).includes('milestone/M001'), 'milestone branch exists before discard');
|
||||
assert.ok(getMilestone('M001'), 'milestone exists in DB before discard');
|
||||
assert.equal(getMilestoneSlices('M001').length, 1, 'slice exists in DB before discard');
|
||||
assert.equal(getSliceTasks('M001', 'S01').length, 1, 'task exists in DB before discard');
|
||||
|
||||
const success = discardMilestone(base, 'M001');
|
||||
assert.ok(success, 'discardMilestone returns true');
|
||||
|
||||
assert.equal(getMilestone('M001'), null, 'milestone row removed from DB');
|
||||
assert.equal(getMilestoneSlices('M001').length, 0, 'slice rows removed from DB');
|
||||
assert.equal(getSliceTasks('M001', 'S01').length, 0, 'task rows removed from DB');
|
||||
assert.ok(!existsSync(wt.path), 'worktree removed after discard');
|
||||
assert.ok(!run('git branch', base).includes('milestone/M001'), 'milestone branch removed after discard');
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Test 12: All milestones parked → no active milestone ─────────────
|
||||
test('All milestones parked → no active', async () => {
|
||||
const base = createFixtureBase();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue