mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
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:
1 parent
d1688735d5
commit
5049550203
11 files changed
+100
-45
No files matched your search
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user