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>
This commit is contained in:
Tom Boucher
2026-05-29 11:18:10 -04:00
committed by GitHub
parent 4b908e6bcd
commit bf68ad4d93
11 changed files with 714 additions and 415 deletions

View File

@@ -96,41 +96,6 @@ describe('acquireStateLock: success path still returns lockPath', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────────
// C3. EEXIST error → retry semantics unchanged
// ─────────────────────────────────────────────────────────────────────────────
describe('acquireStateLock: EEXIST retry semantics unchanged', () => {
test('C3: source still handles EEXIST with retry / stale-lock removal', () => {
const src = fs.readFileSync(STATE_CJS_PATH, 'utf-8');
const fnSrc = extractAcquireStateLockSource(src);
// The EEXIST branch falls through to stale-lock detection and Atomics.wait retry.
// These must still be present after the non-EEXIST fix.
assert.ok(
fnSrc.includes('Atomics.wait'),
'acquireStateLock must still use Atomics.wait() for EEXIST retry sleep'
);
assert.ok(
fnSrc.includes('staleThresholdMs'),
'acquireStateLock must still check stale lock threshold on EEXIST'
);
assert.ok(
fnSrc.includes('maxWaitMs'),
'acquireStateLock must still enforce max wait budget on EEXIST retry exhaustion'
);
// The fix only affects the non-EEXIST branch; the EEXIST guard must still exist.
const eexistGuard = /err\.code\s*!==\s*['"]EEXIST['"]/;
assert.ok(
eexistGuard.test(fnSrc),
'acquireStateLock must still distinguish EEXIST from other errors'
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// C4. RETRY_ERRNOS set: new Docker/NFS transient codes must be present (#3776)
// ─────────────────────────────────────────────────────────────────────────────