mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
The test setup left `CLAUDE_CONFIG_DIR` and the passwd home outside the fixture, and exporting `OPENRIG_E2E_REAL_CODEX=1` turned off home isolation for the whole run, so ordinary suites could read or write a developer's real provider homes. Ordinary suites now stay fixture-scoped even with the real-Codex opt-in exported; the exception is limited to the native Codex end-to-end files. Inherited provider roots (`CODEX_HOME`, `CLAUDE_CONFIG_DIR`) are isolated, while a provider home a test passes explicitly in its `baseEnv` is kept, so scenario environments that set one on purpose still work. Default Codex thread discovery now follows `HOME` (`os.homedir()`) instead of the passwd entry, so it stays inside the fixture too. `docs/as-built/test-layers.md` describes the layers. Thanks to @AdamWhitehurst for the report and to @Aschenbath for the fix.
161 lines
9.6 KiB
TypeScript
161 lines
9.6 KiB
TypeScript
// D12 base-health + WRITE-leak containment (broadcast-leak atom).
|
|
//
|
|
// The Node test suites MUST NOT inherit the seat's live-daemon connection. Two holes:
|
|
//
|
|
// 1. Connection-redirect env (OPENRIG_URL / OPENRIG_PORT / OPENRIG_HOST + RIGGED_*
|
|
// aliases) — honored as a real operator override in production, but ambient inside
|
|
// a seat it bypasses each test's injected mock daemon (up.test.ts: 29 fails ambient
|
|
// vs 1 hermetic). Scrubbed below. Production behavior is untouched.
|
|
//
|
|
// 2. The DEFAULT STATE_FILE discovery — `OPENRIG_HOME/daemon.json` (daemon-lifecycle
|
|
// `OPENRIG_DIR = OPENRIG_HOME`). Scrubbing the env is NOT enough: with URL/PORT
|
|
// unset, the CLI falls back to the live daemon's state file. This is exactly how
|
|
// `broadcast.test.ts` emitted REAL 'System maintenance' broadcasts to 14 seats.
|
|
// The READ direction (results pollution) was known; the WRITE direction (emitting
|
|
// into the live topology) is strictly worse.
|
|
//
|
|
// FORCE + ASSERT (unwritable-by-omission — the guarantee lives where it cannot be
|
|
// skipped, so a new test file inherits safety from the setup that runs first, not
|
|
// from a runner or a per-file convention that 145/164 files already omit):
|
|
// FORCE — establish a fixture-scoped OPENRIG_HOME (a fresh temp dir carrying the
|
|
// `.openrig-fixture` marker) BEFORE any daemon-resolving module loads and
|
|
// captures the eager `OPENRIG_HOME` const, so default discovery finds no
|
|
// live daemon.
|
|
// ASSERT — `assertFixtureScopedHome` throws LOUD if the home is NOT fixture-scoped
|
|
// (mkdtemp failure, or OPENRIG_HOME captured before this ran via an
|
|
// import-order regression). Forcing alone silently fixes and proves
|
|
// nothing; the assert makes it evidence. Its known-negative lives in
|
|
// live-daemon-guard.test.ts.
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { realRuntimesRequested, writeRuntimeStubs } from "./stub-runtimes.js";
|
|
|
|
// Scrub inherited instance selectors before product modules capture their defaults.
|
|
// Tests can still set explicit overrides after setup; test/provider opt-in flags
|
|
// are not instance selectors and remain untouched.
|
|
for (const key of [
|
|
"URL", "PORT", "HOST", "BIND_HOST", "HOST_SELECTED", "DB",
|
|
"TRANSCRIPTS_PATH", "WORKSPACE_ROOT", "WORKSPACE_SLICES_ROOT",
|
|
"WORKSPACE_STEERING_PATH", "WORKSPACE_SPECS_ROOT", "WORKSPACE_PROJECTS_ROOT",
|
|
"WORKSPACE_CATALOG_PATH", "TOPOLOGY_ROOT", "CONTEXT_ROOT", "CONTEXT_PACKS_ROOT",
|
|
"SKILLS_ROOT", "SHARED_DOCS_ROOT", "FILES_ALLOWLIST", "PROGRESS_SCAN_ROOTS",
|
|
"SESSION_NAME", "NODE_ID", "RIG_ID", "AUTH_BEARER_TOKEN", "TERMINAL_BEARER_TOKEN",
|
|
// Slice 15: a seat with compact output on must not change what the suite prints.
|
|
"OUTPUT_COMPACT",
|
|
// Slice 01: a seat with usage windows on must not change what the suite reads.
|
|
"PROVIDER_USAGE_WINDOWS",
|
|
// A seat with input-box verification on must not change what the transport suites verify.
|
|
"TRANSPORT_VERIFY_INPUT_BOX",
|
|
"TRANSPORT_LOCAL_USER_GUARD",
|
|
]) {
|
|
delete process.env[`OPENRIG_${key}`];
|
|
delete process.env[`RIGGED_${key}`];
|
|
}
|
|
|
|
// (2) FORCE a fixture-scoped home — BEFORE importing openrig-compat (whose
|
|
// OPENRIG_HOME const is eager). Node builtins above do not load it; the dynamic
|
|
// imports below run only AFTER this assignment, so the const captures the fixture.
|
|
// ONE stable fixture home per worker. setupFiles runs per test FILE, but
|
|
// openrig-compat's OPENRIG_HOME const is captured ONCE per worker — a fresh per-file
|
|
// home would diverge from that captured const (env says home N, the const holds home
|
|
// 1), which flakes any test reading one vs the other under suite ordering. So reuse a
|
|
// fixture home already established in this worker. Named as a realistic `.openrig`-style
|
|
// home (still fixture-scoped via the marker) so path-shape assertions hold against it.
|
|
const existingHome = process.env["OPENRIG_HOME"];
|
|
let createdHome: string | undefined;
|
|
let createdTmp: string | undefined;
|
|
const fixtureHome =
|
|
existingHome && fs.existsSync(path.join(existingHome, ".openrig-fixture"))
|
|
? existingHome
|
|
: (() => {
|
|
const h = fs.mkdtempSync(path.join(os.tmpdir(), ".openrig-test-fixture-home-"));
|
|
fs.writeFileSync(path.join(h, ".openrig-fixture"), "");
|
|
createdHome = h;
|
|
// The worker's own TMPDIR, so every os.tmpdir() mkdtemp a test makes lands in a directory removed with
|
|
// the homes. The name is short on purpose: it adds 9 characters ("o-" + 6 + "/") to each such path,
|
|
// and a Unix socket path is capped (104 bytes on macOS, where TMPDIR is already ~49 long).
|
|
// A package whose tests are tied to the length of the temp path (the tui's Unix socket tests) opts out with
|
|
// test.env OPENRIG_TEST_KEEP_TMPDIR=1 in its vitest config and keeps the TMPDIR it was given.
|
|
if (process.env["OPENRIG_TEST_KEEP_TMPDIR"] !== "1") {
|
|
createdTmp = fs.mkdtempSync(path.join(os.tmpdir(), "o-"));
|
|
process.env["TMPDIR"] = createdTmp;
|
|
}
|
|
return h;
|
|
})();
|
|
// Remove the homes (and the worker TMPDIR) this process created when it ends: nothing else ever does, and a full run leaves two per
|
|
// worker (a run on 2026-10-14 left ~65,000, 6.2 GB, in the RAM-backed /tmp). A hook here, not a globalSetup
|
|
// teardown: the homes are made inside the workers, which a teardown in the runner cannot name. Vitest ends a
|
|
// finished worker with SIGTERM, which skips "exit" hooks, so the signals clean up too and then take their
|
|
// default effect again. Only the creating file registers (later files in the worker reuse the home), and only
|
|
// a home this process made, still carrying the marker: a reused home from the environment, say a parent
|
|
// run's, is never ours to remove.
|
|
if (createdHome) {
|
|
const home = createdHome;
|
|
const tmp = createdTmp;
|
|
const remove = () => {
|
|
if (!fs.existsSync(path.join(home, ".openrig-fixture"))) return;
|
|
// A failed removal (busy, permissions) must not turn a signal or a clean exit into a crash.
|
|
for (const dir of [home, `${home}-user-home`, ...(tmp ? [tmp] : [])]) {
|
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* leave it: the worker is ending */ }
|
|
}
|
|
};
|
|
process.once("exit", remove);
|
|
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
|
|
process.once(signal, () => { remove(); process.kill(process.pid, signal); });
|
|
}
|
|
}
|
|
process.env["OPENRIG_HOME"] = fixtureHome;
|
|
delete process.env["RIGGED_HOME"];
|
|
// Compaction packets default to the shared /tmp/claude-compaction-restore that real seats use. Give every test a
|
|
// folder under the fixture home instead (removed with it), so a test that forgets its own root can't write there
|
|
// and a seat's value can't reach the suite. Tests that set their own root still do.
|
|
process.env["OPENRIG_COMPACTION_OUT_ROOT"] = path.join(fixtureHome, "compaction-packets");
|
|
// Legacy ~/.rigged discovery and ordinary home-based config must also stay local
|
|
// to this fixture. Merely changing HOME would not override OPENRIG_HOME or DB.
|
|
// Keep ordinary user files outside the state root, as in a real installation.
|
|
// Otherwise file-transfer correctly rejects every ~/ path as OpenRig state.
|
|
// Vitest sets testPath before setupFiles. Plain Node setup consumers have no native-runtime exception.
|
|
const testPath = process.env["VITEST"] ? (await import("vitest")).expect.getState().testPath : undefined;
|
|
const realCodexTest = realRuntimesRequested(process.env, testPath);
|
|
// These files exercise Codex only; an inherited Claude override never belongs to their fixture.
|
|
delete process.env["CLAUDE_CONFIG_DIR"];
|
|
if (!realCodexTest) {
|
|
const userHome = `${fixtureHome}-user-home`;
|
|
fs.mkdirSync(userHome, { recursive: true });
|
|
process.env["HOME"] = userHome;
|
|
delete process.env["CODEX_HOME"];
|
|
for (const [key, suffix] of Object.entries({
|
|
XDG_CONFIG_HOME: ".config", XDG_STATE_HOME: ".local/state",
|
|
XDG_DATA_HOME: ".local/share", XDG_CACHE_HOME: ".cache",
|
|
})) process.env[key] = path.join(userHome, suffix);
|
|
|
|
if (os.homedir() !== userHome || process.env["CODEX_HOME"] !== undefined
|
|
|| process.env["CLAUDE_CONFIG_DIR"] !== undefined) {
|
|
throw new Error("test provider homes must stay inside the fixture");
|
|
}
|
|
|
|
// codex and claude resolve to stubs that answer at once, so no test runs the host's real
|
|
// binaries (a codex wrapper that hangs under a fixture HOME leaves its probes running).
|
|
// The stubs live beside the fixture home and go away with it. One PATH entry per worker.
|
|
const stubDir = writeRuntimeStubs(userHome);
|
|
const pathEntries = (process.env["PATH"] ?? "").split(path.delimiter);
|
|
if (pathEntries[0] !== stubDir) {
|
|
process.env["PATH"] = [stubDir, ...pathEntries.filter((entry) => entry !== stubDir)].join(path.delimiter);
|
|
}
|
|
}
|
|
|
|
// (3) ASSERT — capture the resolved home + fail loud if it is not fixture-scoped.
|
|
const { OPENRIG_HOME } = await import("../packages/cli/src/openrig-compat.js");
|
|
const { assertFixtureScopedHome } = await import("../packages/cli/test/live-daemon-guard.js");
|
|
assertFixtureScopedHome(OPENRIG_HOME);
|
|
|
|
// (4) P37 REQUEST-LAYER GUARD — the universal outbound-request chokepoint. The two
|
|
// guards above close the DISCOVERY paths (connection env, state-file home); this closes
|
|
// the REQUEST layer, where a hardcoded :7433 literal, a mocked URL getter with an
|
|
// un-mocked client, or a production default constant still issues a real request that
|
|
// no env-scrub or fixture-home can catch. Fail-closed on any unregistered target; an
|
|
// in-process fixture registers its origin (allowFetchTarget) when it binds.
|
|
const { installFetchGuard } = await import("../packages/cli/test/fetch-guard.js");
|
|
installFetchGuard();
|