Files
msd-core/tests/bug-3446-resume-continue-here-discovery.test.cjs
Fernando Castillo b01089c05a fix(#3689): use find instead of chained ls for continue-here scan (#3693)
* 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.
2026-05-18 12:42:43 -04:00

112 lines
4.2 KiB
JavaScript

// allow-test-rule: source-text-is-the-product
// Workflow `.md` files are the runtime contract executed by Claude Code as
// embedded bash. This test extracts the actual `check_incomplete_work` bash
// block from resume-project.md and exercises it against a planted directory
// layout — that's a behavioral integration test of the workflow contract,
// not regex-on-source.
'use strict';
const { test, describe, 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 WORKFLOW_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'resume-project.md');
// Extract the first ```bash``` code block inside the
// `<step name="check_incomplete_work">` element. That's the snippet the
// runtime actually executes; it's what we want to validate.
function extractCheckBlock() {
const md = fs.readFileSync(WORKFLOW_PATH, 'utf8');
const stepStart = md.indexOf('<step name="check_incomplete_work">');
assert.ok(stepStart >= 0, 'resume-project.md must contain a check_incomplete_work step');
const stepEnd = md.indexOf('</step>', stepStart);
assert.ok(
stepEnd >= 0,
'check_incomplete_work step must have a closing </step> tag',
);
const stepBody = md.slice(stepStart, stepEnd);
const fenceMatch = stepBody.match(/```(?:bash|sh)\r?\n([\s\S]*?)\r?\n```/);
assert.ok(fenceMatch, 'check_incomplete_work step must embed a ```bash code block');
return fenceMatch[1];
}
function runSnippet(cwd, snippet) {
// has_interrupted_agent is a downstream-orchestrator variable; default it
// to "false" so the embedded `if` branch is a no-op during this test.
return spawnSync('bash', ['-c', snippet], {
cwd,
encoding: 'utf8',
env: { ...process.env, has_interrupted_agent: 'false', interrupted_agent_id: '' },
});
}
describe('bug #3446: resume-project detects non-phase and legacy continue-here handoffs', () => {
let tmpDir;
let snippet;
before(() => {
snippet = extractCheckBlock();
tmpDir = createTempDir('gsd-bug-3446-');
// Plant the three discovery surfaces that bug #3446 was originally
// filed to cover.
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', '.continue-here.md'),
'---\ncontext: default\n---\nroot-of-.planning handoff\n',
'utf8',
);
fs.mkdirSync(path.join(tmpDir, '.planning', 'sketches', 'SKETCH-001'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'sketches', 'SKETCH-001', '.continue-here.md'),
'---\ncontext: sketch\n---\nsketch handoff\n',
'utf8',
);
fs.writeFileSync(
path.join(tmpDir, '.continue-here.md'),
'---\ncontext: legacy\n---\nlegacy repo-root handoff\n',
'utf8',
);
});
after(() => {
cleanup(tmpDir);
});
test('check_incomplete_work surfaces .planning/.continue-here.md (depth 1 under .planning)', () => {
const result = runSnippet(tmpDir, snippet);
assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`);
assert.match(
result.stdout,
/\.planning\/\.continue-here\.md/,
`expected .planning/.continue-here.md in stdout; got: ${JSON.stringify(result.stdout)}`,
);
});
test('check_incomplete_work surfaces .planning/sketches/SKETCH-001/.continue-here.md (depth 3 under .planning)', () => {
const result = runSnippet(tmpDir, snippet);
assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`);
assert.match(
result.stdout,
/\.planning\/sketches\/SKETCH-001\/\.continue-here\.md/,
`expected sketch handoff in stdout; got: ${JSON.stringify(result.stdout)}`,
);
});
test('check_incomplete_work surfaces legacy repo-root .continue-here.md', () => {
const result = runSnippet(tmpDir, snippet);
assert.equal(result.status, 0, `snippet exited ${result.status}; stderr=${result.stderr}`);
assert.match(
result.stdout,
/(^|\n)\.\/\.continue-here\.md(\n|$)/,
`expected legacy ./.continue-here.md in stdout; got: ${JSON.stringify(result.stdout)}`,
);
});
});