* 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>
118 lines
4.9 KiB
JavaScript
118 lines
4.9 KiB
JavaScript
/**
|
|
* Regression tests for #2838: SUMMARY rescue silently fails when .planning/
|
|
* is gitignored.
|
|
*
|
|
* After #3797: execute-phase.md and quick.md delegate worktree cleanup to the
|
|
* SDK's worktree.cleanup-wave command. The SDK's executeWorktreeWaveCleanupPlan
|
|
* handles SUMMARY rescue internally using a filesystem-level find+cp approach
|
|
* (bypassing gitignore) rather than the old git ls-files --exclude-standard
|
|
* form that silently dropped gitignored files.
|
|
*
|
|
* The inline "Safety net" shell rescue block that was previously in both
|
|
* workflow files has been removed — it was part of the SDK-absence fallback
|
|
* which is now dead code since preflight exits if neither local nor global SDK
|
|
* is available.
|
|
*
|
|
* This test file verifies that both workflows correctly delegate to the SDK
|
|
* for SUMMARY rescue, and that neither workflow retains the broken inline form.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
const EXECUTE_PHASE_PATH = path.join(REPO_ROOT, 'get-shit-done', 'workflows', 'execute-phase.md');
|
|
const QUICK_PATH = path.join(REPO_ROOT, 'get-shit-done', 'workflows', 'quick.md');
|
|
|
|
/**
|
|
* Parse a workflow markdown file into a structured contract object.
|
|
* Returns typed boolean fields so tests assert on structure, not raw text.
|
|
*/
|
|
function parseWorkflowContract(filePath) {
|
|
const lines = fs.readFileSync(filePath, 'utf-8').split(/\r?\n/);
|
|
return {
|
|
// Non-empty check
|
|
nonEmpty: lines.length > 0 && lines.some(l => l.length > 0),
|
|
// Does the workflow delegate SUMMARY rescue to worktree.cleanup-wave?
|
|
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 still contain the broken ls-files --exclude-standard rescue form?
|
|
hasBrokenLsFilesForm: lines.some(
|
|
l => l.includes('ls-files --modified --others --exclude-standard'),
|
|
),
|
|
};
|
|
}
|
|
|
|
const executePhaseContract = parseWorkflowContract(EXECUTE_PHASE_PATH);
|
|
const quickContract = parseWorkflowContract(QUICK_PATH);
|
|
|
|
describe('bug-2838: SUMMARY rescue delegates to SDK (worktree.cleanup-wave)', () => {
|
|
|
|
test('execute-phase.md is readable', () => {
|
|
assert.ok(executePhaseContract.nonEmpty, 'execute-phase.md must not be empty');
|
|
});
|
|
|
|
test('quick.md is readable', () => {
|
|
assert.ok(quickContract.nonEmpty, 'quick.md must not be empty');
|
|
});
|
|
|
|
test('execute-phase.md delegates SUMMARY rescue to SDK (worktree.cleanup-wave)', () => {
|
|
// After #3797: worktree.cleanup-wave handles SUMMARY rescue via find+cp
|
|
// (bypasses gitignore, fixing the #2838 bug). The workflow delegates to the
|
|
// SDK rather than implementing rescue inline.
|
|
assert.ok(
|
|
executePhaseContract.delegatesToCleanupWave,
|
|
'execute-phase.md must delegate to worktree.cleanup-wave for SUMMARY rescue (#2838/#3797)',
|
|
);
|
|
});
|
|
|
|
test('quick.md delegates SUMMARY rescue to SDK (worktree.cleanup-wave)', () => {
|
|
// After #3797: worktree.cleanup-wave handles SUMMARY rescue via find+cp
|
|
// (bypasses gitignore, fixing the #2838 bug).
|
|
assert.ok(
|
|
quickContract.delegatesToCleanupWave,
|
|
'quick.md must delegate to worktree.cleanup-wave for SUMMARY rescue (#2838/#3797)',
|
|
);
|
|
});
|
|
|
|
test('execute-phase.md does not retain broken git ls-files --exclude-standard rescue form (#2838)', () => {
|
|
// The broken form used --exclude-standard which silently filtered out
|
|
// gitignored .planning/ files — the root cause of #2838.
|
|
assert.ok(
|
|
!executePhaseContract.hasBrokenLsFilesForm,
|
|
'execute-phase.md must not use ls-files --exclude-standard for SUMMARY rescue (broken for gitignored .planning/)',
|
|
);
|
|
});
|
|
|
|
test('quick.md does not retain broken git ls-files --exclude-standard rescue form (#2838)', () => {
|
|
assert.ok(
|
|
!quickContract.hasBrokenLsFilesForm,
|
|
'quick.md must not use ls-files --exclude-standard for SUMMARY rescue (broken for gitignored .planning/)',
|
|
);
|
|
});
|
|
|
|
test('execute-phase.md cleanup-wave uses || exit 1 (fail-closed so rescue errors surface)', () => {
|
|
// If the SDK's rescue fails (e.g. filesystem error), || exit 1 surfaces
|
|
// the failure to the orchestrator rather than silently continuing and
|
|
// losing the SUMMARY.
|
|
assert.ok(
|
|
executePhaseContract.cleanupWaveFailClosed,
|
|
'execute-phase.md cleanup-wave must use || exit 1 so SUMMARY rescue failures surface (#2838/#3797)',
|
|
);
|
|
});
|
|
|
|
test('quick.md cleanup-wave uses || exit 1 (fail-closed so rescue errors surface)', () => {
|
|
assert.ok(
|
|
quickContract.cleanupWaveFailClosed,
|
|
'quick.md cleanup-wave must use || exit 1 so SUMMARY rescue failures surface (#2838/#3797)',
|
|
);
|
|
});
|
|
});
|