The windows-test-parity ratchet greps test source for fs.rmSync-without-
maxRetries (and six other Windows-portability anti-patterns), failing when an
integer offender COUNT exceeds a frozen baseline (rmSync: 95). A count ratchet
is a Goodhart metric: fixing one offender and adding another keeps the count
constant, so a new defect slips through green. Replace it — and every other
count ratchet in the repo — with a layered, masking-proof design.
Behavioral seam test
- tests/helpers-cleanup.test.cjs proves helpers.cleanup() carries the Windows
EBUSY retry budget. cleanup() delegates retries to Node's fs.rmSync via
maxRetries (it owns no loop), so the test asserts the option contract
(recursive/force/maxRetries>0/retryDelay>0) + real-FS removal + the cwd-guard,
rather than a loop that does not exist. The EBUSY risk is now tested ONCE at
the helper, not approximated textually at every call site.
Write-time ESLint rule (AST-accurate, replaces the grep)
- eslint-rules/no-raw-rmsync-in-tests.cjs (error in tests/**/*.test.cjs) bans
raw fs.rmSync, steering to cleanup(). Catches member, computed (fs['rmSync']),
destructured and aliased forms; escape hatch is inline
`// eslint-disable-next-line local/no-raw-rmsync-in-tests -- <reason>` only.
- Migrated 336 raw fs.rmSync teardown calls across ~116 test files to cleanup().
~18 genuinely load-bearing sites (mid-test SUT/fault-injection removals,
error-swallowing or name-colliding local teardown helpers) keep the raw call
with an inline eslint-disable + reason.
Shared anti-ratchet primitive
- scripts/lib/allowlist-ratchet.cjs:
- assertWithinAllowlist: fails on NOVEL ids (new offender introduced) AND on
STALE ids (a known offender was fixed but not pruned) — identity, not count,
and a ratchet DOWN toward zero.
- assertTightCeiling: a size/length budget whose ceiling must stay within a
grace band of the high-water mark, so budgets may only tighten, never creep.
Ratchets converted onto the primitive
- windows-test-parity-guard.test.cjs: rmSync rule deleted (now ESLint-enforced);
the remaining six patterns moved from integer baselines to named-set
allowlists with ratchet-down.
- scripts/lint-test-file-count.{cjs,allowlist.json}: per-module integer counts →
named filename sets (closes the swap-a-file-keep-the-count blind spot); a
module dropping under cap now FAILS to force pruning its allowlist entry.
- enh-2790 skill-count `<= 63` → named skill allowlist (ratchets toward ~58).
Size budgets hardened (tighten-only)
- agent-size / workflow-size / feat-3039 help-tiered: ceilings lowered to the
current high-water mark and an assertTightCeiling anti-creep check added per
tier. Fixed external-contract limits (description ≤100 chars, agent ≤100 KB)
are intentionally left as-is — they are not grandfathered creeping budgets.
No user-facing behavior change (tests + tooling only); no USER_FACING_PREFIXES
touched, so no changeset fragment is required.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Three parallel diagnostic agents (G: sdk/install path-sep, H: hook scripts,
I: worktree/workspace) characterised the remaining windows-22 failures on
23b52f1a. Patches by class:
CRLF in test parsers / file-content reads
- bug-2136-sh-hook-version: shebang check split('\n') → split(/\r?\n/)
- bug-3542-executor-git-stash-prohibition: strip \r from read content
(git rewrites stashed text with CRLF on win autocrlf=true checkout)
- workspace: BOM-strip + explicit \r strip in parseCommandFile (BOM at
byte 0 defeats /^---/ anchor → fmMatch null → fm.name undefined)
Windows path-separator / 8.3-shortname normalization
- bug-3491-nested-git-worktree: use fs.realpathSync.native to expand
%TEMP% RUNNER~1 → runneradmin; normalize sep before path-equality
- prune-orphaned-worktrees: normalize \\→/ before substring includes
(git emits forward-slash in --porcelain on Windows even when
path.join produced backslashes)
- bug-3017-codex-hook-absolute-node: accept POSIX path OR path with
drive-letter prefix in hookPath equality assertion
- bug-3126-global-skills-base-runtime-path: use path.join for expected
/xdg/<runtime> values (production calls path.join → \xdg\… on win32)
Test under-specified platform / forgot win32 env
- bug-2979-hook-absolute-node: pass {platform:'linux'} to
buildHookCommand + rewriteLegacyManagedNodeHookCommands so the
POSIX-branch tests don't pick up the #3393 GitBash code path
- bug-3288-model-catalog + bug-3571-config-manifest: also set
USERPROFILE alongside HOME so os.homedir() on win32 redirects to
the test fixture instead of the runner's real ~
External-cmd resolution
- bug-2647-outer-tarball-sdk-dist: use npm.cmd + {shell:true} on win32
so execFileSync resolves PATHEXT (literal `npm` is ENOENT)
ESM loader: tests/runtime-bridge-sync-smoke.test.cjs already migrated to
pathToFileURL in 23b52f1a (cluster E).
Explicit per-test/describe skip on win32 (with required string reasons
to satisfy no-unconditional-win32-skip guard)
- feat-3347-graphify-auto-update-hook: 3 describes — harness spawns
bash/kill/sleep + the hook itself is bash
- feat-3595-fs-fault-injection: move \t and \n filenames into the
POSIX-only branch (NTFS forbids 0x00–0x1F in filenames)
- bug-2775/2829/3033/3231/3359: POSIX shim under ~/.local/bin with
chmod 0o755 — not how Windows install works
- bug-3211: single subtest where cp.execSync reassignment isn't
picked up on win32 (POSIX coverage via the mock; live windows
behavior covered by 3211-D which keeps running)
- install-path-detection: parses sh-style export PATH= rc files;
Windows has no rc files (registry Path)
- worktree-safety-policy: single test using POSIX /repo/wt fixture
paths that can't be expressed under win32 path.resolve
Validated: plex2 (ubuntu docker) 11224/0 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>