Files
msd-core/tests/bug-2384-post-merge-deletion-audit.test.cjs
Tom Boucher 2b9fa009d3 fix(#373): replace unquoted $GSD_SDK with space-safe gsd_run launcher (#379)
* fix(#373): replace unquoted $GSD_SDK with space-safe gsd_run launcher

Workflow bash blocks resolved the runtime as GSD_SDK="node $GSD_TOOLS" and
invoked it unquoted ($GSD_SDK query ...). On install paths containing spaces
(e.g. /Volumes/Mini Me/...) the unquoted expansion word-split into
`node /Volumes/Mini gsd-tools.cjs ...`, failing with "Cannot find module
'/Volumes/Mini'" and getting masked by `2>/dev/null || echo "{}"` into a
silent empty state.

Replace the string variable with a single-line shell launcher that defines a
gsd_run function, invokes the runtime with a fully-quoted path and "$@", and
preserves the local-cjs / installed-gsd-tools-on-PATH fallback (#3668) plus
the loud not-found error and install hint. The launcher uses _GSD_SHIM_NAME
indirection so no workflow emits the /gsd-tools substring that the do.md
dispatcher-parity scanner would misread, and is single-line to stay within
the per-file progressive-disclosure line budgets (#2551).

The canonical launcher lives in
get-shit-done/workflows/_runtime-launcher.snippet.sh, is propagated once per
file by scripts/sync-runtime-launcher.cjs, and is locked by
tests/runtime-launcher-parity.test.cjs (fails CI on drift, on a reappearing
$GSD_SDK token, or on a /gsd-tools substring). Dependent workflow-assertion
tests are updated from $GSD_SDK to gsd_run, and the runtime launcher is
registered in CONTEXT.md.

Fixes #373

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(#373): add changeset fragment and allow-test-rule for parity guard

The runtime-launcher parity test is a structural drift guard that reads
workflow markdown to assert the canonical launcher is present and the
retired $GSD_SDK / /gsd-tools tokens are absent; annotate it with
allow-test-rule per the no-source-grep lint escape hatch. Add the required
.changeset fragment for this user-facing fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(#373): make parity PATH-fallback assertion cross-platform (Windows)

Subtest (E) compared GSD_TOOLS against the Node-side absolute temp path,
but the value originates from git-bash which reports the POSIX form, so the
prefix comparison failed on windows-latest while the launcher itself worked
(the installed stub was invoked). Assert the resolved binary by normalized
suffix (/bin/gsd-tools, not .cjs) instead of the absolute prefix; the
behavioral stub-invocation assertion is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:21:05 -04:00

80 lines
3.4 KiB
JavaScript

'use strict';
/**
* Regression test for #2384.
*
* During execute-phase, the orchestrator merges per-plan worktree branches into
* main. The pre-merge deletion check (git diff --diff-filter=D HEAD...WT_BRANCH)
* only catches files deleted on the worktree branch. A post-merge audit is also
* required to catch deletions that made it into the merge commit (e.g., files
* that were in the common ancestor but deleted by the merged worktree) and to
* provide a revert safety net.
*
* After #3797: execute-phase.md delegates worktree cleanup to the SDK's
* worktree.cleanup-wave command, which implements pre-merge deletion checks
* (diff --diff-filter=D) internally via executeWorktreeWaveCleanupPlan.
* The manual post-merge shell audit (MERGE_DEL_COUNT, git reset --hard) has
* been removed from the workflow — it was part of the SDK-absence fallback.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const EXECUTE_PHASE = path.join(
__dirname, '..', 'get-shit-done', 'workflows', 'execute-phase.md'
);
/**
* Parse execute-phase.md into a structured contract object.
* Returns typed boolean fields so tests can assert on structure
* rather than raw text.
*/
function parseExecutePhaseContract(filePath) {
const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
return {
// Does the workflow call the worktree.cleanup-wave SDK command?
delegatesToCleanupWave: lines.some(l => l.includes('worktree.cleanup-wave')),
// Does the cleanup-wave invocation use || exit 1 (fail-closed)?
cleanupWaveFailClosed: lines.some(
l => /gsd_run query worktree\.cleanup-wave.*\|\| exit 1/.test(l),
),
// Does the workflow export/reference WAVE_WORKTREE_MANIFEST for the SDK?
passesWaveManifest: lines.some(l => l.includes('WAVE_WORKTREE_MANIFEST')),
};
}
describe('execute-phase.md — post-merge deletion audit (#2384)', () => {
const contract = parseExecutePhaseContract(EXECUTE_PHASE);
test('execute-phase delegates to worktree.cleanup-wave (which handles deletion audit)', () => {
// After #3797: worktree.cleanup-wave in worktree-safety.cjs performs
// diff --diff-filter=D checks (blocks branches with deletions) before merge.
// The workflow delegates to the SDK rather than duplicating the check inline.
assert.ok(
contract.delegatesToCleanupWave,
'execute-phase.md must delegate to gsd_run query worktree.cleanup-wave (#2384/#3797)',
);
});
test('execute-phase cleanup-wave uses || exit 1 (fail-closed for blocked deletions)', () => {
// If worktree.cleanup-wave detects deletions, it exits 1 (blocked).
// The || exit 1 in the workflow propagates that refusal rather than swallowing it.
assert.ok(
contract.cleanupWaveFailClosed,
'execute-phase.md must use || exit 1 so deletion-blocked cleanups surface to the orchestrator',
);
});
test('execute-phase still has pre-merge deletion check (via guard before worktree.cleanup-wave)', () => {
// The primary deletion guard is now in worktree-safety.cjs (SDK).
// The workflow must still enforce WAVE_WORKTREE_MANIFEST so the SDK
// has the info it needs to validate branches.
assert.ok(
contract.passesWaveManifest,
'execute-phase.md must pass WAVE_WORKTREE_MANIFEST to worktree.cleanup-wave',
);
});
});