Files
openrig/packages/daemon/test/fixture-home-cleanup.test.ts
T
Nash Aschenbath 8bad2f5a01 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.
2026-10-21 00:57:54 -07:00

148 lines
7.2 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
// The shared vitest setup makes a fixture home, a sibling user home and a TMPDIR of its own per worker. It
// removes them when the worker exits; before, a full run left them behind and filled the (RAM-backed) /tmp.
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
function runSetup(env: Record<string, string | undefined> = {}) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fixture-home-run-"));
roots.push(tmp);
const child = spawnSync(process.execPath, [
"--import", createRequire(import.meta.url).resolve("tsx"),
path.join(import.meta.dirname, "fixtures/fixture-home-child.ts"),
], {
encoding: "utf-8", timeout: 60_000,
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;
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-"));
/** Anything the worker left in the TMPDIR it was given: its homes, its own TMPDIR and what tests put in it.
* 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("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);
expect(home).toBe(`${openrigHome}-user-home`);
expect(fs.existsSync(openrigHome)).toBe(false);
expect(fs.existsSync(home)).toBe(false);
expect(fixtureEntries(tmp)).toEqual([]);
});
it("gives the worker its own short TMPDIR, so a test's os.tmpdir() mkdtemp is removed with it", () => {
const { tmp, tmpdir, scratch } = runSetup();
// The test's temp dir was made in a directory of the worker's own, directly under the TMPDIR it was given...
expect(path.dirname(tmpdir)).toBe(tmp);
expect(path.basename(tmpdir)).toMatch(/^o-[A-Za-z0-9]{6}$/);
expect(path.dirname(scratch)).toBe(tmpdir);
// ...and nothing is left once the worker has exited.
expect(fs.existsSync(scratch)).toBe(false);
expect(fs.existsSync(tmpdir)).toBe(false);
expect(leftovers(tmp)).toEqual([]);
});
it("keeps os.tmpdir() as given when the package opts out, and still removes the homes", () => {
const { tmp, openrigHome, home, tmpdir, scratch } = runSetup({ OPENRIG_TEST_KEEP_TMPDIR: "1" });
// No TMPDIR of its own: a test's temp dir is made straight under the TMPDIR the run was given (it is the
// test's to remove, as before)...
expect(tmpdir).toBe(tmp);
expect(path.dirname(scratch)).toBe(tmp);
expect(fs.readdirSync(tmp).filter((name) => name.startsWith("o-"))).toEqual([]);
// ...but the fixture homes go with the worker.
expect(fs.existsSync(openrigHome)).toBe(false);
expect(fs.existsSync(home)).toBe(false);
expect(fixtureEntries(tmp)).toEqual([]);
});
it.each(["SIGTERM", "SIGINT", "SIGHUP"] as const)("removes both homes and its TMPDIR when the worker is ended by %s, and still dies of it", async (signal) => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fixture-home-signal-"));
roots.push(tmp);
const child = spawn(process.execPath, [
"--import", createRequire(import.meta.url).resolve("tsx"),
path.join(import.meta.dirname, "fixtures/fixture-home-child.ts"), "wait",
], { stdio: ["ignore", "pipe", "inherit"], env: { PATH: process.env.PATH, TMPDIR: tmp } });
const closed = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) =>
child.once("close", (code, sig) => resolve({ code, signal: sig })));
try {
let output = "";
const homes = await new Promise<{ openrigHome: string; home: string; tmpdir: string; scratch: string }>((resolve, reject) => {
child.stdout!.on("data", (chunk: Buffer) => {
output += String(chunk);
const line = output.split("\n").find((l) => l.startsWith("{") && l.endsWith("}"));
if (line) resolve(JSON.parse(line));
});
child.once("close", () => reject(new Error(`child ended before it printed its homes: ${output}`)));
});
// Alive, and with both homes on disk, when the signal arrives.
expect(fs.existsSync(homes.openrigHome)).toBe(true);
expect(fs.existsSync(homes.home)).toBe(true);
expect(fs.existsSync(homes.scratch)).toBe(true);
child.kill(signal);
expect(await closed).toEqual({ code: null, signal });
expect(fs.existsSync(homes.openrigHome)).toBe(false);
expect(fs.existsSync(homes.home)).toBe(false);
expect(fs.existsSync(homes.scratch)).toBe(false);
expect(fs.existsSync(homes.tmpdir)).toBe(false);
expect(leftovers(tmp)).toEqual([]);
} finally {
child.kill("SIGKILL");
}
});
it("leaves a fixture home it only reused, which belongs to whoever made it", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fixture-home-parent-"));
roots.push(tmp);
const parentHome = path.join(tmp, ".openrig-test-fixture-home-parent");
fs.mkdirSync(parentHome);
fs.writeFileSync(path.join(parentHome, ".openrig-fixture"), "");
const { openrigHome, tmpdir } = runSetup({ OPENRIG_HOME: parentHome, TMPDIR: tmp });
expect(openrigHome).toBe(parentHome);
expect(fs.existsSync(path.join(parentHome, ".openrig-fixture"))).toBe(true);
// It did not create the home, so it did not make a TMPDIR of its own either: that one is the parent's.
expect(tmpdir).toBe(tmp);
});
it("never touches an OPENRIG_HOME from the environment that has no marker", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fixture-home-owner-"));
roots.push(tmp);
fs.writeFileSync(path.join(tmp, "state.db"), "owner state");
const { openrigHome } = runSetup({ OPENRIG_HOME: tmp });
expect(openrigHome).not.toBe(tmp);
expect(fs.readFileSync(path.join(tmp, "state.db"), "utf-8")).toBe("owner state");
});
});