cmdPhasesClear hard-deleted committed phase directories (rmSync) with no archive, so browsable phase history was silently lost at a milestone switch. The #1447 dirty-tree guard was a no-op for the common committed case (a clean tree passes the guard, then rmSync destroyed the dirs, leaving orphaned uncommitted deletions and no archive). Archive-then-remove: move each non-999 phase dir to milestones/<version>-phases/ (version from getMilestoneInfo; timestamp fallback; collision-safe) using retryRenameSync — mirroring the existing archivePhases path in cmdMilestoneComplete. The #1447 uncommitted-changes guard is retained as a secondary backstop. Tests in tests/new-milestone-clear-phases.test.cjs updated: phase content is asserted to SURVIVE in milestones/*-phases/ (archived), not be destroyed — including the committed-dirs case that previously codified the no-op. Closes #1871
This commit is contained in:
@@ -101,7 +101,7 @@ describe('phases clear command', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('clears nested phase content (recursive delete)', () => {
|
||||
test('archives nested phase content (moved, not deleted) (#1871)', () => {
|
||||
const phasesDir = path.join(tmpDir, '.planning', 'phases');
|
||||
const phase1 = path.join(phasesDir, '01-foundation');
|
||||
const nested = path.join(phase1, 'subdir');
|
||||
@@ -111,10 +111,33 @@ describe('phases clear command', () => {
|
||||
const result = runGsdTools('phases clear --confirm', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
assert.ok(!fs.existsSync(phase1), 'phase directory including nested content should be removed');
|
||||
// Source is cleared (moved away)...
|
||||
assert.ok(!fs.existsSync(phase1), 'phase directory should be moved out of .planning/phases/');
|
||||
// ...but the nested content SURVIVES in the archive (not destroyed).
|
||||
const archive = findPhasesArchive(tmpDir);
|
||||
assert.ok(archive, 'an archive dir milestones/*-phases/ should exist');
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(archive, '01-foundation', 'subdir', 'deep-file.md')),
|
||||
'nested phase content must be preserved in the archive, not deleted',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Locate the `milestones/<version>-phases/` archive directory created by phases clear.
|
||||
function findPhasesArchive(tmpDir) {
|
||||
const milestonesDir = path.join(tmpDir, '.planning', 'milestones');
|
||||
try {
|
||||
for (const entry of fs.readdirSync(milestonesDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && /-phases$/.test(entry.name)) {
|
||||
return path.join(milestonesDir, entry.name);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* no milestones dir */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── #1447: uncommitted-changes guard ───────────────────────────────────────
|
||||
|
||||
describe('phases clear: uncommitted-changes guard (#1447)', () => {
|
||||
@@ -190,7 +213,14 @@ describe('phases clear: uncommitted-changes guard (#1447)', () => {
|
||||
assert.ok(result.success, `should succeed when phase files are committed: ${result.error}`);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.cleared, 1, 'should clear 1 phase directory');
|
||||
assert.ok(!fs.existsSync(phase1), 'committed phase directory should be removed');
|
||||
// #1871: a committed phase dir is ARCHIVED (moved to milestones/*-phases/), not destroyed.
|
||||
assert.ok(!fs.existsSync(phase1), 'committed phase directory should be moved out of .planning/phases/');
|
||||
const archive = findPhasesArchive(tmpDir);
|
||||
assert.ok(archive, 'a milestones/*-phases/ archive should be created for committed phase dirs');
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(archive, '01-foundation', 'PLAN.md')),
|
||||
'committed phase content must be preserved in the archive, not hard-deleted',
|
||||
);
|
||||
});
|
||||
|
||||
test('guard skips gracefully when not in a git repo (no guard, proceeds normally)', () => {
|
||||
|
||||
Reference in New Issue
Block a user