* fix(#3689): use find instead of chained ls for continue-here scan The check_incomplete_work step in resume-project.md chained six bare-glob ls arguments to discover .planning/.continue-here*.md handoff files. Under zsh's default NOMATCH option (macOS default shell), the first non-matching glob aborts the entire command during word-expansion, silently dropping every pattern after it — including .planning/.continue-here*.md, which holds the canonical pause checkpoint for default-context handoffs. The 2>/dev/null || true guard suppresses ls's own stderr / exit code but has no effect on the shell's pre-exec glob-abort. Replace the chain with two find invocations: find .planning -maxdepth 3 -name '.continue-here*.md' -print 2>/dev/null find . -maxdepth 1 -name '.continue-here*.md' -print 2>/dev/null find does not use shell glob expansion and tolerates absent directories on both bash and zsh. Adds tests/bug-3689-resume-glob-nomatch.test.cjs covering: - zsh -o nomatch with .planning/.continue-here-AT-1234.md present and no spike/sketch/deliberation subdirs (the regression scenario) - bash default, same layout - zsh -o nomatch with no checkpoints anywhere (clean exit, no output) - text invariant: resume-project.md no longer carries the chained-ls pattern and does carry the find-based scan Closes #3689 * fix(#3689): guard find commands with || true for Windows safety Restore the trailing '|| true' guard that the original chained ls had, per the windows-robustness.test.cjs invariant (informational bash commands in critical workflows must not let an exit-1 from find on exotic platforms tank the resume-project.md flow). * fix(#3689): address CodeRabbit review feedback - Set changeset frontmatter pr: 3693 (was 3690 placeholder); the release- note link now points at the right PR. - Use createTempDir / cleanup from tests/helpers.cjs instead of local tmpdir/cleanup wrappers, per repo test standards. * fix(#3689): update bug-3446 discovery contract to new find-based scan bug-3446 enforced three text invariants on the chained-ls implementation that this PR replaces with find. Update the assertions to verify the same three discovery paths are still covered: - .planning/.continue-here*.md at depth 1 -> find .planning -maxdepth 3 - .planning/sketches/SKETCH-NNN/.continue-here*.md at depth 3 -> same find with -maxdepth >= 3 (assertion now reads the actual depth and enforces a lower bound so future changes can deepen but not shallow it) - repo-root .continue-here*.md legacy fallback -> find . -maxdepth 1 The discovery contract is preserved; only the implementation under inspection changes. * test(#3689): convert bug-3446 from source-grep to behavioral assertions CodeRabbit flagged the text-regex assertions on the workflow source as a violation of the no-source-grep testing standard. Replace them with a behavioral integration test that: 1. Extracts the actual check_incomplete_work bash block from resume-project.md (so the test stays in sync with whatever the workflow does, no string match required). 2. Plants three handoff files in a temp dir covering the three discovery surfaces bug #3446 originally filed: - .planning/.continue-here.md (depth 1 under .planning) - .planning/sketches/SKETCH-001/.continue-here.md (depth 3) - ./.continue-here.md (legacy repo root) 3. Runs the snippet under bash and asserts each planted file appears in stdout. Same contract; now validated through runtime behavior instead of regex-on-file. * fix(#3689): use \\r?\\n in bash-fence regex for Windows CRLF parity The Windows test-parity guard at tests/windows-test-parity-guard.test.cjs:106 flags any new fence-extraction regex that uses a literal \\n after the language tag — on Windows CRLF the byte after `bash` is \\r, the regex silently fails to match, and the extracted snippet is empty. Switch to the canonical /```(?:bash|sh)\\r?\\n([\\s\\S]*?)```/ pattern. * fix(#3689): harden extractCheckBlock against missing </step> and CRLF closing fence Per CodeRabbit review: assert that the closing </step> tag is present before slicing (otherwise slice(stepStart, -1) silently grabs the wrong block and the test fails misleadingly), and add \r?\n to the closing fence as well so Windows CRLF doesn't sneak a stray carriage return into the captured snippet.
143 lines
5.5 KiB
JavaScript
143 lines
5.5 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// Workflow `.md` files are the runtime contract executed by Claude Code as
|
|
// embedded bash. Asserting on the staged text of resume-project.md and on the
|
|
// behavior of the embedded snippet under real shells is a behavioral test of
|
|
// the workflow itself, not source-grep theater.
|
|
|
|
/**
|
|
* Regression for #3689 — /gsd-resume-work silently drops
|
|
* `.planning/.continue-here*.md` checkpoints under zsh's default NOMATCH.
|
|
*
|
|
* Root cause: the `check_incomplete_work` step in
|
|
* `get-shit-done/workflows/resume-project.md` used a chained `ls` with six
|
|
* bare-glob arguments. Under zsh's default `NOMATCH` setopt the first
|
|
* non-matching glob aborts the entire command during word-expansion — every
|
|
* pattern after that point is never evaluated, including the one that holds
|
|
* valid pause checkpoints (`.planning/.continue-here*.md`). `2>/dev/null ||
|
|
* true` only suppresses ls's own stderr / exit code; it has no effect on the
|
|
* shell's pre-exec abort.
|
|
*
|
|
* Fix: replace the chained `ls` with two `find` calls. `find` does not use
|
|
* shell glob expansion, and `find <missing-dir> -maxdepth N -name PATTERN
|
|
* -print 2>/dev/null` tolerates absent directories on both bash and zsh.
|
|
*
|
|
* This test covers:
|
|
* 1. zsh under `-o nomatch`: checkpoint at `.planning/.continue-here-*.md`
|
|
* is listed even when `.planning/spikes`, `.planning/sketches`,
|
|
* `.planning/deliberations` are absent (the common new-project layout).
|
|
* 2. bash default: same behavior.
|
|
* 3. zsh `-o nomatch` with no `.continue-here` files anywhere: exits 0,
|
|
* no output, no error.
|
|
* 4. Text invariant: resume-project.md no longer carries the brittle
|
|
* chained-ls pattern.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
const WORKFLOW_PATH = path.join(REPO_ROOT, 'get-shit-done', 'workflows', 'resume-project.md');
|
|
|
|
// The exact snippet the workflow now embeds. Keep in sync with
|
|
// resume-project.md `check_incomplete_work` step.
|
|
const FIND_SNIPPET = [
|
|
"find .planning -maxdepth 3 -name '.continue-here*.md' -print 2>/dev/null || true",
|
|
"find . -maxdepth 1 -name '.continue-here*.md' -print 2>/dev/null || true",
|
|
].join('\n');
|
|
|
|
function hasShell(name) {
|
|
const result = spawnSync('which', [name], { encoding: 'utf8' });
|
|
return result.status === 0 && result.stdout.trim().length > 0;
|
|
}
|
|
|
|
describe('bug #3689 — resume-project.md continue-here scan under zsh NOMATCH', () => {
|
|
let tmpDir;
|
|
|
|
before(() => {
|
|
tmpDir = createTempDir('gsd-bug-3689-');
|
|
// Reproduce the common new-project layout: a `.planning/` with a
|
|
// suffixed continue-here file and *no* spike / sketch / deliberation
|
|
// subdirectories.
|
|
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, '.planning', '.continue-here-AT-1234.md'),
|
|
'---\ncontext: default\n---\nhandoff body\n',
|
|
'utf8',
|
|
);
|
|
});
|
|
|
|
after(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('zsh -o nomatch lists the .planning/.continue-here-* checkpoint', { skip: !hasShell('zsh') }, () => {
|
|
const result = spawnSync('zsh', ['-o', 'nomatch', '-c', FIND_SNIPPET], {
|
|
cwd: tmpDir,
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 0, `zsh exited ${result.status}; stderr=${result.stderr}`);
|
|
assert.match(
|
|
result.stdout,
|
|
/\.planning\/\.continue-here-AT-1234\.md/,
|
|
`expected checkpoint in stdout, got: ${JSON.stringify(result.stdout)}`,
|
|
);
|
|
});
|
|
|
|
test('bash default lists the .planning/.continue-here-* checkpoint', { skip: !hasShell('bash') }, () => {
|
|
const result = spawnSync('bash', ['-c', FIND_SNIPPET], {
|
|
cwd: tmpDir,
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 0, `bash exited ${result.status}; stderr=${result.stderr}`);
|
|
assert.match(
|
|
result.stdout,
|
|
/\.planning\/\.continue-here-AT-1234\.md/,
|
|
`expected checkpoint in stdout, got: ${JSON.stringify(result.stdout)}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('bug #3689 — empty workspace exits cleanly', () => {
|
|
let tmpDir;
|
|
|
|
before(() => {
|
|
tmpDir = createTempDir('gsd-bug-3689-');
|
|
// No .planning/ at all, no .continue-here files. Pure greenfield.
|
|
});
|
|
|
|
after(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('zsh -o nomatch with no checkpoints exits 0, empty output', { skip: !hasShell('zsh') }, () => {
|
|
const result = spawnSync('zsh', ['-o', 'nomatch', '-c', FIND_SNIPPET], {
|
|
cwd: tmpDir,
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 0, `zsh exited ${result.status}; stderr=${result.stderr}`);
|
|
assert.equal(result.stdout.trim(), '', `expected no stdout, got: ${JSON.stringify(result.stdout)}`);
|
|
});
|
|
});
|
|
|
|
describe('bug #3689 — workflow text invariant', () => {
|
|
test('resume-project.md no longer chains bare globs through ls', () => {
|
|
const body = fs.readFileSync(WORKFLOW_PATH, 'utf8');
|
|
assert.doesNotMatch(
|
|
body,
|
|
/ls\s+\.planning\/spikes\/\*\/\.continue-here/,
|
|
'resume-project.md still contains the chained `ls .planning/spikes/*/.continue-here*.md` pattern that aborts under zsh NOMATCH; the find-based scan should replace it.',
|
|
);
|
|
assert.match(
|
|
body,
|
|
/find \.planning -maxdepth 3 -name '\.continue-here\*\.md'/,
|
|
'resume-project.md must use the find-based scan introduced by the #3689 fix.',
|
|
);
|
|
});
|
|
});
|