fix(test): deterministic memory-durability Docker test (#136)#321
Conversation
tps init and its test (init.test.ts) each independently resolve the home directory via node:os homedir(), which falls back to the OS user database (getpwuid) rather than reading $HOME directly. The Docker test image runs as root with HOME=/home/tps injected only via ENV (no matching /etc/passwd entry for that path), and Bun's os.homedir() has been observed to cache its resolved value independent of later process.env.HOME mutations — mutations that ~40 sibling test files in this same bun test process perform in their own beforeEach/afterEach hooks for test isolation. That combination leaves the test's expected path (parent process) and the actual files written by the spawned CLI subprocess free to disagree, since agentYaml() also called homedir() a second, independent time inside the child. Fix: resolve home once via a single helper that prefers the live process.env.HOME (falling back to homedir() only if unset), thread that single value through runInit() and agentYaml() instead of each computing it separately, and mirror the same resolution in the test so both sides read the identical string. All three spawnSync calls in the test now explicitly pass this resolved HOME to the subprocess env (previously only the first did), removing the asymmetry. Docker isn't available on this host, so the exact container race couldn't be reproduced directly; validated by running the full local bun test suite (packages/cli) 10x for init.test.ts in isolation plus a full-suite run, both before and after — no new failures (same 5 pre-existing, unrelated environment-dependent failures on main). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tps-kern
left a comment
There was a problem hiding this comment.
Arch lane: ✅ APPROVED.
Root cause analysis is solid. The flake came from two independent homedir() resolutions (parent test process + spawned subprocess) disagreeing under Docker's root-with-injected-HOME-but-no-passwd-entry setup, compounded by Bun's os.homedir() caching behavior and sibling test files mutating process.env.HOME in the same bun test process.
The fix is correct and well-structured:
- resolveHome() preferring live process.env.HOME over homedir() is the right approach — it removes the dependency on OS-level resolution agreeing across process boundaries
- Threading home as a parameter into agentYaml() eliminates the second independent homedir() call — single resolution, single value
- All three spawnSync calls now pass env with HOME — the prior fix (#129) only patched one of three, leaving the race open
Notable: Docker Integration CI check is green on this PR — the exact check that was flaking. That's the strongest signal this works.
The validation caveat (Docker not available locally, couldn't reproduce directly) is honestly stated and doesn't undermine the fix — the logic is sound and CI confirms it.
tps-sherlock
left a comment
There was a problem hiding this comment.
Security review: approved.
Test fix for a flaky Docker init test — no new dependencies, no secrets exposure, no privilege changes.
The fix is sound: resolveHome() prefers process.env.HOME over homedir(), which avoids the Bun os.homedir() caching issue in container environments where $HOME is injected via ENV but the OS user database (getpwuid) disagrees. Threading a single resolved home value through runInit() and agentYaml() (instead of two independent homedir() calls) and passing HOME in all three spawnSync envs keeps the test and subprocess in lockstep.
No security concerns.
Root cause
test/init.test.ts("tps init > scaffolds identity, config, and workspace") is the flaky Docker Integration test referenced in #136 — confirmed via CI history: it failed inside theDocker Integrationjob (docker compose run --rm test) on the exact PRs the issue cites as impacted (#133/#134/#135's underlying commits), withExpected: true / Received: falseon the file-existence assertions.The test and the
tps initcommand it exercises each independently resolve the home directory vianode:oshomedir(), which falls back to the OS user database (getpwuid) when doing so. The Docker test image (Dockerfile.test) runs as root withHOME=/home/tpsinjected only viaENV— there's no realuseradd'd account, so/etc/passwd's entry for root still points at/root. On top of that, Bun'sos.homedir()has been observed (verified locally) to cache its resolved value independent of laterprocess.env.HOMEmutations within a process.packages/cli/test/has ~40 sibling test files that mutateprocess.env.HOMEin their ownbeforeEach/afterEachhooks for isolation, all executing inside the samebun testprocess asinit.test.ts.Put together: the test's expected path (computed once via
homedir()in the parent process) and the actual files written by the spawnedtps initsubprocess (which callshomedir()independently — twice, in fact: once inrunInit()and again inagentYaml()) are not guaranteed to agree. This exact class of bug was already suspected once before — PR #129 tried to patch it by explicitly forwardingHOMEinto the firstspawnSynccall's env, but the issue (filed the next day) shows it wasn't fully resolved, and the other twospawnSynccalls in the file never got that treatment at all.Fix
packages/cli/src/commands/init.ts: addedresolveHome(), which prefers the liveprocess.env.HOME(USERPROFILEon Windows) overhomedir(), used as the fallback only when unset.runInit()now resolveshomeonce and threads it intoagentYaml()as a parameter instead ofagentYaml()independently callinghomedir()a second time.packages/cli/test/init.test.ts: mirrors the sameprocess.env.HOME || homedir()resolution for itsHOMEconstant, so the test and the CLI subprocess derive the identical string. All threespawnSynccalls now explicitly passenv: { ...process.env, HOME }(previously only the first did).This removes the dependency on two independent, OS/runtime-level
homedir()resolutions agreeing across process boundaries, replacing it with a single explicit value threaded through everything that needs it.Validation
Docker is not available on this host, so I could not reproduce the exact container race directly — noting that limitation honestly rather than claiming a live repro.
What I did validate (bare
bun test, no Docker, same bun 1.3.10 as CI):tscbuild ofpackages/cliafter the change.bun test test/init.test.tsrun 10x in isolation: 3/3 pass every time (was already stable outside Docker, consistent with this being a container-specific race).packages/clisuite (bun test, 112 files / 1030 tests) before and after the change: identical result — 1025 pass / 5 fail, and the 5 failures are the same pre-existing, unrelated, environment-dependent tests onmain(PATH-dependentnonofake binary, live SSH reachability timeout, a couple ofverify-command exec tests) — confirmed by diffing the failing-test names before/after. No regressions introduced.Closes #136.