mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(test): isolate provider homes outside native Codex e2e files (#1586)
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.
This commit is contained in:
1 parent
d2a6704788
commit
8bad2f5a01
11 files changed
+168
-38
No files matched your search
@@ -71,8 +71,13 @@ are not yet runnable checks.
|
||||
database, workspace and topology roots, seat identity, bearer tokens), forces a fixture
|
||||
`OPENRIG_HOME` and fails if the home is not fixture-scoped. It also wraps `fetch`, so a real
|
||||
request to anything other than a loopback ephemeral-port fixture or a registered target fails.
|
||||
Unless `OPENRIG_E2E_REAL_CODEX=1`, it points `HOME` and the XDG directories at a fixture user
|
||||
home and clears `CODEX_HOME`; that opt-in keeps the real homes. A test can still set its own
|
||||
It points `HOME` and the XDG directories at a fixture user home and clears `CODEX_HOME` and
|
||||
`CLAUDE_CONFIG_DIR`. `OPENRIG_E2E_REAL_CODEX=1` keeps native homes and binaries only for
|
||||
`seat-handover-model-fidelity-e2e.test.ts` and `seat-lifecycle-set-model-resume-e2e.test.ts`;
|
||||
all other files remain isolated. The scenario scaffold always uses fixture homes and runtime stubs.
|
||||
The two native files rely on Vitest's default per-file isolation; runs without it are unsupported.
|
||||
Scenario scaffolds scrub inherited provider roots but preserve those passed in an explicit `baseEnv`.
|
||||
A test can still set its own
|
||||
overrides after setup. The UI suite (jsdom) uses its own `packages/ui/test/setup.ts`.
|
||||
|
||||
- **Stale vendored daemon.** `npm run build:package` leaves an assembled copy of the daemon at
|
||||
|
||||
@@ -203,7 +203,7 @@ export class CodexThreadIdResolver {
|
||||
const readFromLogs = injected
|
||||
? (p: number, home: string, report?: CodexCaptureReporter) => injected(p, home, report, startSecond)
|
||||
: (p: number, home: string, report?: CodexCaptureReporter) => readCodexThreadIdFromLogs(p, home, undefined, minTs, undefined, report, startSecond);
|
||||
const defaultHome = this.opts.defaultHome ?? safeUserHomeDir() ?? os.homedir();
|
||||
const defaultHome = this.opts.defaultHome ?? os.homedir();
|
||||
const now = this.opts.now ?? Date.now;
|
||||
const ttl = this.opts.homeTtlMs ?? 60_000;
|
||||
|
||||
@@ -285,19 +285,11 @@ void _resolveRequiresIdentityPin;
|
||||
|
||||
function uniqueHomes(candidateHomes: Array<string | undefined>): string[] {
|
||||
const homes = candidateHomes.filter((home): home is string => Boolean(home));
|
||||
const userHome = safeUserHomeDir();
|
||||
if (userHome) homes.push(userHome);
|
||||
// os.homedir() honors HOME. The passwd home must not bypass an explicit provider/test home.
|
||||
homes.push(os.homedir());
|
||||
return [...new Set(homes)];
|
||||
}
|
||||
|
||||
function safeUserHomeDir(): string | undefined {
|
||||
try {
|
||||
return os.userInfo().homedir;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** How a gated read treats rows logged inside the identity's own start second (see {@link lstartToMinTs}). */
|
||||
export type StartSecondRows =
|
||||
/** Exclude them: the default, for every caller that can wait for a later row. */
|
||||
|
||||
@@ -2,11 +2,15 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import Database from "better-sqlite3";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { CodexThreadIdResolver, lstartToMinTs } from "../src/domain/codex-thread-id.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { CodexThreadIdResolver, lstartToMinTs, readCodexThreadIdFromCandidateHomes } from "../src/domain/codex-thread-id.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const identity = "Sun Sep 6 23:52:32 2026";
|
||||
const start = lstartToMinTs(identity)!;
|
||||
function fixture() {
|
||||
@@ -24,6 +28,38 @@ function fixture() {
|
||||
};
|
||||
}
|
||||
|
||||
describe("Codex home discovery respects an explicit HOME", () => {
|
||||
it("reads the default home before resolving a process home and never consults passwd", async () => {
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), "codex-default-home-")); roots.push(home);
|
||||
vi.stubEnv("HOME", home);
|
||||
const passwd = vi.spyOn(os, "userInfo").mockImplementation(() => { throw new Error("passwd home must not be read"); });
|
||||
const processHome = vi.fn(() => "/synthetic-other-home");
|
||||
const read = vi.fn((_pid: number, candidate: string) => candidate === home ? "conversation" : undefined);
|
||||
const resolver = new CodexThreadIdResolver({ readFromLogs: read, resolveHomeDirByPid: processHome });
|
||||
expect(await resolver.resolve(60422, identity)).toBe("conversation");
|
||||
expect(read).toHaveBeenCalledExactlyOnceWith(60422, home, expect.any(Function), "exclude");
|
||||
expect(processHome).not.toHaveBeenCalled();
|
||||
expect(passwd).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("candidate-home fallback finds fixture logs without searching the passwd home", () => {
|
||||
const f = fixture(); f.thread("conversation"); f.log("conversation"); f.finish();
|
||||
vi.stubEnv("HOME", f.home);
|
||||
const passwd = vi.spyOn(os, "userInfo").mockImplementation(() => { throw new Error("passwd home must not be read"); });
|
||||
expect(readCodexThreadIdFromCandidateHomes(60422, [])).toBe("conversation");
|
||||
expect(passwd).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("an empty fixture home never resolves another home's conversation", () => {
|
||||
const foreign = fixture(); foreign.thread("foreign-conversation"); foreign.log("foreign-conversation"); foreign.finish();
|
||||
const home = fs.mkdtempSync(path.join(os.tmpdir(), "codex-empty-home-")); roots.push(home);
|
||||
vi.stubEnv("HOME", home);
|
||||
const passwd = vi.spyOn(os, "userInfo").mockReturnValue({ homedir: foreign.home } as ReturnType<typeof os.userInfo>);
|
||||
expect(readCodexThreadIdFromCandidateHomes(60422, [])).toBeUndefined();
|
||||
expect(passwd).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("native conversation identity excludes same-process auxiliary threads", () => {
|
||||
it("resolves the first native conversation while title generation is the newest log activity", async () => {
|
||||
const f = fixture(); f.thread("conversation"); f.log("conversation"); f.log("title", start + 2);
|
||||
|
||||
@@ -21,7 +21,10 @@ function runSetup(env: Record<string, string | undefined> = {}) {
|
||||
env: { PATH: process.env.PATH, TMPDIR: tmp, ...env },
|
||||
});
|
||||
expect(child.status, child.stderr).toBe(0);
|
||||
const homes = JSON.parse(child.stdout.trim().split("\n").pop()!) as { openrigHome: string; home: string; tmpdir: string; scratch: string };
|
||||
const homes = JSON.parse(child.stdout.trim().split("\n").pop()!) as {
|
||||
openrigHome: string; home: string; tmpdir: string; scratch: string;
|
||||
osHome: string; codexHome?: string; claudeConfigDir?: string; xdgConfig: string; codexVersion: string;
|
||||
};
|
||||
return { tmp, ...homes };
|
||||
}
|
||||
const fixtureEntries = (tmp: string) => fs.readdirSync(tmp).filter((name) => name.startsWith(".openrig-test-fixture-home-"));
|
||||
@@ -29,7 +32,22 @@ const fixtureEntries = (tmp: string) => fs.readdirSync(tmp).filter((name) => nam
|
||||
* Not tsx's loader cache, which the child's launcher makes before the setup runs. */
|
||||
const leftovers = (tmp: string) => fs.readdirSync(tmp).filter((name) => !name.startsWith("tsx-"));
|
||||
|
||||
describe.skipIf(process.env["OPENRIG_E2E_REAL_CODEX"] === "1")("fixture homes are removed when the worker exits", () => {
|
||||
describe("fixture homes are removed when the worker exits", () => {
|
||||
it.each([undefined, "1"])("isolates provider homes and runtime lookup with native opt-in=%s", optIn => {
|
||||
const { tmp, openrigHome, home, osHome, codexHome, claudeConfigDir, xdgConfig, codexVersion } = runSetup({
|
||||
HOME: "/synthetic-host-home", CODEX_HOME: "/synthetic-host-codex",
|
||||
CLAUDE_CONFIG_DIR: "/synthetic-host-claude", XDG_CONFIG_HOME: "/synthetic-host-xdg",
|
||||
OPENRIG_E2E_REAL_CODEX: optIn,
|
||||
});
|
||||
expect(home).toBe(`${openrigHome}-user-home`);
|
||||
expect(osHome).toBe(home);
|
||||
expect(codexHome).toBeUndefined();
|
||||
expect(claudeConfigDir).toBeUndefined();
|
||||
expect(xdgConfig).toBe(path.join(home, ".config"));
|
||||
expect(codexVersion).toBe("codex-cli 0.139.0");
|
||||
expect(leftovers(tmp)).toEqual([]);
|
||||
});
|
||||
|
||||
it("removes the fixture home and the user home", () => {
|
||||
const { tmp, openrigHome, home } = runSetup();
|
||||
expect(path.dirname(openrigHome)).toBe(tmp);
|
||||
|
||||
+7
-1
@@ -3,11 +3,17 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import "../../../../test/hermetic-env.setup.js";
|
||||
|
||||
// What a test does: make a temp dir the usual way, with os.tmpdir().
|
||||
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "test-scratch-"));
|
||||
fs.writeFileSync(path.join(scratch, "file"), "x");
|
||||
console.log(JSON.stringify({ openrigHome: process.env["OPENRIG_HOME"], home: process.env["HOME"], tmpdir: os.tmpdir(), scratch }));
|
||||
console.log(JSON.stringify({
|
||||
openrigHome: process.env["OPENRIG_HOME"], home: process.env["HOME"], tmpdir: os.tmpdir(), scratch,
|
||||
osHome: os.homedir(), codexHome: process.env["CODEX_HOME"], claudeConfigDir: process.env["CLAUDE_CONFIG_DIR"],
|
||||
xdgConfig: process.env["XDG_CONFIG_HOME"],
|
||||
codexVersion: execFileSync("codex", ["--version"], { encoding: "utf-8", timeout: 5000 }).trim(),
|
||||
}));
|
||||
// With "wait", stay alive for the test to signal: a vitest worker is ended with SIGTERM while it is running.
|
||||
if (process.argv[2] === "wait") setInterval(() => {}, 1000);
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { realRuntimesRequested, writeRuntimeStubs } from "../../../../test/stub-runtimes.js";
|
||||
import { writeRuntimeStubs } from "../../../../test/stub-runtimes.js";
|
||||
|
||||
/**
|
||||
* Inherited environment variables that redirect the CLI/daemon client at a
|
||||
@@ -280,7 +280,7 @@ export interface HermeticScaffold {
|
||||
}
|
||||
|
||||
export interface PrepareHermeticEnvOptions {
|
||||
/** Base environment to derive the clean child env from. Defaults to process.env. */
|
||||
/** Base environment to derive the clean child env from. Explicit provider roots are preserved; defaults to process.env. */
|
||||
baseEnv?: EnvLike;
|
||||
/**
|
||||
* The A3-R3 injected clock. When set, the scaffold's child env carries
|
||||
@@ -331,8 +331,8 @@ export function prepareHermeticEnv(opts: PrepareHermeticEnvOptions = {}): Hermet
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
// codex and claude resolve to stubs inside the scaffold, so a scenario daemon never runs the host's
|
||||
// real binaries (they can hang under a scratch HOME). Tests that opt into real runtimes keep them.
|
||||
const runtimeStubDir = realRuntimesRequested(baseEnv) ? undefined : writeRuntimeStubs(root);
|
||||
// real binaries (they can hang under a scratch HOME). Native Codex e2e tests use their own harness.
|
||||
const runtimeStubDir = writeRuntimeStubs(root);
|
||||
|
||||
// Test-only command seam: every child `tmux` call gets one explicit private
|
||||
// socket. TMUX_TMPDIR remains for compatibility, but is never the selector.
|
||||
@@ -358,6 +358,11 @@ process.exit(result.status ?? 1);
|
||||
const env: Record<string, string | undefined> = { ...baseEnv };
|
||||
for (const name of DAEMON_TARGET_ENV_VARS) delete env[name];
|
||||
for (const name of CREDENTIAL_ENV_VARS) delete env[name];
|
||||
// Inherited provider roots belong to the runner; explicit ones are test inputs.
|
||||
if (opts.baseEnv === undefined) {
|
||||
for (const name of ["CODEX_HOME", "CLAUDE_CONFIG_DIR"]) delete env[name];
|
||||
}
|
||||
delete env.OPENRIG_E2E_REAL_CODEX;
|
||||
// Scrub any injected-clock var (defense in depth behind the fail-closed guard) so
|
||||
// only an explicit injectClockNow can set it below — never an inherited value.
|
||||
for (const name of TEST_CLOCK_ENV_VARS) delete env[name];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { delimiter, join } from "node:path";
|
||||
import {
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
describe("prepareHermeticEnv scaffold", () => {
|
||||
const scaffolds: HermeticScaffold[] = [];
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
for (const s of scaffolds.splice(0)) s.cleanup();
|
||||
});
|
||||
const make = (baseEnv?: Record<string, string | undefined>) => {
|
||||
@@ -88,6 +89,35 @@ describe("prepareHermeticEnv scaffold", () => {
|
||||
expect(s.env).not.toBe(base);
|
||||
});
|
||||
|
||||
it("scrubs inherited provider roots and the native opt-in from scenario children", () => {
|
||||
vi.stubEnv("CODEX_HOME", "/synthetic-host-codex");
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", "/synthetic-host-claude");
|
||||
vi.stubEnv("OPENRIG_E2E_REAL_CODEX", "1");
|
||||
const s = make();
|
||||
expect(s.env.HOME).toBe(s.home);
|
||||
for (const key of ["CODEX_HOME", "CLAUDE_CONFIG_DIR", "OPENRIG_E2E_REAL_CODEX"]) {
|
||||
expect(s.env[key]).toBeUndefined();
|
||||
}
|
||||
expect(process.env.CODEX_HOME).toBe("/synthetic-host-codex");
|
||||
expect(process.env.CLAUDE_CONFIG_DIR).toBe("/synthetic-host-claude");
|
||||
expect(process.env.OPENRIG_E2E_REAL_CODEX).toBe("1");
|
||||
});
|
||||
|
||||
it("preserves explicit provider roots but still scrubs the native opt-in", () => {
|
||||
const base = {
|
||||
HOME: "/synthetic-host-home", CODEX_HOME: "/synthetic-host-codex",
|
||||
CLAUDE_CONFIG_DIR: "/synthetic-host-claude", OPENRIG_E2E_REAL_CODEX: "1",
|
||||
};
|
||||
const s = make(base);
|
||||
expect(s.env.HOME).toBe(s.home);
|
||||
expect(s.env.CODEX_HOME).toBe(base.CODEX_HOME);
|
||||
expect(s.env.CLAUDE_CONFIG_DIR).toBe(base.CLAUDE_CONFIG_DIR);
|
||||
expect(s.env.OPENRIG_E2E_REAL_CODEX).toBeUndefined();
|
||||
expect(base.CODEX_HOME).toBe("/synthetic-host-codex");
|
||||
expect(base.CLAUDE_CONFIG_DIR).toBe("/synthetic-host-claude");
|
||||
expect(base.OPENRIG_E2E_REAL_CODEX).toBe("1");
|
||||
});
|
||||
|
||||
it("cleanup removes the scaffold root", () => {
|
||||
const s = prepareHermeticEnv({ baseEnv: { HOME: "/real-home" } });
|
||||
const root = s.root;
|
||||
|
||||
@@ -428,7 +428,8 @@ describe("createDaemon startup composition", () => {
|
||||
});
|
||||
|
||||
it("passes daemon CLI reachability env and PATH into launched tmux sessions", async () => {
|
||||
vi.stubEnv("PATH", "/proof/openrig/bin:/usr/bin:/bin");
|
||||
const launchPath = `/proof/openrig/bin${path.delimiter}${process.env.PATH}`;
|
||||
vi.stubEnv("PATH", launchPath);
|
||||
vi.stubEnv("OPENRIG_PORT", "17433");
|
||||
vi.stubEnv("OPENRIG_HOST", "127.0.0.1");
|
||||
const cmuxFactory: CmuxTransportFactory = async () => {
|
||||
@@ -448,7 +449,7 @@ describe("createDaemon startup composition", () => {
|
||||
.map((call) => call[0])
|
||||
.find((cmd) => cmd.includes("tmux new-session"));
|
||||
expect(newSessionCmd).toBeDefined();
|
||||
expect(newSessionCmd).toContain("-e 'PATH=/proof/openrig/bin:/usr/bin:/bin'");
|
||||
expect(newSessionCmd).toContain(`-e 'PATH=${launchPath}'`);
|
||||
expect(newSessionCmd).toContain("-e 'OPENRIG_PORT=17433'");
|
||||
expect(newSessionCmd).toContain("-e 'OPENRIG_HOST=127.0.0.1'");
|
||||
|
||||
@@ -465,7 +466,8 @@ describe("createDaemon startup composition", () => {
|
||||
const saved = saveEnv("HOME", "CODEX_HOME", "PATH", "OPENRIG_RECOVERY_PROVIDER_AUTH_ENV_ALLOWLIST", "OPENAI_API_KEY");
|
||||
process.env.HOME = daemonHome;
|
||||
delete process.env.CODEX_HOME;
|
||||
process.env.PATH = "/proof/openrig/bin:/usr/bin:/bin";
|
||||
const launchPath = `/proof/openrig/bin${path.delimiter}${process.env.PATH}`;
|
||||
process.env.PATH = launchPath;
|
||||
process.env.OPENRIG_RECOVERY_PROVIDER_AUTH_ENV_ALLOWLIST = "OPENAI_API_KEY";
|
||||
process.env.OPENAI_API_KEY = "gap7-openai-key";
|
||||
const tmuxExec = vi.fn<ExecFn>(async () => "");
|
||||
@@ -479,7 +481,7 @@ describe("createDaemon startup composition", () => {
|
||||
expect(result.deps.sessionEnv).toMatchObject({
|
||||
HOME: daemonHome,
|
||||
CODEX_HOME: path.join(daemonHome, ".codex"),
|
||||
PATH: "/proof/openrig/bin:/usr/bin:/bin",
|
||||
PATH: launchPath,
|
||||
OPENAI_API_KEY: "gap7-openai-key",
|
||||
});
|
||||
|
||||
@@ -489,7 +491,7 @@ describe("createDaemon startup composition", () => {
|
||||
const command = tmuxExec.mock.calls.map((call) => call[0]).find((line) => line.includes("tmux new-session"));
|
||||
expect(command).toContain(`-e 'HOME=${daemonHome}'`);
|
||||
expect(command).toContain(`-e 'CODEX_HOME=${path.join(daemonHome, ".codex")}'`);
|
||||
expect(command).toContain("-e 'PATH=/proof/openrig/bin:/usr/bin:/bin'");
|
||||
expect(command).toContain(`-e 'PATH=${launchPath}'`);
|
||||
expect(command).toContain("-e 'OPENAI_API_KEY=gap7-openai-key'");
|
||||
} finally {
|
||||
result?.db.close();
|
||||
|
||||
@@ -5,11 +5,11 @@ import { tmpdir } from "node:os";
|
||||
import { delimiter, join, sep } from "node:path";
|
||||
import { probeCodexDaemonSupport } from "../src/domain/codex-daemon-support.js";
|
||||
import { prepareHermeticEnv, type HermeticScaffold } from "./helpers/hermetic-env.js";
|
||||
import { realRuntimesRequested } from "../../../test/stub-runtimes.js";
|
||||
|
||||
// Daemon tests must never run the host's real `codex` or `claude`. Under a fixture HOME the host's
|
||||
// ~/.local/bin/codex wrapper can hang (mise use -g), and every daemon start probes `codex --version`.
|
||||
// The shared vitest setup and prepareHermeticEnv put stubs first on PATH instead.
|
||||
const realRuntimes = process.env["OPENRIG_E2E_REAL_CODEX"] === "1";
|
||||
|
||||
function resolveOnPath(binary: string, pathVar: string): string | undefined {
|
||||
for (const dir of pathVar.split(delimiter)) {
|
||||
@@ -19,7 +19,22 @@ function resolveOnPath(binary: string, pathVar: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
describe.skipIf(realRuntimes)("stub codex and claude on the test PATH", () => {
|
||||
describe("stub codex and claude on the test PATH", () => {
|
||||
it.each(["seat-handover-model-fidelity-e2e.test.ts", "seat-lifecycle-set-model-resume-e2e.test.ts"])(
|
||||
"allows only the opted-in native e2e file %s", name => {
|
||||
const testPath = join(import.meta.dirname, name);
|
||||
expect(realRuntimesRequested({ OPENRIG_E2E_REAL_CODEX: "1" }, testPath)).toBe(true);
|
||||
expect(realRuntimesRequested({}, testPath)).toBe(false);
|
||||
expect(realRuntimesRequested({ OPENRIG_E2E_REAL_CODEX: "1" }, join(tmpdir(), name))).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not grant the native exception to ordinary tests or plain Node setup consumers", () => {
|
||||
const env = { OPENRIG_E2E_REAL_CODEX: "1" };
|
||||
expect(realRuntimesRequested(env, import.meta.filename)).toBe(false);
|
||||
expect(realRuntimesRequested(env)).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves codex and claude inside the fixture root under the vitest setup", () => {
|
||||
const root = realpathSync(process.env["OPENRIG_HOME"]!);
|
||||
for (const binary of ["codex", "claude"]) {
|
||||
@@ -86,10 +101,11 @@ describe.skipIf(realRuntimes)("stub codex and claude on the test PATH", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves the host's binaries alone when real runtimes are opted in", () => {
|
||||
it("keeps scenario runtimes stubbed even when the native e2e opt-in is inherited", () => {
|
||||
const s = prepareHermeticEnv({ baseEnv: { HOME: "/real-home", PATH: "/usr/bin", OPENRIG_E2E_REAL_CODEX: "1" } });
|
||||
scaffolds.push(s);
|
||||
expect(s.env["PATH"]?.split(delimiter).some((dir) => dir.startsWith(s.root) && existsSync(join(dir, "codex")))).toBe(false);
|
||||
expect(s.env["PATH"]?.split(delimiter).some((dir) => dir.startsWith(s.root) && existsSync(join(dir, "codex")))).toBe(true);
|
||||
expect(s.env["OPENRIG_E2E_REAL_CODEX"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { writeRuntimeStubs } from "./stub-runtimes.js";
|
||||
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
|
||||
@@ -115,7 +115,12 @@ process.env["OPENRIG_COMPACTION_OUT_ROOT"] = path.join(fixtureHome, "compaction-
|
||||
// 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.
|
||||
if (process.env["OPENRIG_E2E_REAL_CODEX"] !== "1") {
|
||||
// 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;
|
||||
@@ -125,6 +130,11 @@ if (process.env["OPENRIG_E2E_REAL_CODEX"] !== "1") {
|
||||
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.
|
||||
|
||||
+13
-3
@@ -10,6 +10,7 @@
|
||||
// do. The Codex version is >= 0.130 so startup skips the `codex_hooks` feature-flag write.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const STUBS = {
|
||||
codex: {
|
||||
@@ -26,9 +27,18 @@ const STUBS = {
|
||||
|
||||
const STUB_NAME = ".openrig-test-runtime-stubs";
|
||||
|
||||
/** True when a test run opted into the host's real runtimes (the existing E2E opt-in). */
|
||||
export function realRuntimesRequested(env: Record<string, string | undefined> = process.env): boolean {
|
||||
return env["OPENRIG_E2E_REAL_CODEX"] === "1";
|
||||
const REAL_CODEX_TESTS = [
|
||||
"seat-handover-model-fidelity-e2e.test.ts",
|
||||
"seat-lifecycle-set-model-resume-e2e.test.ts",
|
||||
].map(name => fileURLToPath(new URL(`../packages/daemon/test/${name}`, import.meta.url)));
|
||||
|
||||
/** The opt-in permits native runtimes only in the two tests that exercise real Codex. */
|
||||
export function realRuntimesRequested(
|
||||
env: Record<string, string | undefined> = process.env,
|
||||
testPath?: string,
|
||||
): boolean {
|
||||
return env["OPENRIG_E2E_REAL_CODEX"] === "1" && testPath !== undefined
|
||||
&& REAL_CODEX_TESTS.includes(path.resolve(testPath));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user