Commit Graph

10 Commits

Author SHA1 Message Date
Tom Boucher
4698b3e349 fix(#1051): force-exit + per-chunk timeout for the windows full-test lane; close leaked test handles (#1054)
The `full test (windows-latest, 22)` job intermittently got CANCELLED at its
20m wall-clock cap with no failed test step — a false-negative gate (recurrence
of #869). Root cause: a unit test leaves an open event-loop handle, so the
chunk's `node --test` child hangs ~150s on Windows after its last test prints;
two such stalls push the already-~13m job past 20m.

Fix (defense in depth):
- run-tests.cjs: pass --test-force-exit (Node >=22; engines requires >=22.0.0)
  so the runner exits once all tests finish regardless of lingering handles —
  the durable backstop. Account for the flag in the argv-length ceiling.
- run-tests.cjs: add a per-chunk execFileSync timeout (default 600000ms, env
  RUN_TESTS_CHUNK_TIMEOUT_MS) that fails loudly with a diagnostic naming the
  chunk's files, so a hung chunk can never silently eat the job budget.
- perf-316 test: terminate both Worker threads on all paths (afterEach +
  finally) so they cannot outlive the test.
- locking-bugs test: kill spawned children in a finally that wraps the whole
  spawn -> waitFor -> barrier-release -> Promise.all sequence, so a barrier
  timeout no longer leaks live child processes.
- Refresh the stale synckit comment (synckit/SDK bridge was removed).

Regression tests in run-tests-harness: a hung chunk hits the per-chunk timeout
and fails with a clear message; force-exit lets a chunk with a leaked handle
exit cleanly.

Closes #1051

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:11:50 -04:00
Tom Boucher
b4e817cfa0 chore(lint): refactor magic-sleep tests to async waits + ratchet rules to error (#735)
Replace raw setTimeout/Atomics.wait synchronization sleeps in 4 test files
with a shared async delay()/waitFor() poll-for-condition helper in
tests/helpers.cjs, then flip local/no-magic-sleep-in-tests and
no-restricted-syntax from warn to error so the debt can't regrow.

- tests/helpers.cjs: add delay(ms) + waitFor(predicate, opts), exported
- bug-1974: setTimeout backoff -> await delay()
- config.test: drop Atomics.wait sleep(); async retry via await delay()
- graphify: waitForBuildStatus/cleanupHookRepo async via await delay()
- locking-bugs: 3 Atomics.wait poll loops -> await waitFor()
- eslint.config.mjs: ratchet both rules warn -> error

Refs #733

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:04:38 -04:00
Tom Boucher
ba231ecbfc chore: clean up clear-cut ESLint warnings (#732) (#734)
Pay down pre-existing error→warn lint debt. Removes dead imports/vars, unused functions, redundant regex/string escapes, and stale eslint-disable directives; converts unused `catch (_e)` to optional catch binding (src/*.cts).

No behavior change. Lint 345→125 warnings (0 errors); deferred categories (n/no-process-exit, test-sleeps, control-regex) tracked in #732 for follow-up. Full test suite green (0 failures); code-review verified all removals unused and all escape fixes semantics-preserving.

Closes #732

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:24:48 -04:00
Tom Boucher
bf68ad4d93 fix(tests): deterministic concurrency via injectable clock seam; delete flaky racing tests (#459)
* feat(#453): add deterministic clock seam to lock modules

Introduces get-shit-done/bin/lib/clock.cjs exporting realClock with
now() (Date.now) and sleep() (Atomics.wait). acquireStateLock,
writeStateMd, and readModifyWriteStateMd in state.cjs each accept an
optional trailing clock param (default: realClock). withPlanningLock in
planning-workspace.cjs gains the same seam. No production behavior
change — all callers that omit the param continue to use realClock.

Adds tests/helpers/clock.cjs (makeFakeClock) and tests/clock-seam.test.cjs
with 20 deterministic in-process tests covering: lock serialization,
timeout throw at maxWaitMs boundary, stale-lock takeover, lock released
on error path, withPlanningLock timeout recovery, exit-cleanup integration,
readModifyWriteStateMd call-site coverage (7 cmd*), and roadmap analyze
behavioral assertion (50 phases, no elapsed-time gate).

Deletes/converts per research verdicts: removes 11 source-grep/elapsed-time/
non-deterministic-concurrent tests across concurrency-safety.test.cjs,
locking-bugs-1909-1916-1925-1927.test.cjs, and bug-1974-context-exhaustion-
record.test.cjs. All deleted tests have deterministic replacements in
clock-seam.test.cjs or surviving barrier-based tests.

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

* fix(#453): update module inventory for clock.cjs; make EEXIST-retry assertion behavioral

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

* fix(#453): satisfy lint-tests — allow-test-rule annotation on readFileSync/includes runtime output check

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

---------

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:18:10 -04:00
Tom Boucher
ea67479bfb fix(3496): include all version patterns in changelog extraction (#90)
* fix(3496): include all version patterns in changelog extraction

parseChangelog now handles multi-line bullets (continuation lines
starting with two or more spaces) where the (#NNNN) PR trailer
appears on a continuation line, not the opening dash line. The
previous single-line regex silently dropped every such bullet,
causing Feature/Enhancement sections to return 0 entries.

Also adds an `extract` subcommand to scripts/changeset/cli.cjs:
  changeset/cli.cjs extract --from VERSION --to VERSION [--changelog FILE] [--json]
Extracts releases strictly after --from (exclusive) and up to and
including --to (inclusive). Accepts v-prefixed versions. Exits 2
when no releases fall in range, giving /gsd:update a deterministic
range-aware helper instead of vague/manual extraction.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(3496): use production parseChangelog in markdown-mode assertion

Replace raw stdout.includes() in the emits-markdown test with a
parseChangelog call on the output so the assertion targets version
strings via the production parser rather than a raw substring match.
Eliminates the output-grep anti-pattern flagged by test-rigor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(changeset): add fragment for fix #3796 (issue #3496)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(3496): reject malformed --from/--to semver in extract with structured error

`parseSemver` coerced non-numeric components to 0 (e.g. `1.41.x` → `1.41.0`),
making range selection silently wrong under typos or version-shape drift.

Add a strict N.N.N validation gate before comparison; exit 1 with a JSON
error report when either bound fails.  Add two regression tests covering
alphabetic and dotted-letter inputs.

Codex adversarial review finding: high severity (scripts/changeset/cli.cjs:226-244)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(3496): preserve bullets without PR trailer in parseChangelog (pr: null)

Previously flushBullet() silently discarded any bullet that lacked a
trailing (# NNNN) token.  On the real CHANGELOG.md this dropped 7 entries
from v1.41.0 alone, so cmdExtract returned incomplete release notes to the
/gsd:update confirmation step.

Store PR-less bullets as { body, pr: null } instead.  Update cmdExtract's
textOutput renderer to emit `- body` (no trailer) for null-pr bullets.

Add regression tests:
  - serialize: preserves bullets without trailer as pr:null (not dropped)
  - cli extract: preserves PR-less and PR bullets together in extracted JSON
  - cli extract: rejects malformed --from/--to (1.41.x, foo) with exit 1

Codex adversarial review finding: high severity (scripts/changeset/serialize.cjs:64-73)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(3496): wire extract into update.md + reject pre-release in range, fix CHANGELOG parser edge cases

BLOCKER fixes:
- F1: workflows/update.md show_changes_and_confirm step now invokes
  `scripts/changeset/cli.cjs extract --from $INSTALLED_VERSION --to
  $LATEST_VERSION --changelog $CHANGELOG_TMP --json` with explicit exit-2
  handling ("no releases in range") and fallback text.  The prior prose
  ("extract entries between versions") was never wired to the binary and
  silently skipped intermediate versions (#3496).
- F2: releases.filter in cmdExtract now rejects any rel.version that does
  not pass SEMVER_RE before numeric-tuple comparison.  parseSemver('1.0.0-rc.1')
  previously returned [1,0,0] (same as '1.0.0'), causing pre-release entries to
  corrupt range queries.  Architectural choice: skip pre-release + 4-part
  versions with a stderr warning; full semver §11 pre-release ordering deferred
  to a consolidation issue (see F8 note below).

MAJOR fixes:
- F3 (serialize.cjs): releaseMatch regex updated to
  /^##\s+\[([^\]]+)\](?:\([^)]*\))?\s*(?:-\s*(\S+))?/ so linked-header
  format `## [1.42.1](url) - 2026-05-15` captures the date correctly.
- F4 (serialize.cjs): continuation-line test now checks `!/^\s+-\s/`
  so `  - nested item` terminates the current bullet instead of folding in.
- F5 (cli.cjs): 4-part versions (e.g. 1.0.0.1) fail SEMVER_RE and are
  skipped by the same guard added for F2.  No separate code path needed.
- F6 (serialize.cjs): v-prefix stripped from in-file version capture;
  `## [v1.0.0]` now parses as version "1.0.0".
- F7 (serialize.cjs): continuation-line indentation relaxed from /^[ \t]{2}/
  to /^\s+/ so 1-space-indented continuations fold correctly (F4's
  bullet-terminator guard prevents nested bullets from being folded).

MINOR fixes:
- F9 (cli.cjs): unknown-command path now exits 1 instead of 2 (exit 2
  is reserved for "no releases in range" semantic).
- F10 (cli.cjs): text-mode exit-2 path now writes
  "no releases found in range (from=X, to=Y)" to stderr.
- F11 (tests): exit-2 test now asserts r.json is present and
  r.json.releases.length === 0.

NOTE — F8 (semver consolidation): this codebase has 5+ distinct semver
comparators with divergent pre-release policies; this PR adds a 6th (the
SEMVER_RE guard in cmdExtract).  A follow-up consolidation issue should be
filed to unify all call sites.  Out of scope for this PR.

Regression tests added for F1–F6: pre-release exclusion, linked-header
date parsing, nested-bullet termination, 4-part/v-prefix edge cases, and
workflow wiring.  All 15 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(lint): add allow-test-rule annotation to F1 workflow wiring test

The F1 test reads get-shit-done/workflows/update.md (a product markdown
file, not CJS source) to assert the extract subcommand invocation was
wired.  The lint-no-source-grep detector flags any readFileSync-bound
variable used with .includes() regardless of file extension; annotate
with // allow-test-rule to exempt this legitimate product-content
assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: apply deterministic barrier to locking-bugs #1927 config-set test

The 'both concurrent config-set calls persist their values' test used
Promise.all([execAsync(A), execAsync(B)]) without a barrier, which is
non-deterministic under Docker load: one subprocess can complete before
the other starts (no real contention) or both can race O_EXCL and observe
stale fs state (lost write / assertion failure).

Mirrors the locking-bugs:180 and :235 redesigns: erect a barrier file,
spawn both subprocesses, wait for both to signal readiness via ready files,
then drop the barrier simultaneously so both config-set calls genuinely
contend on withPlanningLock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:51:37 -04:00
Tom Boucher
c22e869bec test: redesign locking-bugs:180 with deterministic barrier (mirrors :235 fix) (#3790)
Previous design relied on OS scheduler to interleave two subprocess writes.
Redesigned using Atomics.wait file-barrier pattern (matches the redesign on
PR #3764 for line 235) to guarantee real concurrent lock contention every run.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:22:40 -04:00
Tom Boucher
c1c8b0d109 fix(3739): gap-checker now detects padded-prefix CONTEXT.md (#3764)
* test(3739): add RED tests for padded-prefix CONTEXT.md gap-checker miss

Covers bare and padded (01-CONTEXT.md, 02.1-CONTEXT.md) forms, an
uncovered-decision counter-test, and unit tests for the upcoming
findContextMdIn() helper. All 6 new tests fail before the fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(3739): extract findContextMdIn() helper; fix gap-checker bare lookup

gap-checker.cjs:136 used a bare path.join(absPhaseDir, 'CONTEXT.md')
that silently returned '' for any phase using the padded-prefix
convention (01-CONTEXT.md, 02.1-CONTEXT.md, etc.).

Extract findContextMdIn(absDir) to planning-workspace.cjs — the module
already imported by gap-checker, init, roadmap, and core — and wire
gap-checker.cjs to call it instead of the bare lookup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(3739): replace inline dual-form predicate with findContextMdIn() at all 4 remaining sites

The dual-form predicate `f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'`
existed verbatim at 5 sites across init.cjs (×3), roadmap.cjs, and
core.cjs — Rule of Three mandates extraction at ≥3 sites. All 4
remaining call sites now delegate to findContextMdIn() from
planning-workspace.cjs. No behaviour change; all existing tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(3739): update changeset to reference PR #3764

* fix(3739): findContextMdIn prefers bare CONTEXT.md over padded form (deterministic precedence)

`.find()` with `f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'` returned
the first match in `readdirSync` order — undefined on most filesystems.
When both `CONTEXT.md` and `01-CONTEXT.md` exist the winner was arbitrary.

The old gap-checker.cjs:136 code always used bare `CONTEXT.md` first
(existsSync on the bare path). Restore that invariant: check
`files.includes('CONTEXT.md')` before falling through to the padded scan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(3739): dual-file precedence test — bare CONTEXT.md wins over padded form

Adds two test cases for the scenario where both CONTEXT.md and
01-CONTEXT.md exist in the same phase directory:

1. Helper level: findContextMdIn() must return 'CONTEXT.md' (not the
   padded filename) when both files are present on disk.
2. Integration level: gap-analysis must resolve decisions from the bare
   form only; D-PADDED (from 01-CONTEXT.md) must not appear when the
   bare form shadows it.

Without these tests a future change to findContextMdIn could silently
regress the precedence guarantee.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(3739): bug-2798 tests skip cleanly when sdk/dist is absent (was hard-fail)

The 3 tests in bug-2798-context-window-config-key.test.cjs invoke the built
SDK CLI (sdk/dist/cli.js) and require sdk/dist/query/config-schema.js. When
dist is absent, they threw hard errors rather than observable skips.

Apply the same `if (!existsSync(...)) { t.skip(...); return; }` guard used in
bug-2767-gsd-sdk-commit-files-flag.test.cjs (c2812313). Tests 1 & 2 guard on
sdk/dist/cli.js; test 3 guards on sdk/dist/query/config-schema.js. When dist
is present all 3 run; when absent all 3 emit actionable skip lines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(3739): eliminate double readdirSync in findContextMdIn callers

findContextMdIn now accepts either a directory path or an already-read
files array, allowing callers that already hold a directory listing
(core.cjs:getPhaseFileStats, roadmap.cjs:countPhasePlansAndSummaries,
gap-checker.cjs:runGapAnalysis) to skip redundant readdirSync calls.

Test coverage added for the array-argument overload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: redesign flaky concurrent add-blocker test with deterministic barrier

Previous design relied on OS scheduler to interleave two subprocess writes,
producing a flake under CI load. Redesigned using a file-barrier (Option A)
that forces both subprocesses to reach a ready-gate before either proceeds,
guaranteeing true concurrent lock contention and eliminating timing dependency.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 22:39:34 -04:00
Tom Boucher
abb2cb63f6 refactor: extract planning-workspace seam from core.cjs (#2901)
* refactor: extract planning workspace seam from core

* docs: document planning-workspace module and inventory updates

* fix: harden planning lock timeout and preserve workstream set contract

---------

Co-authored-by: Tom Boucher <thomas.boucher@sas.com>
2026-04-30 11:38:13 -04:00
Tom Boucher
aeef87de7f docs(test-standards): enforce no-source-grep rule with CI linter + CONTRIBUTING.md (#2700)
* docs(test-standards): enforce no-source-grep rule with CI linter + update CONTRIBUTING.md

Adds scripts/lint-no-source-grep.cjs — a static linter that detects readFileSync
on .cjs source files in tests without an allow-test-rule annotation. Wires it
into CI as a new lint-tests job in test.yml and as npm run lint:tests.

Resolves all 9 existing violations across the test suite:
- Rewrites workspace routing tests (3) as behavioral runGsdTools calls that
  verify each command is router-recognized (exit != "Unknown init workflow")
- Adds allow-test-rule annotations with explanatory comments to 7 legitimate
  structural tests: architectural invariants (locking, orphan-worktree),
  structural regression guards (milestone-regex-global), docs-parity
  (config-field-docs), integration-test-input (copilot-install), and
  structural-implementation-guards (bug-1891, discuss-mode)

Updates CONTRIBUTING.md Testing Standards section with:
- "Prohibited: Source-Grep Tests" section with the before/after pattern,
  root cause analysis of why it breaks (commit 990c3e64), and CI reference
- allow-test-rule exemption table (6 recognized categories with when-to-use)
- "CI Test Quality Checks" table showing lint-tests job and local run command

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve CodeRabbit findings on PR #2700

- CONTRIBUTING.md: "four recognized categories" → "six" (table has 6 rows)
- workspace.test.cjs: use positional args in routing tests (no --name flag)
- lint-no-source-grep.cjs: add source-dir guard to READ_WITH_INLINE_CJS_RE
  (mirrors CJS_PATH_CONST_RE's protection against false positives on temp files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(lint): tighten allow-test-rule and add recursive test discovery

- ALLOW_ANNOTATION now requires at least one non-whitespace char after the
  colon so bare '// allow-test-rule:' cannot bypass the lint gate
- findTestFiles() recurses into subdirectories so nested *.test.cjs files
  are covered if the tests/ tree ever grows subdirs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:34:55 -04:00
Tom Boucher
4dd35f6b69 fix(state): correct TOCTOU races, busy-wait, lock cleanup, and config locking (#1944)
cmdStateUpdateProgress, cmdStateAddDecision, cmdStateAddBlocker,
cmdStateResolveBlocker, cmdStateRecordSession, and cmdStateBeginPhase from
bare readFileSync+writeStateMd to readModifyWriteStateMd, eliminating the
TOCTOU window where two concurrent callers read the same content and the
second write clobbers the first.

Atomics.wait(), matching the pattern already used in withPlanningLock in
core.cjs.

and core.cjs and register a process.on('exit') handler to unlink them on
process exit. The exit event fires even when process.exit(1) is called
inside a locked region, eliminating stale lock files after errors.

read-modify-write body of setConfigValue in a planning lock, preventing
concurrent config-set calls from losing each other's writes.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 17:39:29 -04:00