Skip to content

fix(test): deterministic memory-durability Docker test (#136)#321

Merged
tps-flint merged 1 commit into
mainfrom
fix/issue-136-docker-init-test-flake
Jul 9, 2026
Merged

fix(test): deterministic memory-durability Docker test (#136)#321
tps-flint merged 1 commit into
mainfrom
fix/issue-136-docker-init-test-flake

Conversation

@tps-flint

Copy link
Copy Markdown
Contributor

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 the Docker Integration job (docker compose run --rm test) on the exact PRs the issue cites as impacted (#133/#134/#135's underlying commits), with Expected: true / Received: false on the file-existence assertions.

The test and the tps init command it exercises each independently resolve the home directory via node:os homedir(), which falls back to the OS user database (getpwuid) when doing so. The Docker test image (Dockerfile.test) runs as root with HOME=/home/tps injected only via ENV — there's no real useradd'd account, so /etc/passwd's entry for root still points at /root. On top of that, Bun's os.homedir() has been observed (verified locally) to cache its resolved value independent of later process.env.HOME mutations within a process. packages/cli/test/ has ~40 sibling test files that mutate process.env.HOME in their own beforeEach/afterEach hooks for isolation, all executing inside the same bun test process as init.test.ts.

Put together: the test's expected path (computed once via homedir() in the parent process) and the actual files written by the spawned tps init subprocess (which calls homedir() independently — twice, in fact: once in runInit() and again in agentYaml()) are not guaranteed to agree. This exact class of bug was already suspected once before — PR #129 tried to patch it by explicitly forwarding HOME into the first spawnSync call's env, but the issue (filed the next day) shows it wasn't fully resolved, and the other two spawnSync calls in the file never got that treatment at all.

Fix

  • packages/cli/src/commands/init.ts: added resolveHome(), which prefers the live process.env.HOME (USERPROFILE on Windows) over homedir(), used as the fallback only when unset. runInit() now resolves home once and threads it into agentYaml() as a parameter instead of agentYaml() independently calling homedir() a second time.
  • packages/cli/test/init.test.ts: mirrors the same process.env.HOME || homedir() resolution for its HOME constant, so the test and the CLI subprocess derive the identical string. All three spawnSync calls now explicitly pass env: { ...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):

  • Clean tsc build of packages/cli after the change.
  • bun test test/init.test.ts run 10x in isolation: 3/3 pass every time (was already stable outside Docker, consistent with this being a container-specific race).
  • Full packages/cli suite (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 on main (PATH-dependent nono fake binary, live SSH reachability timeout, a couple of verify-command exec tests) — confirmed by diffing the failing-test names before/after. No regressions introduced.

Closes #136.

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-flint
tps-flint requested a review from a team as a code owner July 9, 2026 20:04

@tps-kern tps-kern left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 tps-sherlock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@tps-flint
tps-flint merged commit 65d3e7f into main Jul 9, 2026
11 checks passed
@tps-flint
tps-flint deleted the fix/issue-136-docker-init-test-flake branch July 9, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Flaky Docker test (memory durability check)

3 participants