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:
87
tests/helpers/clock.cjs
Normal file
87
tests/helpers/clock.cjs
Normal file
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Fake clock helper for deterministic lock tests (issue #453).
|
||||
*
|
||||
* Usage:
|
||||
* const { makeFakeClock } = require('./helpers/clock.cjs');
|
||||
* const clock = makeFakeClock(0); // start at t=0
|
||||
* clock.advance(5000); // jump 5 000 ms forward
|
||||
* acquireStateLock(path, clock); // no real waits; timeout/stale logic driven by advance()
|
||||
*
|
||||
* The fake clock returned by makeFakeClock() is compatible with the clock seam
|
||||
* accepted by acquireStateLock(statePath, clock) in state.cjs and
|
||||
* withPlanningLock(cwd, fn, clock) in planning-workspace.cjs.
|
||||
*
|
||||
* API
|
||||
* ───
|
||||
* now() → returns the current virtual epoch milliseconds.
|
||||
* sleep(ms) → records a sleep call without blocking; advances virtual time by ms.
|
||||
* advance(ms) → advance the virtual clock by ms without sleeping. Use between
|
||||
* synchronous retries to simulate elapsed time.
|
||||
* sleepCalls → array of ms values passed to sleep(), for assertion use.
|
||||
* nowValue → current virtual milliseconds (same as calling now()).
|
||||
*
|
||||
* Design notes
|
||||
* ────────────
|
||||
* • sleep() advances the clock by the requested duration so that a retry loop
|
||||
* checking `clock.now() - startedAt >= maxWaitMs` eventually trips the timeout
|
||||
* without needing any real sleeps in between.
|
||||
* • advance() allows the test to simulate arbitrary elapsed time without triggering
|
||||
* a sleep call (useful for driving the stale-lock check independently).
|
||||
* • Both now() and sleep() are intentionally synchronous so tests using them remain
|
||||
* fully synchronous — no async needed for lock serialization / timeout assertions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} [startMs=0] - initial virtual epoch milliseconds
|
||||
* @returns {{ now(): number, sleep(ms: number): void, advance(ms: number): void, sleepCalls: number[], nowValue: number }}
|
||||
*/
|
||||
function makeFakeClock(startMs) {
|
||||
if (startMs === undefined) startMs = 0;
|
||||
|
||||
let _now = startMs;
|
||||
const _sleepCalls = [];
|
||||
|
||||
const clock = {
|
||||
/** Return current virtual time (epoch ms). */
|
||||
now() {
|
||||
return _now;
|
||||
},
|
||||
|
||||
/**
|
||||
* Record a sleep call and advance virtual time by ms.
|
||||
* Does NOT block.
|
||||
*
|
||||
* @param {number} ms
|
||||
*/
|
||||
sleep(ms) {
|
||||
_sleepCalls.push(ms);
|
||||
_now += ms;
|
||||
},
|
||||
|
||||
/**
|
||||
* Advance the virtual clock by ms without recording a sleep call.
|
||||
* Use to simulate time passing between lock attempts.
|
||||
*
|
||||
* @param {number} ms
|
||||
*/
|
||||
advance(ms) {
|
||||
_now += ms;
|
||||
},
|
||||
|
||||
/** Array of ms values passed to sleep() in call order. */
|
||||
get sleepCalls() {
|
||||
return _sleepCalls;
|
||||
},
|
||||
|
||||
/** Current virtual time (same as now()). */
|
||||
get nowValue() {
|
||||
return _now;
|
||||
},
|
||||
};
|
||||
|
||||
return clock;
|
||||
}
|
||||
|
||||
module.exports = { makeFakeClock };
|
||||
Reference in New Issue
Block a user