fix(test): isolate route tests from the production ~/.codeman data dir - #356
Conversation
PR Ark0N#356 stopped the remote-hosts.json fixture write from clobbering prod. Two holes in the same file remain: 1. The quick-start afterEach still ran rmSync(CASES_DIR, recursive). CASES_DIR is join(homedir(), 'codeman-cases'), and on Linux builds where os.homedir() reads /etc/passwd instead of $HOME it resolves to the PROD case tree - so a full-suite run deleted the real ~/codeman-cases. Add a shared safeRmHomeTree() containment gate that only deletes a path under the redirected test HOME. 2. setup.ts teardown did rmSync(process.env.CODEMAN_DATA_DIR ?? '') AFTER restoring the env - if a pre-existing prod CODEMAN_DATA_DIR was set, that deleted prod. Capture the throwaway dir in a const and clean that. A broader test-isolation sweep (10 files: cli-skill-target, edge-cases, integration-flows, operation-lightspeed, ralph-integration, case-clone-routes, voice-routes, session-cleanup, sse-events, sse-subscription-filter) also applies the same containment gates to every per-case delete. It is intentionally NOT included here to keep this PR skinny; it is identified and available on request.
timkjr
commented
Aug 31, 2026
Test-isolation sweep identified — available on request While hardening this PR I found the same containment gap in a broader set of tests: per-case cleanup that does The affected files (identified, changes ready, deliberately not included here to keep this PR skinny):
Each applies the same |
timkjr
commented
Aug 31, 2026
Pushed a follow-up fix: the "never writes hooks for a remote attach" test was stubbing |
session-routes-workspace-hooks.test.ts wrote its h1/box/10.0.0.5 host fixture into getDataDir()/remote-hosts.json. getDataDir() resolves via homedir() → ~/.codeman (INSTANCE_SUFFIX='' by default), and overriding HOME in test/setup.ts does NOT change os.homedir() on Linux — so every full-suite run silently overwrote the PRODUCTION remote-hosts.json, wiping user-defined remote hosts, emptying the launch-case dropdown and breaking remote session creation (found live 2026-08-29). The vitest v4 test.env config key is ignored (probe confirmed the worker still saw CODEMAN_DATA_DIR=undefined), so the reliable fix is stubbing the env inside the test: the fixture write now goes to a throwaway /tmp dir via vi.stubEnv + finally unstub. Verified: prod remote-hosts.json hash is identical before and after the suite run.
PR Ark0N#356 stopped the remote-hosts.json fixture write from clobbering prod. Two holes in the same file remain: 1. The quick-start afterEach still ran rmSync(CASES_DIR, recursive). CASES_DIR is join(homedir(), 'codeman-cases'), and on Linux builds where os.homedir() reads /etc/passwd instead of $HOME it resolves to the PROD case tree - so a full-suite run deleted the real ~/codeman-cases. Add a shared safeRmHomeTree() containment gate that only deletes a path under the redirected test HOME. 2. setup.ts teardown did rmSync(process.env.CODEMAN_DATA_DIR ?? '') AFTER restoring the env - if a pre-existing prod CODEMAN_DATA_DIR was set, that deleted prod. Capture the throwaway dir in a const and clean that. A broader test-isolation sweep (10 files: cli-skill-target, edge-cases, integration-flows, operation-lightspeed, ralph-integration, case-clone-routes, voice-routes, session-cleanup, sse-events, sse-subscription-filter) also applies the same containment gates to every per-case delete. It is intentionally NOT included here to keep this PR skinny; it is identified and available on request.
…ads it The "never writes hooks for a remote attach" test stubbed CODEMAN_DATA_DIR to a separate throwaway dir just for this write, but session-routes.ts's CODEMAN_CONFIG_DIR is a module-load-time constant frozen at test/setup.ts's sandboxed dir before this test ever runs. The fixture landed somewhere the route handler could never read, so the remote-host lookup silently failed (NOT_FOUND) and the test passed for the wrong reason -- createErrorResponse never sets reply.code(), so Fastify's default 200 made the NOT_FOUND branch and the intended success branch indistinguishable by status code alone. Write straight to getDataDir() instead, matching the docker-hosts fixture convention already used elsewhere in this file. Verified the fix actually exercises the success path (host resolves, 200 with a real session), not just an accidental 200 from the error branch.
Ark0N#356 introduced safeRmHomeTree/isUnderTestHome to stop tests from deleting the PRODUCTION ~/codeman-cases tree on platforms where os.homedir() ignores the $HOME override -- but only applied it to the one file caught doing it live. CASES_DIR has no CODEMAN_DATA_DIR-style env override at all, so every other test file's raw rmSync(join(CASES_DIR, ...)) was the same unguarded pattern, just not yet triggered. Routes every CASES_DIR delete in these 10 files through safeRmHomeTree: cli-skill-target, edge-cases, integration-flows, operation-lightspeed, ralph-integration, routes/case-clone-routes, routes/voice-routes, session-cleanup, sse-events, sse-subscription-filter. Also fixes one instance in case-clone-routes.test.ts that mkdirSync'd then rmSync'd a CASES_DIR path directly with no guard at all -- the exact clobbering pattern Ark0N#356 exists to prevent, found by extending the sweep. Held as a separate commit (and intended as a separate PR once Ark0N#356 merges) rather than folding into Ark0N#356 -- keeps the already-checked skinny fix reviewable on its own; this is the same bug class applied broadly, not new functionality. Verified: all 10 files pass (180 tests), npm run typecheck clean.
eaaa1d7 to
4a63ab1CompareUh oh!
There was an error while loading. Please reload this page.
#356 stopped a bare suite run from overwriting the production `remote-hosts.json` by pointing `CODEMAN_DATA_DIR` at a throwaway dir, and it gated every case-tree delete on the temp HOME. Both changes are right; the explanation written next to them is not. It says `os.homedir()` reads /etc/passwd rather than `$HOME` on Linux, which would mean the temp HOME in test/setup.ts never worked. It does: libuv checks the env var before the passwd entry (measured: `HOME=/tmp/x node -e 'console.log(os.homedir())'` prints /tmp/x), and CLAUDE.md's testing section relies on exactly that. What bypasses the temp HOME is `CODEMAN_DATA_DIR` itself. `getDataDir()` reads it as an absolute override before it looks at `homedir()`, so one inherited from the shell (a second instance, a beta run) sends the whole suite at the real data dir. That is the case setup.ts now closes, and #371 names the same variable from the other direction. The comments in setup.ts, the `safeRmHomeTree` helper, the voice-routes and case-clone tests now say that, and the containment gate is described as what it is: defense in depth. CLAUDE.md's testing paragraph gets the same note so the next reader does not chase a homedir() bug that does not exist. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qg6bcATm1pNNY4kQWGwzgu
Ark0N
commented
Sep 4, 2026
Merged, thank you. Both changes are right and they ship in today's release. One correction to the write-up so the next reader does not chase it: Node's |
#356 and #371 fixed the same leak two ways. #356 pointed CODEMAN_DATA_DIR at a second throwaway directory and cleaned it up in afterAll and on exit; #371 deletes the variable along with CODEMAN_INSTANCE and CODEMAN_TMUX_SOCKET, so `getDataDir()` falls back to `homedir()`, which the temp HOME already redirects. Merged as they were, setup.ts set the variable and deleted it a few lines later, and the second directory was created for nothing. The strip wins: same protection, one tree to clean up, and the isolation test #371 adds pins the list statically. The extra directory, its restore and its two rmSync calls go, the vitest config `env` entries that set the same variable go (they were documented as inert and would now be contradicted by the setup file either way), the two test comments that described the old mechanism are reworded, and CLAUDE.md's testing paragraph names the three stripped variables and why CODEMAN_INSTANCE has to be stripped in the setup file rather than a hook. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qg6bcATm1pNNY4kQWGwzgu
Problem
test/routes/session-routes-workspace-hooks.test.tswrites itsh1/box/10.0.0.5remote-host fixture intogetDataDir()/remote-hosts.json:getDataDir()resolves viahomedir()→~/.codeman(INSTANCE_SUFFIX=''by default). OverridingHOMEintest/setup.tsdoes not changeos.homedir()on Linux — it reads/etc/passwd, not$HOME. So every full-suite run silently overwrites the production~/.codeman/remote-hosts.json: user-defined remote hosts are wiped, the launch-case dropdown empties, and remote session creation breaks with "host not found".Found live 2026-08-29: a bare suite run destroyed a production host registry. The test suite is tmux-safe by design, but this write path bypassed every safety.
Why the obvious fix doesn't work
Vitest 4.1.8 ignores the
test.envconfig key (probe-confirmed: a worker still sawCODEMAN_DATA_DIR=undefined). Setting the env intest/setup.tsalso doesn't propagate reliably. So the fix must happen inside the test itself, at call time —getDataDir()readsprocess.env.CODEMAN_DATA_DIRon every call, sovi.stubEnvworks:Changes
test/routes/session-routes-workspace-hooks.test.ts: redirect the fixture write to a throwaway/tmpdir +finallyunstub.test/setup.ts: setCODEMAN_DATA_DIRto a temp dir as defense-in-depth, and clean it up.config/vitest.config.ts+config/vitest.ci.config.ts: declareCODEMAN_DATA_DIR(inert in v4, correct in v5+ — harmless, and keeps intent documented).Verification
md5sum ~/.codeman/remote-hosts.jsonbefore and aftervitest run test/routes/session-routes-workspace-hooks.test.ts— identical (was being clobbered before)./tmp/codeman-hook-fixture-*, not prod.Suggested rule
Any test that writes into
getDataDir()MUST stubCODEMAN_DATA_DIRto a throwaway dir first. The suite'sHOMEoverride is not sufficient on Linux.