refactor(#1191): inject clock/reset testability seams + handle valid-null settings (#1233)

* refactor(#1191): inject clock/reset testability seams + handle valid-null settings

- worktree-safety reapOrphanWorktrees: injectable deps.nowMs clock for deterministic stale-lock boundary tests (mirrors snapshotWorktreeInventory's options.nowMs).

- active-workstream-store: _resetControllingTtyCacheForTests() seam clears the memoized controlling-TTY probe cache; test replaces require.cache busting.

- gen-capability-registry: export stripGeneratedComment (additive); test imports the real helper + equivalence assertion, keeping the deliberate drift oracle.

- install.js readSettings: a successfully-parsed JSON null is treated as empty settings ({}) instead of being mis-reported as malformed; genuine parse failures still warn. readSettings/stripJsonComments exported (GSD_TEST_MODE-guarded require) for real behavioral tests.

Closes #1191

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1191): add changeset for valid-null settings fix (#1233)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#1191): replace Stryker-incompatible structural reset test with behavioral isTTY-spy

The seam-2 reset test read the BUILT active-workstream-store.cjs and grepped for 'didProbeControllingTtyToken = false' — Stryker instruments that file so the literal is absent, failing the mutation DRY RUN. Replaced with a behavioral test that spies on process.stdin.isTTY access count to prove a post-reset probe re-runs (kills the didProbe-reset mutant) without reading source text. Local stryker: dry run passes, score 85.21% >= 80.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-14 20:52:56 -04:00
committed by GitHub
parent 55eb1fd72e
commit b783410815
9 changed files with 453 additions and 68 deletions

View File

@@ -49,6 +49,12 @@ function sanitizeWorkstreamSessionToken(value: unknown): string | null {
return token ? token.slice(0, 160) : null;
}
/** Test-only seam: clear the memoized controlling-TTY probe cache (#1191). */
function _resetControllingTtyCacheForTests(): void {
cachedControllingTtyToken = null;
didProbeControllingTtyToken = false;
}
function probeControllingTtyToken(): string | null {
if (didProbeControllingTtyToken) return cachedControllingTtyToken;
didProbeControllingTtyToken = true;
@@ -347,4 +353,5 @@ export = {
parseCliWorkstream,
resolveActiveWorkstream,
applyResolvedWorkstreamEnv,
_resetControllingTtyCacheForTests,
};

View File

@@ -100,6 +100,8 @@ interface WorktreeDeps {
readFileSafe?: (file: string) => string | null;
mtimeSafe?: (file: string) => Date | null;
reapMtimeGuardMs?: number;
/** Injected current time in ms since epoch for deterministic tests (#1191). */
nowMs?: number;
parseWorktreePorcelain?: (porcelain: string) => WorktreeBranchEntry[];
}
@@ -878,6 +880,7 @@ function reapOrphanWorktrees(repoRoot: string, deps: WorktreeDeps = {}): ReapRes
const readFileSafe = deps.readFileSafe || defaultReadFileSafe;
const mtimeSafe = deps.mtimeSafe || defaultMtimeSafe;
const reapMtimeGuardMs = deps.reapMtimeGuardMs !== undefined ? deps.reapMtimeGuardMs : REAP_MTIME_GUARD_MS;
const nowMs = deps.nowMs ?? Date.now();
const results: ReapResult[] = [];
@@ -982,7 +985,7 @@ function reapOrphanWorktrees(repoRoot: string, deps: WorktreeDeps = {}): ReapRes
// 4a. Stale-lock guard: skip if lock is too fresh (PID recycling / race).
const lockMtime = mtimeSafe(lockedFile);
if (!lockMtime || Date.now() - lockMtime.getTime() < reapMtimeGuardMs) {
if (!lockMtime || nowMs - lockMtime.getTime() < reapMtimeGuardMs) {
results.push({ path: worktreePath, status: 'skipped', reason: 'lock_too_fresh' });
continue;
}