test: stabilize daemon fixtures under startup contention (#2628)

Makes the daemon test fixtures hold on macOS paths and under startup contention, without weakening any assertion: readiness waits instead of fixed sleeps, temp-root resolution through the real path, a script-file shell for long commands, serialized mutations where tests raced, a long-path command, and the shared hermetic test setup. The refocus-overlap cell still asserts the lost pendingOn race, now through a deterministic handshake (the time budget only scales timeouts). The existing Linux CI job that covers /proc seat environments also runs pi-shell-launch-native.test.ts, and the Linux long-command session tokens stay within their limit. Test and setup only; no product code changes. Reported by seanmcn in #2627.
This commit is contained in:
Seán McNamara authored and GitHub committed 2026-11-06 20:15:53 -08:00
1 parent d1688735d5
commit 5049550203
11 files changed
+100 -45

No files matched your search

+2 -2
View File
@@ -79,7 +79,7 @@ jobs:
linux-seat-environment:
if: ${{ github.event_name != 'push' }}
# This integration test reads /proc/<pid>/environ and is skipped on macOS.
# Cover /proc seat environments and Linux canonical-input limits that macOS cannot exercise.
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
@@ -108,7 +108,7 @@ jobs:
ip link set lo up
exec runuser -u "$1" -- env -i HOME="$2/home" PATH="$3" TMPDIR="$2" TMUX_TMPDIR="$2" \
CI=true TERM=xterm-256color \
npm run test -w packages/daemon -- test/seat-env-parent-session.integration.test.ts --maxWorkers=1 --reporter=verbose
npm run test -w packages/daemon -- test/seat-env-parent-session.integration.test.ts test/pi-shell-launch-native.test.ts --maxWorkers=1 --reporter=verbose
SUITE
package-tests:
@@ -142,14 +142,17 @@ describe("a Claude member's own subscription token (claude_token)", () => {
const dir = tempDir(), tokenFile = path.join(dir, "dev.token");
writeFileSync(tokenFile, `${SYNTHETIC}\n`, { mode: 0o600 });
const step = claudeSeatTokenStep({ label: "dev-sub", tokenFile }, shellQuote);
const traced = spawnSync("/bin/sh", ["-xc", `${step} && /bin/sh -c 'printf %s "$CLAUDE_CODE_OAUTH_TOKEN"'; echo traced-after`],
const script = path.join(dir, "launch.sh");
writeFileSync(script, `${step} && /bin/sh -c 'printf %s "$CLAUDE_CODE_OAUTH_TOKEN"'; echo traced-after`);
const traced = spawnSync("/bin/sh", ["-x", script],
{ encoding: "utf8", env: { PATH: "/usr/bin:/bin" } });
expect(traced.status, traced.stderr).toBe(0);
expect(traced.status, JSON.stringify({ stderr: traced.stderr, signal: traced.signal, error: traced.error })).toBe(0);
expect(traced.stdout).toBe(`${SYNTHETIC}traced-after\n`);
expect(traced.stderr).not.toContain(SYNTHETIC);
expect(traced.stderr).toContain("echo traced-after");
const missing = claudeSeatTokenStep({ label: "dev-sub", tokenFile: path.join(dir, "missing.token") }, shellQuote, { required: true });
const failed = spawnSync("/bin/sh", ["-xc", `${missing}; echo "rc=$?"`], { encoding: "utf8", env: { PATH: "/usr/bin:/bin" } });
writeFileSync(script, `${missing}; echo "rc=$?"`);
const failed = spawnSync("/bin/sh", ["-x", script], { encoding: "utf8", env: { PATH: "/usr/bin:/bin" } });
expect(failed.stdout).toBe("rc=1\n");
});
+3 -2
View File
@@ -16,6 +16,7 @@ import { provenCodexResume } from "./helpers/codex-resume-proof.js";
const exec = promisify(execFile), roots: string[] = [];
afterEach(() => { vi.restoreAllMocks(); for (const r of roots.splice(0)) fs.rmSync(r, { recursive: true, force: true }); });
/** Build a private launch environment whose scripted Codex reports its selected home. */
function fixture() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-seat-home-")); roots.push(root);
const home = path.join(root, "home"), daemonHome = path.join(root, "daemon codex"), seatHome = path.join(root, "seat's codex");
@@ -33,7 +34,7 @@ function fixture() {
const fsOps = { homedir: home, readFile: (p: string) => fs.readFileSync(p, "utf8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c),
exists: fs.existsSync, mkdirp: (p: string) => { fs.mkdirSync(p, { recursive: true }); } };
const launchEnv = new (SeatLaunchEnvironment as any)(tmux, env, root, cli, daemonHome) as SeatLaunchEnvironment;
const run = (cmd: string) => exec("/bin/bash", ["--noprofile", "--norc", "-c", cmd], { cwd, env, timeout: 5000 });
const run = (cmd: string) => exec("/bin/bash", ["--noprofile", "--norc", "-c", cmd], { cwd, env, timeout: 15000 });
const seatHomeOf = (nodeId: string) => nodeId === "node" ? seatHome : undefined;
return { root, home, daemonHome, seatHome, cwd, env, tmux, fsOps, launchEnv, run, seatHomeOf };
}
@@ -170,7 +171,7 @@ it("the legacy Codex resume launches in the seat's own home when one is passed,
expect((await adapter.resume("seat@rig", "codex_id", "old", f.cwd)).ok).toBe(true);
expect(await homeOf()).toBe(f.daemonHome);
expect(ensured).toEqual([f.seatHome, undefined]);
});
}, 35_000);
it("context usage finds a thread's state in a declared seat home", () => {
const f = fixture(); seed(f.seatHome, "seat-thread");
@@ -10,6 +10,7 @@ import { createRequire } from "node:module";
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); });
/** Run the real setup in a child and return its paths after exit-time cleanup. */
function runSetup(env: Record<string, string | undefined> = {}) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "fixture-home-run-"));
roots.push(tmp);
@@ -48,6 +49,18 @@ describe("fixture homes are removed when the worker exits", () => {
expect(leftovers(tmp)).toEqual([]);
});
it.skipIf(process.platform !== "darwin").each([false, true])("handles an explicit macOS per-user TMPDIR (keep=%s)", keep => {
const systemTmp = spawnSync("getconf", ["DARWIN_USER_TEMP_DIR"], { encoding: "utf8" });
expect(systemTmp.status, systemTmp.stderr).toBe(0);
const supplied = fs.mkdtempSync(path.join(fs.realpathSync(systemTmp.stdout.trim()), "fixture-override-"));
roots.push(supplied);
const { openrigHome, tmpdir, scratch } = runSetup({ TMPDIR: supplied, OPENRIG_TEST_KEEP_TMPDIR: keep ? "1" : undefined });
expect(path.dirname(openrigHome)).toBe(keep ? supplied : fs.realpathSync("/tmp"));
expect(keep ? tmpdir : path.dirname(tmpdir)).toBe(keep ? supplied : fs.realpathSync("/tmp"));
expect(fs.existsSync(openrigHome)).toBe(false);
expect(leftovers(supplied)).toEqual(keep ? [path.basename(scratch)] : []);
}, 65_000);
it("removes the fixture home and the user home", () => {
const { tmp, openrigHome, home } = runSetup();
expect(path.dirname(openrigHome)).toBe(tmp);
@@ -24,7 +24,7 @@ const quote = (s: string) => "'" + s.replace(/'/g, "'\"'\"'") + "'";
// The runner entry is deliberately offline: this exercises the real adapter,
// tmux, canonical macOS tty and shell, without starting Pi or touching provider settings.
describe.skipIf(!hasTmux || process.platform === "win32")("Pi launch through native tty", () => {
it.each([...["fresh", "fork", "resume", "short-long-tmpdir", "fresh-long-tmpdir", "fork-long-tmpdir", "resume-long-tmpdir"].flatMap(mode => [
it.each([...(process.platform === "linux" ? [{ mode: "fresh-linux-long", canonical: true }] : []), ...["fresh", "fork", "resume", "short-long-tmpdir", "fresh-long-tmpdir", "fork-long-tmpdir", "resume-long-tmpdir"].flatMap(mode => [
{ mode, canonical: true }, { mode, canonical: false },
]), ...["fresh-routing", "fork-routing", "resume-routing", "fork-routing-long-tmpdir", "resume-routing-long-tmpdir"].map(mode => ({ mode, canonical: false }))])("preserves $mode runner arguments (canonical reader: $canonical)", async ({ mode, canonical }) => {
const launchMode = mode.split("-")[0];
@@ -33,9 +33,12 @@ describe.skipIf(!hasTmux || process.platform === "win32")("Pi launch through nat
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "pi-shell-"));
const socket = path.join(temp, "tmux.sock");
const session = "pi-fixture";
const long = Array.from({ length: 8 }, () => "nested-directory-" + "x".repeat(35)).join(path.sep);
const long = Array.from({ length: 14 }, () => "nested-directory-" + "x".repeat(35)).join(path.sep);
const stateRoot = path.join(temp, longTmp ? "short" : long, "state");
const cwd = path.join(temp, longTmp ? "short" : long, "project with 'quotes'");
// Keep session tokens below 1024 characters; Linux's oversized command comes from cwd instead.
const projectPath = mode === "fresh-linux-long"
? Array.from({ length: 64 }, () => "nested-directory-" + "x".repeat(35)).join(path.sep) : long;
const cwd = path.join(temp, longTmp ? "short" : projectPath, "project with 'quotes'");
fs.mkdirSync(cwd, { recursive: true });
const routeServer = routing ? createServer((req, res) => {
res.end(JSON.stringify({ instance: "intended", node: req.headers["x-node"] }));
@@ -74,7 +77,8 @@ setInterval(() => {}, 1000);
const parent = path.join(stateRoot, session, "sessions", "parent.jsonl");
fs.mkdirSync(path.dirname(parent), { recursive: true });
fs.writeFileSync(parent, "fixture\n");
const model = mode === "short-long-tmpdir" ? undefined : "provider/model-" + "m".repeat(longTmp ? 400 : 7000);
// Long paths exceed the terminal bound without an unrealistic multi-kilobyte model argument.
const model = mode === "short-long-tmpdir" ? undefined : "provider/model-" + "m".repeat(400);
const expected = buildPiRunnerCommand({ runnerEntryPath: runner, sessionName: session,
stateRoot, cwd, model, launchId: "attempt", trust: "no-approve",
sessionFile: launchMode === "resume" ? parent : undefined,
@@ -83,7 +87,7 @@ setInterval(() => {}, 1000);
else if (longTmp) {
expect(Buffer.byteLength(expected)).toBeGreaterThan(512);
expect(Buffer.byteLength(expected)).toBeLessThan(1024);
} else expect(Buffer.byteLength(expected)).toBeGreaterThan(7000);
} else expect(Buffer.byteLength(expected)).toBeGreaterThan(mode === "fresh-linux-long" ? 4096 : 2000);
const fsOps = { readFile: (p: string) => fs.readFileSync(p, "utf8"),
writeFile: (p: string, c: string) => fs.writeFileSync(p, c),
exists: fs.existsSync, mkdirp: (p: string) => { fs.mkdirSync(p, { recursive: true }); } };
@@ -130,7 +134,8 @@ setInterval(() => {}, 1000);
expect(result, JSON.stringify(result)).toMatchObject({ ok: true });
} else {
const adapter = new PiRuntimeAdapter({ tmux, fsOps, stateRoot, runnerEntryPath: runner, seatLaunchEnvironment,
sleep: () => new Promise(resolve => setTimeout(resolve, 25)), newLaunchId: () => "attempt" });
// The adapter polls 60 times for readiness; 100 ms each gives a loaded runner ~6 s, like the resume path's 4 s.
sleep: () => new Promise(resolve => setTimeout(resolve, 100)), newLaunchId: () => "attempt" });
const result = await adapter.launchHarness({ tmuxSession: session, cwd, model, nodeId: "fixture-node", launchGeneration: "reserved-generation" } as never,
{ name: session, ...(launchMode === "fork" ? { forkSource: { kind: "native_id" as const, value: parent } } : {}) });
expect(result, JSON.stringify(result)).toMatchObject({ ok: true });
@@ -333,17 +338,18 @@ await main();
seatLaunchEnvironment, maxWaitMs: 6000, pollMs: 25, newLaunchId: () => "attempt",
});
const result = await adapter.resume(session, "pi_session_file", parent, cwd, model, "floor", "high", wrapper);
expect(result).toMatchObject({ ok: true });
expect(result, JSON.stringify(result)).toMatchObject({ ok: true });
} else {
const adapter = new PiRuntimeAdapter({
tmux, fsOps, stateRoot, runnerEntryPath: runner, seatLaunchEnvironment,
sleep: () => new Promise(resolve => setTimeout(resolve, 25)), newLaunchId: () => "attempt",
// Match the resume fixture's six-second readiness allowance for wrapper and child startup.
sleep: () => new Promise(resolve => setTimeout(resolve, 100)), newLaunchId: () => "attempt",
});
const result = await adapter.launchHarness(
{ tmuxSession: session, cwd, model, effort: "high", nodeId: "fixture-node", launchGeneration: "reserved-generation", launchWrapper: wrapper } as never,
{ name: session, ...(launchMode === "fork" ? { forkSource: { kind: "native_id" as const, value: parent } } : {}) },
);
expect(result).toMatchObject({ ok: true });
expect(result, JSON.stringify(result)).toMatchObject({ ok: true });
}
expect(fs.existsSync(bareVersionMarker)).toBe(false);
@@ -652,9 +658,8 @@ setInterval(() => {}, 1000);
expect(res).toMatchObject({ ok: true });
}
for (let i = 0; i < 100 && !fs.existsSync(codexRecordPath); i++) {
await new Promise(resolve => setTimeout(resolve, 20));
}
// The wrapper and its Node child can take more than two seconds to start under suite load.
await vi.waitFor(() => expect(fs.existsSync(codexRecordPath)).toBe(true), { timeout: 10_000, interval: 25 });
const wrapperRec = JSON.parse(fs.readFileSync(wrapperRecordPath, "utf8"));
const codexRec = JSON.parse(fs.readFileSync(codexRecordPath, "utf8"));
expect(wrapperRec.wrapperPid).toBe(codexRec.codexPid);
@@ -671,14 +676,12 @@ setInterval(() => {}, 1000);
expect(codexRec.generation).toBe(mode === "resume" ? "codex-gen-1" : "codex-gen-2");
process.kill(codexRec.codexPid, "SIGUSR2");
for (let i = 0; i < 100 && !fs.existsSync(codexExitFile); i++) {
await new Promise(resolve => setTimeout(resolve, 20));
}
await vi.waitFor(() => expect(fs.existsSync(codexExitFile)).toBe(true), { timeout: 10_000, interval: 25 });
expect(codexRec.signalHandlerReady).toBe(true);
expect(JSON.parse(fs.readFileSync(codexExitFile, "utf8"))).toEqual({ code: 13, pid: codexRec.codexPid });
} finally {
await run("tmux", ["-S", socket, "kill-server"]).catch(() => {});
fs.rmSync(temp, { recursive: true, force: true });
}
}, 10_000);
}, 30_000);
});
@@ -33,8 +33,9 @@ afterEach(async () => {
function alive(pid: number): boolean {
try { process.kill(pid, 0); return true; } catch { return false; }
}
/** Wait for a spawned probe to publish its PID and register it for cleanup. */
async function readPid(file: string): Promise<number> {
for (let i = 0; i < 200; i++) {
for (let i = 0; i < 600; i++) {
const text = fs.existsSync(file) ? fs.readFileSync(file, "utf-8").trim() : "";
if (text) { seenPids.add(Number(text)); return Number(text); }
await new Promise((r) => setTimeout(r, 25));
@@ -103,7 +104,7 @@ describe.skipIf(process.platform === "win32")("runtime probe groups end when the
} finally {
process.env.PATH = savedPath;
}
});
}, 60_000);
it("reports a probe the daemon's stop ended as cut short, not as a failed runtime", () => {
expect(runtimeProbeFailure(new ProbeStoppedError("codex --version", "ended"))).toBe("check cut short: the daemon is stopping");
@@ -276,7 +277,7 @@ describe.skipIf(process.platform === "win32")("a daemon that stops during a runt
const outcome = verifying.then(() => undefined, (err: unknown) => err);
// Each probe writes its own marker once its trap is installed: stop only when both are up.
const markers = () => fs.readdirSync(root).filter((name) => name.startsWith("ready.") && fs.readFileSync(path.join(root, name), "utf-8").trim());
for (let i = 0; i < 400 && markers().length < 2; i++) await new Promise((r) => setTimeout(r, 25));
for (let i = 0; i < 600 && markers().length < 2; i++) await new Promise((r) => setTimeout(r, 25));
expect(markers()).toHaveLength(2);
for (const name of markers()) await readPid(path.join(root, name));
@@ -3,7 +3,7 @@
// Each case runs the real hook as a harness does (JSON on stdin, killed at the registration's outer bound), with a stub
// `rig` and a stub trace script whose delays are set per case. The overlaps are hook processes overlapping; whether a
// harness overlaps these events natively is not established here.
import { afterEach, describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { spawn } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
@@ -25,6 +25,7 @@ afterEach(() => {
interface Fixture { home: string; bin: string; log: string }
/** Build isolated hook state and controllable lookup scripts for timing and overlap cases. */
function fixture(): Fixture {
root = mkdtempSync(join(tmpdir(), "refocus-budget-"));
const home = join(root, "openrig");
@@ -45,6 +46,10 @@ case "$1 $2" in
esac
`);
writeFileSync(join(bin, "trace"), `#!/bin/sh
[ -n "$STUB_TRACE_READY" ] && touch "$STUB_TRACE_READY"
if [ -n "$STUB_TRACE_RELEASE" ]; then
while [ ! -f "$STUB_TRACE_RELEASE" ]; do sleep 0.02; done
fi
[ -n "$STUB_TRACE_PARTIAL" ] && echo "$STUB_TRACE_PARTIAL"
${sleep("STUB_TRACE_MS")}
echo "TRACE OK: work and topology"
@@ -145,10 +150,10 @@ describe("refocus: the time budget", () => {
}, 20_000);
it("with no budget selected, gives main's result: each lookup keeps its 2 s cap, unclipped, inside the 60 s bound", async () => {
// Lookups each under their own 2 s cap (with room for a slow runner's process start-up) that together pass 5 s: on
// Lookups each under their own 2 s cap (with room for a slow runner's process start-up) that together pass the 4.5 s lookup budget: on
// main's 5 s registration this was killed; now it completes as main's code does.
const f = fixture();
const slow = { STUB_WHOAMI_MS: "1700", STUB_TRACE_MS: "1700", STUB_CONTEXT_MS: "1700", OPENRIG_REFOCUS_CONTENT_REF: "refocus/team.md",
const slow = { STUB_WHOAMI_MS: "1550", STUB_TRACE_MS: "1550", STUB_CONTEXT_MS: "1550", OPENRIG_REFOCUS_CONTENT_REF: "refocus/team.md",
OPENRIG_REFOCUS_WORK_NODE: "", OPENRIG_REFOCUS_TREES: "both" };
await hook(f, "PostCompact", "s1");
const r = await hook(f, "UserPromptSubmit", "s1", slow);
@@ -210,10 +215,19 @@ describe("refocus: overlapping events for one occupant (UNRESOLVED; characterize
await hook(f, "UserPromptSubmit", "s1", { OPENRIG_REFOCUS_NOW: "1" });
const file = join(f.home, "refocus", `${SEAT}__s1.json`);
writeFileSync(file, JSON.stringify({ ...stateOf(f, "s1"), pendingOn: "Stop", pendingAt: "2026-10-18T17:00:00.000Z" }));
const delivering = hook(f, "UserPromptSubmit", "s1", { STUB_TRACE_MS: "1500" });
await wait(600);
await hook(f, "PostCompact", "s1");
expect(stateOf(f, "s1")).toMatchObject({ pendingOn: "PostCompact" });
const ready = join(root!, "trace-ready");
const release = join(root!, "trace-release");
// Hold delivery after it has read the state; process start-up time cannot choose the interleaving.
const delivering = hook(f, "UserPromptSubmit", "s1", {
OPENRIG_REFOCUS_BUDGET_MS: "52000", STUB_TRACE_READY: ready, STUB_TRACE_RELEASE: release,
});
try {
await vi.waitFor(() => expect(existsSync(ready)).toBe(true), { timeout: 10000 });
await hook(f, "PostCompact", "s1");
expect(stateOf(f, "s1")).toMatchObject({ pendingOn: "PostCompact" });
} finally {
writeFileSync(release, "");
}
expect(context(await delivering)).toContain("REFOCUS");
// The delivery wrote back the state it read before the PostCompact: the compaction's pending is gone.
expect(stateOf(f, "s1")["pendingOn"]).toBeUndefined();
@@ -250,7 +264,7 @@ describe("refocus: state left by an earlier candidate", () => {
writeFileSync(file, JSON.stringify({ ...stateOf(f, "s1"), pendingSeq: 3,
delivering: { token: "left-behind", at: new Date().toISOString(), pendingSeq: 3 } }));
writeFileSync(`${file}.lock`, "left-behind");
const r = await hook(f, "UserPromptSubmit", "s1");
const r = await hook(f, "UserPromptSubmit", "s1", { OPENRIG_REFOCUS_BUDGET_MS: "10000" });
expect(context(r)).toContain("REFOCUS (just compacted");
expect(stateOf(f, "s1")).toMatchObject({ firedOn: "UserPromptSubmit" });
}, 20_000);
@@ -72,7 +72,8 @@ describe.skipIf(!hasTmux)("the runner conformance check", () => {
}, 60_000);
// Each break, the case that must catch it, and the check in that case that names it.
it.concurrent.each([
// Each mutation launches many tmux sessions; overlap can fail startup before reaching the intended break.
it.each([
["record-early", "1", "no launch record while session/new is still unanswered"],
["record-on-failure", "1b", "no conversation in a launch record"],
["split-lines", "2", "one pasted block is exactly one session/prompt"],
@@ -107,6 +107,7 @@ async function daemon(options: { people: string[]; owner?: string }) {
// The test double with the fake agent, given the environment a runner-preset launch gives it, on plain pipes. Input
// arrives as `rig send` delivers it: one bracketed paste, then Enter.
/** Start a scripted runner and wait for its native reference before testing permission routes. */
async function launchRunner(d: Awaited<ReturnType<typeof daemon>>, scenario: object = { turns: [{ permission: true }, {}] }) {
const dir = mkdtempSync(join(tmpdir(), "runner-real-routes-"));
const log = join(dir, "agent.jsonl"), record = join(dir, "record.json");
@@ -128,7 +129,12 @@ async function launchRunner(d: Awaited<ReturnType<typeof daemon>>, scenario: obj
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
const entries = (): LogEntry[] => !existsSync(log) ? [] : readFileSync(log, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
const opened = await waitFor(() => existsSync(record) && JSON.parse(readFileSync(record, "utf8")).nativeRef);
// Shell, runner and agent startup can be slower than the route checks under suite load.
const opened = await waitFor(() => {
if (!existsSync(record)) return undefined;
try { return JSON.parse(readFileSync(record, "utf8")).nativeRef; }
catch { return undefined; } // The runner may still be writing the record.
}, 15000);
expect(opened, "the runner opens its conversation and writes the launch record").toBeTruthy();
return {
send: (text: string) => child.stdin.write(`\x1b[200~${text}\x1b[201~\r`),
@@ -159,7 +165,7 @@ describe("the conformance check's test double against OpenRig's real permission
const idle = await waitFor(() => { const latest = d.latest(); return latest?.state === "idle" && latest; });
expect(idle).toMatchObject({ generationProvenance: "resolved", stale: false });
expect(r.answers()).toHaveLength(1);
}, 30_000);
}, 60_000);
it("OpenRig's permit, with no one to ask, is delivered at once and opens no ask", async () => {
const d = await daemon({ people: [] });
@@ -170,7 +176,7 @@ describe("the conformance check's test double against OpenRig's real permission
expect(d.asks()).toEqual([]);
expect(await waitFor(() => r.turnEnded())).toBe(true);
expect(d.waitsOnPerson()).toBe(false);
}, 30_000);
}, 60_000);
it("the seat's opt-in refusal (denied_by_seat) is refused at once and held as needs input", async () => {
const d = await daemon({ people: ["mike"], owner: "deny" });
@@ -183,7 +189,7 @@ describe("the conformance check's test double against OpenRig's real permission
expect(await waitFor(() => r.turnEnded())).toBe(true);
await sleep(300);
expect(d.waitsOnPerson(), "no idle post replaces the held attention").toBe(true);
}, 30_000);
}, 60_000);
it("a person's no, when the agent offered no reject option, reaches the agent as cancelled, never a grant, holding nothing", async () => {
const d = await daemon({ people: ["mike"] });
@@ -199,7 +205,7 @@ describe("the conformance check's test double against OpenRig's real permission
// No person is still waiting, so the turn's end is reported and the seat doesn't read as needing one.
expect(await waitFor(() => d.latest()?.state === "idle")).toBe(true);
expect(d.waitsOnPerson()).toBe(false);
}, 30_000);
}, 60_000);
it("an earlier occupant's request is refused at submit (stale_generation), so the runner answers cancelled, opens no ask, and its posts don't count", async () => {
const d = await daemon({ people: ["mike"] });
@@ -213,5 +219,5 @@ describe("the conformance check's test double against OpenRig's real permission
expect(d.asks()).toEqual([]);
expect(await waitFor(() => r.turnEnded())).toBe(true);
expect(await waitFor(() => d.latest()?.reason === "generation_mismatch" && d.latest())).toMatchObject({ state: "unknown" });
}, 30_000);
}, 60_000);
});
@@ -58,10 +58,12 @@ function fixture() {
cleanup.push(() => new Promise<void>((r, reject) => server.close(e => e ? reject(e) : r())));
env.OPENRIG_URL = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
}
/** Execute the launch envelope and verify the child received the fixture connection and environment. */
async function execute(command: string, managed = false) {
expect(command).not.toContain(env.OPENRIG_ACTIVITY_HOOK_TOKEN);
expect(command).not.toContain(env.ANTHROPIC_API_KEY);
const { stdout } = await exec("/bin/bash", ["--noprofile", "--rcfile", rc, "-ic", command], { env, cwd: binding.cwd, timeout: 5000 });
// This starts a shell, a Node fixture and its CLI child before the local HTTP check.
const { stdout } = await exec("/bin/bash", ["--noprofile", "--rcfile", rc, "-ic", command], { env, cwd: binding.cwd, timeout: 15_000 });
const observed = JSON.parse(stdout);
expect(observed).toMatchObject({ body: { node: "node-current" }, home: env.OPENRIG_HOME, node: "node-current", OPENRIG_DB: env.OPENRIG_DB, TMUX_TMPDIR: env.TMUX_TMPDIR });
if (!managed) expect(observed).toMatchObject({ generation: "successor-generation", HOME: "/user/home", CODEX_HOME: "/user/codex", CLAUDE_CONFIG_DIR: "/user/claude", USER_VALUE: "keep" });
+14 -3
View File
@@ -71,6 +71,17 @@ if (process.env["TERM_PROGRAM"] === "rex") delete process.env["TERM_PROGRAM"];
// 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.
// Where the fixture home and the worker TMPDIR go: the given temp folder with its symlinks resolved, so a path a
// test builds and the same path the product resolves are spelled alike. macOS's default is a per-user folder under
// /var/folders, ~57 characters once resolved, which leaves too little of the 104-byte Unix socket cap for the tmux
// and daemon sockets under it; in its place the base is /tmp, resolved (/private/tmp), as the CI job gets by running
// with a short resolved TMPDIR. This includes explicit overrides under /private/var/folders; set
// OPENRIG_TEST_KEEP_TMPDIR=1 to preserve those exactly. Other roots are kept. mkdtemp makes each folder 0700.
// A package that keeps its TMPDIR (OPENRIG_TEST_KEEP_TMPDIR=1) keeps it here too, unresolved.
const keepTmpdir = process.env["OPENRIG_TEST_KEEP_TMPDIR"] === "1";
const givenTmp = fs.realpathSync(os.tmpdir());
const tmpBase = keepTmpdir ? os.tmpdir()
: process.platform === "darwin" && givenTmp.startsWith("/private/var/folders/") ? fs.realpathSync("/tmp") : givenTmp;
const existingHome = process.env["OPENRIG_HOME"];
let createdHome: string | undefined;
let createdTmp: string | undefined;
@@ -78,7 +89,7 @@ const fixtureHome =
existingHome && fs.existsSync(path.join(existingHome, ".openrig-fixture"))
? existingHome
: (() => {
const h = fs.mkdtempSync(path.join(os.tmpdir(), ".openrig-test-fixture-home-"));
const h = fs.mkdtempSync(path.join(tmpBase, ".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
@@ -86,8 +97,8 @@ const fixtureHome =
// 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-"));
if (!keepTmpdir) {
createdTmp = fs.mkdtempSync(path.join(tmpBase, "o-"));
process.env["TMPDIR"] = createdTmp;
}
return h;