test: wait for complete shell fixture output (#1627)

Three daemon tests waited only for a shell fixture's output file to exist, then read it. A redirection or `writeFileSync` creates the file before writing it, so a read in between saw an empty file and failed intermittently.

- **`tmux-agent-environment.test.ts`:** `waitFile` keeps its bound but returns only once the file ends in a newline, which all five writers now print last, and it throws on timeout. A fake-timer case checks that an empty or partial file doesn't resolve it.
- **`claude-executable-foreground.test.ts`:** the recorder writes its JSON to a `.part` file and renames it into place.
- **`transcript-seat-env-native.test.ts`:** the fixture writes its environment to a `.part` file and moves it into place.

Test-only; no runtime file changes, and every payload assertion is unchanged.
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-10-20 09:28:31 -07:00
1 parent a25cd1c341
commit 7296063795
3 files changed
+34 -8

No files matched your search

@@ -44,9 +44,10 @@ for (const shell of ["bash", "zsh", "dash", "ksh"]) {
"const fs = require('node:fs');",
"process.title = 'claude-fixture';",
"process.on('SIGTERM', () => process.exit(0));",
"fs.writeFileSync(process.env.MARKER, JSON.stringify({pid:process.pid,ppid:process.ppid,argv:process.argv.slice(2),",
"fs.writeFileSync(process.env.MARKER + '.part', JSON.stringify({pid:process.pid,ppid:process.ppid,argv:process.argv.slice(2),",
"node:process.env.OPENRIG_NODE_ID,home:process.env.OPENRIG_HOME,renderer:process.env.CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN,",
"resume:process.env.OPENRIG_RESUME_LAUNCH,wrapper:process.env.FIXTURE_WRAPPER,config:process.env.CLAUDE_CONFIG_DIR}));",
"fs.renameSync(process.env.MARKER + '.part', process.env.MARKER);",
"setInterval(() => {}, 1000);",
].join("\n"));
fs.writeFileSync(fake, "#!/bin/sh\nexec " + quote(process.execPath) + " " + quote(recorder) + ' "$@"\n', { mode: 0o755 });
@@ -11,9 +11,34 @@ import { SeatLaunchEnvironment } from "../src/domain/seat-launch-environment.js"
import { shellQuote } from "../src/adapters/shell-quote.js";
const exec = promisify(execFile);
const waitFile = async (file: string) => {
for (let i=0; i<100 && !existsSync(file); i++) await new Promise(r=>setTimeout(r,25));
return readFileSync(file,"utf8").trim().split("|");
// Redirection creates the file before printf writes it; the final newline marks completion.
for (let i = 0; i < 100; i++) {
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
if (text.endsWith("\n")) return text.trim().split("|");
await new Promise(r => setTimeout(r, 25));
}
throw new Error(`Timed out waiting for complete environment result: ${file}`);
};
describe("environment result completion", () => {
it("waits through an empty and partially written file", async () => {
const home = mkdtempSync(path.join(tmpdir(), "tmux-result-"));
const file = path.join(home, "result");
vi.useFakeTimers();
try {
writeFileSync(file, "");
let settled = false;
const result = waitFile(file).then(value => { settled = true; return value; });
await vi.advanceTimersByTimeAsync(25);
expect(settled).toBe(false);
writeFileSync(file, "first|");
await vi.advanceTimersByTimeAsync(25);
expect(settled).toBe(false);
writeFileSync(file, "first|second\n");
await vi.advanceTimersByTimeAsync(25);
expect(await result).toEqual(["first", "second"]);
} finally { vi.useRealTimers(); rmSync(home, { recursive: true, force: true }); }
});
});
describe.skipIf(process.platform === "win32")("#1178 initial-shell environment policy (actual adapter)", () => {
for (const clean of [false,true]) for (const successor of [false,true]) {
it(`${clean ? "daemon baseline" : "stock inherited"}, ${successor ? "successor" : "fresh"}: shell startup and classic function remain usable`, async () => {
@@ -25,7 +50,7 @@ describe.skipIf(process.platform === "win32")("#1178 initial-shell environment p
const cli=path.join(home,"rig-fixture"), rc=path.join(home,"private.bashrc");
writeFileSync(cli,"#!/bin/sh\nexit 0\n",{mode:0o755});
// This deliberately custom default-command must still run; its function and override are intended setup.
writeFileSync(rc, `unset HISTFILE\nprintf '%s|%s|%s|%s' "\${Q1178_SERVER-absent}" "\${Q1178_DAEMON-absent}" "\${Q1178_AUTH-absent}" "\${TMUX_PANE-absent}" > ${shellQuote(initial)}\nexport Q1178_STARTUP=startup Q1178_AUTH=startup\nclaude() { printf '%s|%s|%s|%s' "\${Q1178_SERVER-absent}" "\${Q1178_DAEMON-absent}" "$Q1178_AUTH" "$Q1178_STARTUP" > ${shellQuote(final)}; }\n`);
writeFileSync(rc, `unset HISTFILE\nprintf '%s|%s|%s|%s\\n' "\${Q1178_SERVER-absent}" "\${Q1178_DAEMON-absent}" "\${Q1178_AUTH-absent}" "\${TMUX_PANE-absent}" > ${shellQuote(initial)}\nexport Q1178_STARTUP=startup Q1178_AUTH=startup\nclaude() { printf '%s|%s|%s|%s\\n' "\${Q1178_SERVER-absent}" "\${Q1178_DAEMON-absent}" "$Q1178_AUTH" "$Q1178_STARTUP" > ${shellQuote(final)}; }\n`);
const custom=`exec /bin/bash --noprofile --rcfile ${shellQuote(rc)} -i`;
const selected={HOME:home,PATH:process.env.PATH!,OPENRIG_HOME:home,OPENRIG_NODE_ID:"node",OPENRIG_SESSION_NAME:"seat",Q1178_AUTH:"daemon"};
const baseline={...selected,Q1178_DAEMON:"daemon"};
@@ -72,11 +97,11 @@ describe.skipIf(process.platform === "win32")("#1178 initial-shell environment p
expect((await adapter.createSession("seat",home,{OPENRIG_NODE_ID:"node",OPENRIG_SESSION_NAME:"seat"})).ok).toBe(true);
const pane=await tmux(["display-message","-p","-t","=seat:","#{pane_id}"]);
const inspected=path.join(home,"shell-environment");
await tmux(["send-keys","-t","=seat:","-l",`printf '%s|%s|%s' "$Q1178_DAEMON" "\${Q1178_SERVER-absent}" "$TMUX_PANE" > ${shellQuote(inspected)}`]);
await tmux(["send-keys","-t","=seat:","-l",`printf '%s|%s|%s\\n' "$Q1178_DAEMON" "\${Q1178_SERVER-absent}" "$TMUX_PANE" > ${shellQuote(inspected)}`]);
await tmux(["send-keys","-t","=seat:","Enter"]);
expect(await waitFile(inspected)).toEqual(["daemon","absent",pane]);
const mode=path.join(home,"login-mode");
await tmux(["send-keys","-t","=seat:","-l",`if shopt -q login_shell; then printf login > ${shellQuote(mode)}; else printf nonlogin > ${shellQuote(mode)}; fi`]);
await tmux(["send-keys","-t","=seat:","-l",`if shopt -q login_shell; then printf 'login\\n' > ${shellQuote(mode)}; else printf 'nonlogin\\n' > ${shellQuote(mode)}; fi`]);
await tmux(["send-keys","-t","=seat:","Enter"]);
expect(await waitFile(mode)).toEqual(["login"]);
} finally { await tmux(["kill-server"]).catch(()=>{}); rmSync(home,{recursive:true,force:true,maxRetries:5,retryDelay:20}); }
@@ -106,7 +131,7 @@ describe.skipIf(process.platform === "win32")("#1178 initial-shell environment p
await tmux(["set-option","-g","update-environment","SSH_AUTH_SOCK SSH_ASKPASS Q1178_CUSTOM_UPDATED"]);
await tmux(["set-option","-g","default-shell","/bin/bash"]);
const script=path.join(home,"observe.sh");
writeFileSync(script,`export HISTFILE=/dev/null\nprintf '%s|%s|%s' "$SSH_AUTH_SOCK" "\${SSH_ASKPASS-absent}" "$Q1178_CUSTOM_UPDATED" > ${shellQuote(observed)}\nexec /bin/bash --noprofile --norc\n`);
writeFileSync(script,`export HISTFILE=/dev/null\nprintf '%s|%s|%s\\n' "$SSH_AUTH_SOCK" "\${SSH_ASKPASS-absent}" "$Q1178_CUSTOM_UPDATED" > ${shellQuote(observed)}\nexec /bin/bash --noprofile --norc\n`);
await tmux(["set-option","-g","default-command",`exec /bin/bash ${shellQuote(script)}`]);
expect((await adapter.createSession("seat",home,{OPENRIG_NODE_ID:"owned-node"})).ok).toBe(true);
expect(await waitFile(observed)).toEqual(["/synthetic/daemon-agent.sock","absent","daemon-custom"]);
@@ -28,7 +28,7 @@ describe.skipIf(process.platform === "win32")("native transcript seat environmen
const adapter = new TmuxAdapter(async (cmd) => (await run(`tmux -S ${shellQuote(socket)} ${cmd.slice(5)}`, { env })).stdout,
undefined, argv ? async (args) => (await runFile(args[0]!, ["-S", socket, ...args.slice(1)], { env })).stdout : undefined);
expect(await adapter.createSession("fixture", temp)).toEqual({ ok: true });
await runFile("tmux", ["-S", socket, "respawn-pane", "-k", "-t", "=fixture:", `/usr/bin/env > ${shellQuote(captured)}; sleep 60`], { env });
await runFile("tmux", ["-S", socket, "respawn-pane", "-k", "-t", "=fixture:", `/usr/bin/env > ${shellQuote(captured + ".part")} && mv ${shellQuote(captured + ".part")} ${shellQuote(captured)}; sleep 60`], { env });
const deadline = Date.now() + 2000;
while (!fs.existsSync(captured) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 10));
const seatEnv = Object.fromEntries(fs.readFileSync(captured, "utf8").split("\n").filter((line) => line.includes("=")).map((line) => {