fix(test): sandbox the home directory on every platform, add CI (v1.52.2) - #183
Merged
paulkr merged 2 commits intoAug 11, 2026
Merged
Conversation
…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>
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>
paulkr
approved these changes
Aug 11, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
INT-4396
Problem
Every test file sandboxed
process.env.HOME. That is a no-op on Windows:os.homedir()follows$HOMEon POSIX but readsUSERPROFILEon win32 and ignoresHOMEentirely.So on Windows the suite read and wrote the developer's real
~/.one:analytics.test.tsappended 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 nextonecommand, so those would have shipped to production PostHog.action-details.test.tswrote into the real knowledge cache.config.test.tsread through to the real~/.one/config.json, so ~29 assertions failed against whatever config happened to be installed. NotablyresolveConfig"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 20os.homedir()call sites and is overridable viaONE_HOMEidentically 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), andembedded-postgres(DEFAULTS.dataDir) each captured the home directory at import time, which defeats any override no matter how early it is set.config.ts:9already carried a comment warning about exactly this, from a 2026-04-21 incident where the suite clobbered a realconfig.json; these three sites had not been converted.test-support/home.ts— isolation you cannot get half-right.withTempHome()/setHomeTo()setONE_HOME,HOMEandUSERPROFILE.assertHomeIsSandboxed()throws rather than silently falling back to the real home — the failure mode that kept this invisible. All 9 test files that sandboxedHOMEnow use it.getProjectRootno longer walks above$HOME. It skipped home as a project root but kept climbing, so a.git,package.json, or.oneinC:\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, whereos.tmpdir()lives inside the real home (%LOCALAPPDATA%\Temp).scripts/run-tests.mjsreplaces 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 declaredengines.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
better-sqlite3will not build on Node 24).npx tsc --noEmitpasses.~/.oneis byte-for-byte unchanged across three consecutive full runs — 63 files, zero added, modified, or removed, verified by snapshottingLastWriteTimeUtc+Lengthbefore and after.withTempHometest caught a genuine flaw in my own guard during development (assertHomeIsSandboxedcompared againstos.homedir(), which the sandbox had already moved viaUSERPROFILE); it now compares against the real home captured at module load.Notes for review
engines.node: >=18. If it fails on first run, the honest fix is raisingenginesrather than dropping the job, since we currently promise 18 and don't verify it.package-lock.jsonversion fields hand-edited rather than regenerated:npm installon Node 24 drops thepgoptional 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.~/.one/.analytics-queue.jsonlon my machine (I removed 255; a backup is at.analytics-queue.jsonl.bak). Anyone who rannpm teston Windows should check theirs before the nextonecommand drains it.🤖 Generated with Claude Code