From 481f00e38a4fc1d841300c4178364992440b5fce Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 13 Jul 2026 00:02:52 -0400 Subject: [PATCH] fix(#2118): honor --dry-run in milestone complete with zero-mutation preview (#2155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(2118): honor --dry-run in milestone complete with zero-mutation preview milestone complete treated --dry-run as a no-op: the flag was neither parsed nor rejected, so a caller who expected a preview instead triggered the full destructive mutation (archive phases → move audit artifacts → rewrite STATE.md) with no way to back out. Fix (option 2 from the issue): add dryRun to MilestoneCompleteOptions, parse --dry-run in the dispatcher, and return a JSON preview plan (would_archive, would_update) after the read-only stats gathering but before any mutations. Also gated platformEnsureDir on !dryRun so the archive directory is not created during preview. 3 regression tests: no-mutation happy path, --no-archive-phases combo, and --force bypass combo. * docs(changeset): backfill PR number (#2155) * chore(#2118): regenerate pi/qwen golden fixtures after next merge The milestone --dry-run fix changes gsd-core/bin/gsd-tools.cjs; qwen.json (missed at authoring) and pi.json (added on next, never carried the fix) were the only two runtime fixtures still holding the pre-fix hash. All other runtimes already reflect the change. Regenerated via the sanctioned gen-golden-install-parity script. * fix(#2118): surface accomplishments in dry-run preview; fix --dry-run --raw Orthogonal review findings on the --dry-run preview: - The preview omitted the already-computed accomplishments (the primary MILESTONES.md content a real run writes); surface it as a top-level field, mirroring the real-run result. - `--dry-run --raw` discarded the structured payload and printed the literal string "dry-run"; drop the raw-value arg so --raw emits the full preview JSON, matching the real-run output() call. Adds tests: --dry-run --raw is parseable JSON, preview includes accomplishments, and --dry-run --force is proven zero-mutation. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .changeset/2118-milestone-complete-dry-run.md | 5 + gsd-core/bin/gsd-tools.cjs | 4 +- src/milestone.cts | 48 ++++++- .../golden-install-parity/antigravity.json | 2 +- .../golden-install-parity/augment.json | 2 +- .../golden-install-parity/claude-local.json | 2 +- .../golden-install-parity/claude.json | 2 +- .../fixtures/golden-install-parity/cline.json | 2 +- .../golden-install-parity/codebuddy.json | 2 +- .../fixtures/golden-install-parity/codex.json | 2 +- .../golden-install-parity/copilot.json | 2 +- .../golden-install-parity/cursor.json | 2 +- .../golden-install-parity/hermes.json | 2 +- .../fixtures/golden-install-parity/kilo.json | 2 +- .../fixtures/golden-install-parity/kimi.json | 2 +- .../golden-install-parity/opencode.json | 2 +- tests/fixtures/golden-install-parity/pi.json | 2 +- .../fixtures/golden-install-parity/qwen.json | 2 +- .../fixtures/golden-install-parity/trae.json | 2 +- .../golden-install-parity/windsurf.json | 2 +- .../fixtures/golden-install-parity/zcode.json | 2 +- tests/milestone.test.cjs | 125 ++++++++++++++++++ 22 files changed, 197 insertions(+), 21 deletions(-) create mode 100644 .changeset/2118-milestone-complete-dry-run.md diff --git a/.changeset/2118-milestone-complete-dry-run.md b/.changeset/2118-milestone-complete-dry-run.md new file mode 100644 index 000000000..d0f3d2659 --- /dev/null +++ b/.changeset/2118-milestone-complete-dry-run.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2155 +--- +**`milestone complete --dry-run` now prints a preview plan instead of silently mutating** — `gsd-tools milestone complete --dry-run` was neither parsed nor rejected, so a caller expecting a preview triggered the full destructive mutation (archive phases, move audit artifacts, rewrite STATE.md) with no way to back out. The `--dry-run` flag is now honored: it returns a JSON plan listing `would_archive` (roadmap, requirements, audit, phase dirs) and `would_update` (MILESTONES.md, STATE.md) targets with zero filesystem mutations. (#2118) diff --git a/gsd-core/bin/gsd-tools.cjs b/gsd-core/bin/gsd-tools.cjs index 376c0a0b0..fdf8aa129 100755 --- a/gsd-core/bin/gsd-tools.cjs +++ b/gsd-core/bin/gsd-tools.cjs @@ -1500,7 +1500,9 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand // new-milestone never inherits un-archived dirs. --no-archive-phases opts out. const archivePhases = !args.includes('--no-archive-phases'); const force = args.includes('--force'); - milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force }, raw); + // #2118: --dry-run prints a preview plan without mutating. + const dryRun = args.includes('--dry-run'); + milestone.cmdMilestoneComplete(cwd, args[2], { name: milestoneName, archivePhases, force, dryRun }, raw); } else { error('Unknown milestone subcommand. Available: complete', ERROR_REASON.SDK_UNKNOWN_COMMAND); } diff --git a/src/milestone.cts b/src/milestone.cts index b7c318ca5..1dbc8142d 100644 --- a/src/milestone.cts +++ b/src/milestone.cts @@ -38,6 +38,7 @@ interface MilestoneCompleteOptions { name?: string; force?: boolean; archivePhases?: boolean; + dryRun?: boolean; } function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: boolean): void { @@ -161,8 +162,10 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo const today = realClock.localToday(); const milestoneName = options.name || version; - // Ensure archive directory exists - platformEnsureDir(archiveDir); + // Ensure archive directory exists (skipped in dry-run — no mutations) + if (!options.dryRun) { + platformEnsureDir(archiveDir); + } // Scope stats and accomplishments to only the phases belonging to the // current milestone's ROADMAP. Uses the shared filter from roadmap-parser.cjs @@ -295,6 +298,47 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo /* intentionally empty */ } + // #2118: --dry-run preview — compute what WOULD happen without mutating. + // The stats above are read-only; all mutations start at the archive section below. + if (options.dryRun) { + const phaseDirsToArchive: string[] = []; + if (options.archivePhases !== false) { + try { + const entries = fs.readdirSync(phasesDir, { withFileTypes: true }); + for (const e of entries) { + if (e.isDirectory() && isDirInMilestone(e.name)) { + phaseDirsToArchive.push(e.name); + } + } + } catch { /* phasesDir missing — nothing to archive */ } + } + const dryRunResult = { + dry_run: true, + version, + name: milestoneName, + stats: { phases: phaseCount, plans: totalPlans, tasks: totalTasks }, + accomplishments, + would_archive: { + roadmap: fs.existsSync(roadmapPath) + ? { source: path.relative(cwd, roadmapPath).split(path.sep).join('/'), target: path.relative(cwd, path.join(archiveDir, `${version}-ROADMAP.md`)).split(path.sep).join('/') } + : null, + requirements: fs.existsSync(reqPath) + ? { source: path.relative(cwd, reqPath).split(path.sep).join('/'), target: path.relative(cwd, path.join(archiveDir, `${version}-REQUIREMENTS.md`)).split(path.sep).join('/') } + : null, + audit: fs.existsSync(path.join(planningBase, `${version}-MILESTONE-AUDIT.md`)) + ? { source: path.relative(cwd, path.join(planningBase, `${version}-MILESTONE-AUDIT.md`)).split(path.sep).join('/'), target: path.relative(cwd, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)).split(path.sep).join('/') } + : null, + phases: phaseDirsToArchive, + }, + would_update: { + milestones_md: path.relative(cwd, milestonesPath).split(path.sep).join('/'), + state_md: fs.existsSync(statePath) ? path.relative(cwd, statePath).split(path.sep).join('/') : null, + }, + }; + output(dryRunResult, raw); + return; + } + // Archive ROADMAP.md if (fs.existsSync(roadmapPath)) { const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8'); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index c81c1c0ee..6ff09f85e 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", - "gsd-core/bin/gsd-tools.cjs": "e8ccbbe8cefc2e9a", + "gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 8b4b9e282..765321b6d 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -109,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index ec1bd721d..c9acaf61f 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -108,7 +108,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index c2f7e3af9..cfd2a3b7f 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -37,7 +37,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index 2a41dd607..fd422bb06 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -41,7 +41,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "476aa24e8c4f03cf", - "gsd-core/bin/gsd-tools.cjs": "93273f4eda2750d2", + "gsd-core/bin/gsd-tools.cjs": "a84914f7cab74332", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index ebccb820a..bf52365d7 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -109,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index cd6c6ccf2..0c21c5a99 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -144,7 +144,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index d24e717ad..965eaf83c 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -39,7 +39,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "ea841e2865248e74", - "gsd-core/bin/gsd-tools.cjs": "e8ccbbe8cefc2e9a", + "gsd-core/bin/gsd-tools.cjs": "600e9ed0cb6ed7bb", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index dd01c479c..e97487dc7 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -109,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "2525f1ae8b086828", - "gsd-core/bin/gsd-tools.cjs": "3fa86b509ea5c8fc", + "gsd-core/bin/gsd-tools.cjs": "db16ded31dd6eecf", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index 57f4fee5d..c3d0421b6 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "3a3409215044af9f", - "gsd-core/bin/gsd-tools.cjs": "e979f092ce2b15ee", + "gsd-core/bin/gsd-tools.cjs": "b3f927ee4b4c4711", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index a2cc18051..91af4ccfc 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -109,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index eef41d3cd..0163d2a0e 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -102,7 +102,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 7c19f52c7..d9c557dff 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -109,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index bcf46ba88..77cf8f726 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -5,7 +5,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index 81c60c85f..32a131972 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "6e98d76e955e35a2", - "gsd-core/bin/gsd-tools.cjs": "0341a6cf54b9acbe", + "gsd-core/bin/gsd-tools.cjs": "6454021dec4d9563", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 027a8b0ac..a8dc5060c 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "de4627dff103d527", - "gsd-core/bin/gsd-tools.cjs": "75bf5691ded4927b", + "gsd-core/bin/gsd-tools.cjs": "c28c9076058c5e73", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 76b19cb99..6e81b5413 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -38,7 +38,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "5636ca0b726871b2", - "gsd-core/bin/gsd-tools.cjs": "dd746ae946645155", + "gsd-core/bin/gsd-tools.cjs": "568e7c6bdd415c1a", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index 7c3ef7577..5180a9cd9 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -109,7 +109,7 @@ "gsd-core/VERSION": "ef0deccd81a6723c", "gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74", "gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62", - "gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1", + "gsd-core/bin/gsd-tools.cjs": "37fb6081fcb18038", "gsd-core/bin/gsd_run": "62d9b647ede212e6", "gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16", "gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8", diff --git a/tests/milestone.test.cjs b/tests/milestone.test.cjs index 915c7a3d1..60f96aed0 100644 --- a/tests/milestone.test.cjs +++ b/tests/milestone.test.cjs @@ -113,6 +113,131 @@ describe('milestone complete command', () => { assert.ok(milestones.includes('Set up project infrastructure')); }); + test('#2118 — --dry-run does NOT mutate: no archive, no STATE.md rewrite, no phase move', () => { + writeRoadmap(tmpDir, `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), + `# Requirements\n\n- [x] User auth\n`, + ); + writeState(tmpDir); + const phasePath = mkPhaseDir(tmpDir, '01-foundation', { oneLiner: 'Set up project infrastructure' }); + + // Capture pre-state + const stateBefore = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + + const result = runGsdTools('milestone complete v1.0 --name Test --dry-run', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.dry_run, true, 'dry_run must be true'); + assert.strictEqual(output.version, 'v1.0'); + assert.ok(output.stats.phases >= 1, 'should count at least 1 phase'); + assert.ok(output.would_archive.roadmap, 'should list roadmap archive plan'); + assert.ok(output.would_archive.requirements, 'should list requirements archive plan'); + assert.ok( + output.would_archive.phases.includes('01-foundation'), + 'should list phase dir for archive', + ); + assert.ok( + Array.isArray(output.accomplishments) && output.accomplishments.includes('Set up project infrastructure'), + 'dry-run preview should surface accomplishments from phase SUMMARY one-liners (#2118)', + ); + + // CRITICAL: no mutations occurred + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'ROADMAP.md')), + 'ROADMAP.md must NOT be archived (dry-run)', + ); + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md')), + 'REQUIREMENTS.md must NOT be archived (dry-run)', + ); + assert.ok( + fs.existsSync(phasePath), + 'phase directory must NOT be moved (dry-run)', + ); + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')), + 'MILESTONES.md must NOT be created (dry-run)', + ); + assert.strictEqual( + fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'), + stateBefore, + 'STATE.md must be unchanged (dry-run)', + ); + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'milestones')), + 'archive dir must NOT be created (dry-run) — platformEnsureDir must be gated', + ); + }); + + test('#2118 — --dry-run --no-archive-phases omits phase list from preview', () => { + writeRoadmap(tmpDir, `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`); + writeState(tmpDir); + mkPhaseDir(tmpDir, '01-foundation'); + + const result = runGsdTools('milestone complete v1.0 --dry-run --no-archive-phases', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.dry_run, true); + assert.deepStrictEqual(output.would_archive.phases, [], 'phases list must be empty with --no-archive-phases'); + }); + + test('#2118 — --dry-run --force bypasses the unstarted-phases guard', () => { + writeRoadmap(tmpDir, `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`); + writeState(tmpDir, 'milestone: v1.0\n'); + // No phase directory for Phase 1 — guard would block without --force + + // Capture pre-state + const stateBefore = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + + const result = runGsdTools('milestone complete v1.0 --dry-run --force', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.dry_run, true, 'preview should run past the guard with --force'); + + // CRITICAL: --force must still be a zero-mutation dry-run preview + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'milestones')), + 'archive dir must NOT be created (dry-run --force) — platformEnsureDir must be gated', + ); + assert.strictEqual( + fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'), + stateBefore, + 'STATE.md must be unchanged (dry-run --force)', + ); + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')), + 'MILESTONES.md must NOT be created (dry-run --force)', + ); + }); + + test('#2118 — --dry-run --raw emits structured preview JSON, not the literal "dry-run" string', () => { + writeRoadmap(tmpDir, `# Roadmap v1.0 MVP\n\n### Phase 1: Foundation\n**Goal:** Setup\n`); + writeState(tmpDir); + mkPhaseDir(tmpDir, '01-foundation', { oneLiner: 'Set up project infrastructure' }); + + const result = runGsdTools(['milestone', 'complete', 'v1.0', '--name', 'Test', '--dry-run', '--raw'], tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + // Before the fix, output(dryRunResult, raw, 'dry-run') meant --raw discarded + // the structured payload and printed only the literal string "dry-run", + // which is not parseable JSON. + let output; + assert.doesNotThrow( + () => { output = JSON.parse(result.output); }, + `--dry-run --raw output must be parseable JSON, not the literal "dry-run" string; got ${JSON.stringify(result.output)}`, + ); + assert.strictEqual(output.dry_run, true, 'dry_run must be true, not the literal string "dry-run"'); + assert.strictEqual(output.version, 'v1.0'); + assert.ok( + Array.isArray(output.accomplishments) && output.accomplishments.includes('Set up project infrastructure'), + 'structured preview surviving --raw should include accomplishments', + ); + }); + test('prepends to existing MILESTONES.md (reverse chronological)', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'MILESTONES.md'),