Skip to content

fix(test): sandbox the home directory on every platform, add CI (v1.52.2) - #183

Merged
paulkr merged 2 commits into
mainfrom
feature/int-4396-fix-windows-test-isolation-and-add-ci
Aug 11, 2026
Merged

fix(test): sandbox the home directory on every platform, add CI (v1.52.2)#183
paulkr merged 2 commits into
mainfrom
feature/int-4396-fix-windows-test-isolation-and-add-ci

Conversation

@siddharth-bhansali

Copy link
Copy Markdown
Collaborator

INT-4396

Problem

Every test file sandboxed process.env.HOME. That is a no-op on Windows: os.homedir() follows $HOME on POSIX but reads USERPROFILE on win32 and ignores HOME entirely.

process.env.HOME = 'C:/tmp/fake-home'
os.homedir()  // → C:\Users\<you>   ← unchanged

So on Windows the suite read and wrote the developer's real ~/.one:

  • analytics.test.ts appended 255 fabricated rollup events (distinct_id: "user-0" … "user-49") to the live telemetry send-queue. That queue is a retry queue — drainQueue() fires on the next one command, so those would have shipped to production PostHog.
  • action-details.test.ts wrote into the real knowledge cache.
  • config.test.ts read through to the real ~/.one/config.json, so ~29 assertions failed against whatever config happened to be installed. Notably resolveConfig "returns null scope when no config exists anywhere" failed with 'global'.

No workflow ran the suite on push or PR, so none of this was ever visible in CI.

Changes

lib/home.ts — one home resolver. homeDir() replaces all 20 os.homedir() call sites and is overridable via ONE_HOME identically on every platform. Also a genuine feature for containers and multi-tenant shells; documented in the README.

Three module-load bindings made lazy. update.ts (ONE_DIR/CACHE_PATH/LOCK_PATH), schedule-registry.ts (REGISTRY_DIR/REGISTRY_FILE), and embedded-postgres (DEFAULTS.dataDir) each captured the home directory at import time, which defeats any override no matter how early it is set. config.ts:9 already carried a comment warning about exactly this, from a 2026-04-21 incident where the suite clobbered a real config.json; these three sites had not been converted.

test-support/home.ts — isolation you cannot get half-right. withTempHome() / setHomeTo() set ONE_HOME, HOME and USERPROFILE. assertHomeIsSandboxed() throws rather than silently falling back to the real home — the failure mode that kept this invisible. All 9 test files that sandboxed HOME now use it.

getProjectRoot no longer walks above $HOME. It skipped home as a project root but kept climbing, so a .git, package.json, or .one in C:\Users / /home / / would be adopted as the project root for every marker-less directory under home. This is a real bug independent of tests — and it is also what let the sandbox escape on Windows, where os.tmpdir() lives inside the real home (%LOCALAPPDATA%\Temp).

scripts/run-tests.mjs replaces the glob. tsx --test \"src/**/*.test.ts\" relies on runner-side glob expansion that only landed in Node 21. On Node 18 and 20 — both inside our declared engines.node: >=18 — the pattern is passed through literally, matches nothing, and the runner exits 0 having run zero tests. A contributor on Node 20 sees a green run that asserted nothing. The new runner discovers files itself, works the same across sh/cmd/PowerShell, and fails loudly on an empty discovery.

.github/workflows/ci.yml. Typecheck + tests on Node 18/20/22 across ubuntu and windows, plus a build job so tsup/dts failures surface on a PR rather than on a release tag. Windows coverage is the point: this entire class of bug was invisible everywhere else.

Verification

  • Full suite: ~29 failures → 0. 405 tests, 392 pass, 13 skipped (suites that skip when the optional native deps are absent — better-sqlite3 will not build on Node 24).
  • npx tsc --noEmit passes.
  • The real ~/.one is byte-for-byte unchanged across three consecutive full runs — 63 files, zero added, modified, or removed, verified by snapshotting LastWriteTimeUtc + Length before and after.
  • The withTempHome test caught a genuine flaw in my own guard during development (assertHomeIsSandboxed compared against os.homedir(), which the sandbox had already moved via USERPROFILE); it now compares against the real home captured at module load.

Notes for review

  • Node 18 in the matrix is untested locally — I can't run it here. It is included to honour the declared engines.node: >=18. If it fails on first run, the honest fix is raising engines rather than dropping the job, since we currently promise 18 and don't verify it.
  • package-lock.json version fields hand-edited rather than regenerated: npm install on Node 24 drops the pg optional dependency from the lockfile. That dirty lockfile was already in the working tree when I started and I reverted it — worth a look at whoever's local Node.
  • Version 1.52.1 → 1.52.2. Collides with fix(sync): never destroy a healthy database on a driver fault (v1.52.3) #179's bump; whichever merges later needs a rebase.
  • Cleanup still outstanding from the same root cause: 45 test-fixture events remain in the real ~/.one/.analytics-queue.jsonl on my machine (I removed 255; a backup is at .analytics-queue.jsonl.bak). Anyone who ran npm test on Windows should check theirs before the next one command drains it.

🤖 Generated with Claude Code

…2.2)

Every test sandboxed `process.env.HOME`. `os.homedir()` follows $HOME on
POSIX but reads USERPROFILE on Windows and ignores HOME entirely, so on
Windows the suite read and wrote the developer's REAL ~/.one: it injected
fabricated events into the live telemetry send-queue (which the next `one`
command would have shipped to production PostHog), wrote into the real
knowledge cache, and failed ~29 assertions against whatever config happened
to be installed. No CI ran the suite, so nothing caught it.

- lib/home.ts: `homeDir()` is now the single home resolver, overridable with
  ONE_HOME on every platform. All 20 `os.homedir()` call sites moved to it.
- Three module-load bindings (update.ts ONE_DIR/CACHE_PATH/LOCK_PATH,
  schedule-registry REGISTRY_DIR/FILE, embedded-postgres DEFAULTS.dataDir)
  captured the home directory at import time, defeating any override. Now
  lazy, per the warning already written at config.ts:9.
- test-support/home.ts: withTempHome() / setHomeTo() sets all three vars, and
  assertHomeIsSandboxed() fails loudly rather than silently using the real
  home. All 9 test files that sandboxed HOME now use it.
- getProjectRoot skipped $HOME but kept walking ABOVE it, so a `.git`,
  `package.json` or `.one` in `C:\Users` / `/home` / `/` was adopted as the
  project root for every marker-less directory under home. Stops at home now.
  This is also what let the sandbox escape on Windows, where os.tmpdir()
  lives inside the real home.
- scripts/run-tests.mjs replaces `tsx --test "src/**/*.test.ts"`. Runner-side
  glob expansion only exists in Node 21+, so on Node 18/20 — both inside our
  declared engines range — that pattern matched nothing and exited 0 having
  run nothing. The new runner discovers files itself and fails on an empty
  discovery.
- .github/workflows/ci.yml: typecheck + test on Node 18/20/22 across ubuntu
  and windows, plus a build job. Previously the only workflow was
  release-triggered publish, so nothing was verified on push or PR.

Verified: full suite goes from ~29 failures to 0 (405 tests, 13 skipped where
optional native deps are absent), and a byte-for-byte snapshot of the real
~/.one is unchanged across three consecutive runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear

linear Bot commented Aug 11, 2026

Copy link
Copy Markdown

INT-4396

Follow-up to the same concern, found by a parallel audit of the test suite.

- `AGENTS` was a module-scope array. On POSIX its entries hold lazy `~/...`
  strings that expandPath() resolves at use time, but on win32 the Windsurf,
  Cursor and Claude-Desktop helpers return ABSOLUTE paths — so importing the
  module bound the real home on Windows and nothing could redirect it
  afterwards. The platform asymmetry is precisely what kept it invisible: the
  same code is lazy on the maintainers' machines and eager on Windows. Now
  built per call by getAgents().

- Claude Desktop's MCP config is NOT home-rooted on Windows — agents.ts
  resolves it through %APPDATA%\Claude, which no home override can intercept.
  Added APPDATA to the sandboxed vars, so a Windows test run can no longer
  rewrite the developer's real Claude Desktop MCP config.

3 new regression tests assert agent paths follow a home change after import,
never resolve inside the real home while sandboxed, and are fully expanded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
siddharth-bhansali added a commit that referenced this pull request Aug 11, 2026
Both this branch and #183 (INT-4396) were cut from 1.52.1 and independently
claimed 1.52.2, so whichever merged second would have published a duplicate
version. Sequenced behind #183, which lands the CI that verifies this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@paulkr
paulkr merged commit f533777 into main Aug 11, 2026
7 checks passed
@paulkr
paulkr deleted the feature/int-4396-fix-windows-test-isolation-and-add-ci branch August 11, 2026 13:54
paulkr pushed a commit that referenced this pull request Aug 13, 2026
…2.2) (#183)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants