mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
When `rig setup` (step 4 of the one-line installer) finishes without being ready, the installer now ends with `INSTALLED, SETUP NOT COMPLETE (exit N).` instead of `FAILED [4/4] … (exit 1)`, which read as "OpenRig didn't install" though the CLI was installed and the compatibility check ran. The summary gives the installed CLI path, the Node/SQLite check result (`passed`, or `SKIPPED (not checked; see above)`), a pointer to setup's own "Some steps need attention" list, with a Fix where setup gives one, or to the error that stopped it, and "Fix those, then run rig setup again; the CLI does not need reinstalling." - Setup's exit status passes through exactly, and setup's own output stays above the summary. - Steps 1-3 keep their `FAILED [n/4]` line, and a ready setup keeps today's closing line. - It names no cause that setup's result doesn't establish: a provider check fails for a missing sign-in but also for a probe that timed out or errored, and the installer sees only setup's exit status, so the summary doesn't say "sign in". - A skipped compatibility check is never reported as passed. Setup's readiness and exit behaviour, provider selection and every other exit status are unchanged, with no extra probe. The README and getting-started are unchanged on purpose: they show the pinned release's installer, which still ends with `FAILED [4/4]`. They move together with the pin, at the first release tag that contains this script. Tests: the wrapper tests cover setup exit 1, 40 and a piped run's 43, unchanged early failures, a skipped check and the ready ending; five setup-boundary cases cover sign-in only, a mixed failure, ready, an unselected provider and an uncertain probe.
214 lines
11 KiB
JavaScript
214 lines
11 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import http from "node:http";
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
|
|
const script = path.join(import.meta.dirname, "install.sh");
|
|
const source = fs.readFileSync(script, "utf8");
|
|
const nodeAdvice = [
|
|
"Node.js 22 or 24 with npm",
|
|
"Distribution packages can be older; check node --version.",
|
|
"official Node installer",
|
|
"Linux route (NodeSource or nvm)",
|
|
"https://github.com/mvschwarz/openrig/blob/main/docs/reference/getting-started.md",
|
|
];
|
|
|
|
function fixture(t, overrides = {}) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openrig-install-"));
|
|
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
const bin = path.join(dir, "tools");
|
|
const prefix = path.join(dir, "npm prefix with spaces");
|
|
const modules = path.join(dir, "global modules with spaces");
|
|
const checker = path.join(modules, "@openrig", "cli", "scripts", "check-abi.mjs");
|
|
fs.mkdirSync(bin);
|
|
fs.mkdirSync(path.join(prefix, "bin"), { recursive: true });
|
|
fs.mkdirSync(path.dirname(checker), { recursive: true });
|
|
fs.writeFileSync(checker, `import fs from "node:fs";
|
|
fs.appendFileSync(process.env.TEST_LOG, "abi:check\\n");
|
|
console.log("ABI native output");
|
|
console.error("ABI native detail");
|
|
process.exitCode = Number(process.env.ABI_EXIT || 0);
|
|
`);
|
|
const write = (file, body) => fs.writeFileSync(file, `#!/bin/sh\n${body}\n`, { mode: 0o755 });
|
|
write(path.join(bin, "node"), 'printf "node:%s\\n" "$*" >>"$TEST_LOG"; if [ "$1" = --version ]; then printf "v22.22.1\\n"; else exec "$TEST_NODE" "$@"; fi');
|
|
write(path.join(bin, "npm"), `
|
|
printf 'npm:%s\n' "$*" >>"$TEST_LOG"
|
|
case "$*" in
|
|
--version) printf '%s\n' "$TEST_NPM_VERSION" ;;
|
|
'install -g @openrig/cli')
|
|
if [ "$SKIP_POSTINSTALL" = 1 ]; then printf 'npm warn install-scripts postinstall skipped\n' >&2; fi
|
|
printf 'npm native output\n'; printf 'npm native error\n' >&2; exit "\${INSTALL_EXIT:-0}" ;;
|
|
'prefix -g') printf '%s\n' "$TEST_PREFIX"; exit "\${PREFIX_EXIT:-0}" ;;
|
|
'root -g') printf '%s\n' "$TEST_MODULES"; exit "\${ROOT_EXIT:-0}" ;;
|
|
*) exit 90 ;;
|
|
esac`);
|
|
write(path.join(bin, "rig"), 'printf "STALE\\n" >>"$TEST_LOG"; exit 91');
|
|
write(path.join(prefix, "bin", "rig"), `
|
|
printf 'installed:%s\n' "$*" >>"$TEST_LOG"
|
|
# Any available stdin would be an implicit answer or unconsumed script bytes.
|
|
if read -r answer; then printf 'unexpected stdin\n' >&2; exit 92; fi
|
|
case "$*" in
|
|
'setup --dry-run') printf 'setup preview\n'; exit "\${PREVIEW_EXIT:-0}" ;;
|
|
setup) printf 'setup native output\n'; printf 'setup native error\n' >&2
|
|
printf 'Next steps: choose providers, then rig up\n'; exit "\${SETUP_EXIT:-0}" ;;
|
|
*) exit 93 ;;
|
|
esac`);
|
|
const env = { PATH: bin, HOME: dir, TEST_PREFIX: prefix, TEST_MODULES: modules, TEST_NODE: process.execPath, TEST_NPM_VERSION: "10.9.4", TEST_LOG: path.join(dir, "calls"), ...overrides };
|
|
return { dir, bin, prefix, checker, env,
|
|
calls: () => fs.existsSync(env.TEST_LOG) ? fs.readFileSync(env.TEST_LOG, "utf8").trim().split("\n") : [],
|
|
run: (...args) => spawnSync("/bin/sh", [script, ...args], { env, encoding: "utf8", timeout: 10000 }),
|
|
};
|
|
}
|
|
|
|
test("dry run needs no tools and only prints the complete plan", t => {
|
|
const syntax = spawnSync("/bin/sh", ["-n", script], { encoding: "utf8" });
|
|
assert.equal(syntax.status, 0, syntax.stderr);
|
|
const f = fixture(t);
|
|
f.env.PATH = path.join(f.dir, "absent");
|
|
const r = f.run("--dry-run");
|
|
assert.equal(r.status, 0, r.stderr);
|
|
for (const text of ["[1/4]", "npm install -g @openrig/cli", "npm root -g", "scripts/check-abi.mjs", "even if npm skipped postinstall", "rig setup --dry-run", "[4/4] rig setup", "both Claude Code and Codex", "rig setup installs herdr by default", "unavailable herdr install is a warning", "Existing cmux settings stay unchanged", "rig setup --ghostty attempts it after acceptance", "launch a team or open a kernel conversation", "Dry run:", ...nodeAdvice]) assert.ok(r.stdout.includes(text), text);
|
|
assert.deepEqual(f.calls(), []);
|
|
});
|
|
|
|
for (const tool of ["node", "npm"]) test(`missing ${tool} reports the prerequisite step`, t => {
|
|
const f = fixture(t);
|
|
fs.unlinkSync(path.join(f.bin, tool));
|
|
const r = f.run();
|
|
assert.equal(r.status, 127);
|
|
assert.match(r.stderr, new RegExp(`Missing prerequisite: ${tool}`));
|
|
for (const text of nodeAdvice) assert.ok(r.stderr.includes(text), text);
|
|
assert.match(r.stderr, /FAILED \[1\/4\].*exit 127/);
|
|
assert.deepEqual(f.calls(), []);
|
|
});
|
|
|
|
for (const [variable, code, step, command] of [
|
|
["INSTALL_EXIT", 37, 2, "npm install -g @openrig/cli"],
|
|
["PREFIX_EXIT", 38, 2, "npm prefix -g"],
|
|
["ROOT_EXIT", 45, 2, "npm root -g"],
|
|
["ABI_EXIT", 44, 2, "check-abi.mjs"],
|
|
["PREVIEW_EXIT", 39, 3, "setup --dry-run"],
|
|
]) test(`${command} failure preserves status and native output`, t => {
|
|
const f = fixture(t, { [variable]: String(code) });
|
|
const r = f.run();
|
|
assert.equal(r.status, code, r.stderr);
|
|
assert.ok(r.stderr.includes(`FAILED [${step}/4]`), r.stderr);
|
|
assert.ok(r.stderr.includes(command), r.stderr);
|
|
assert.ok(r.stderr.includes(`exit ${code}`), r.stderr);
|
|
assert.match(r.stdout, /npm native output/);
|
|
assert.match(r.stderr, /npm native error/);
|
|
assert.ok(!r.stdout.includes("Follow the next steps"));
|
|
if (step < 4) assert.ok(!f.calls().includes("installed:setup"));
|
|
if (variable === "ABI_EXIT") {
|
|
assert.match(r.stdout, /ABI native output/);
|
|
assert.match(r.stderr, /ABI native detail/);
|
|
assert.ok(!r.stdout.includes("Node/SQLite compatibility check passed."));
|
|
assert.ok(!f.calls().includes("installed:setup --dry-run"));
|
|
}
|
|
});
|
|
|
|
test("installed rig wins over stale PATH and a prefix containing spaces works", t => {
|
|
const f = fixture(t);
|
|
const r = f.run();
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.deepEqual(f.calls(), ["node:--version", "npm:--version", "npm:install -g @openrig/cli", "npm:prefix -g", "npm:root -g", `node:${f.checker}`, "abi:check", "installed:setup --dry-run", "installed:setup"]);
|
|
assert.match(r.stdout, /choose providers, then rig up/);
|
|
assert.match(r.stderr, /setup native error/);
|
|
});
|
|
|
|
test("skipped npm postinstall still checks the installed package before setup", t => {
|
|
const f = fixture(t, { SKIP_POSTINSTALL: "1", TEST_NPM_VERSION: "11.19.0" });
|
|
const r = f.run();
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stderr, /npm warn install-scripts postinstall skipped/);
|
|
assert.match(r.stdout, /Node\/SQLite compatibility check passed\./);
|
|
const calls = f.calls();
|
|
assert.equal(calls.filter(x => x === "abi:check").length, 1);
|
|
assert.ok(calls.indexOf("abi:check") < calls.indexOf("installed:setup --dry-run"));
|
|
});
|
|
|
|
test("missing installed checker is explicitly skipped without claiming success", t => {
|
|
const f = fixture(t);
|
|
fs.unlinkSync(f.checker);
|
|
const r = f.run();
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stderr, /Node\/SQLite compatibility check SKIPPED: installed checker not found/);
|
|
assert.ok(!r.stdout.includes("Node/SQLite compatibility check passed."));
|
|
assert.ok(!f.calls().includes("abi:check"));
|
|
assert.ok(f.calls().includes("installed:setup"));
|
|
});
|
|
|
|
test("missing installed executable never falls back to a stale rig", t => {
|
|
const f = fixture(t);
|
|
fs.unlinkSync(path.join(f.prefix, "bin", "rig"));
|
|
const r = f.run();
|
|
assert.equal(r.status, 127);
|
|
assert.match(r.stderr, /FAILED \[2\/4\] locate installed rig/);
|
|
assert.ok(!f.calls().includes("STALE"));
|
|
});
|
|
|
|
test("the actual HTTP download piped to sh preserves setup failure", async t => {
|
|
const f = fixture(t, { SETUP_EXIT: "43" });
|
|
const curl = spawnSync("/bin/sh", ["-c", "command -v curl"], { encoding: "utf8" });
|
|
assert.equal(curl.status, 0, "curl is required for the pipe control");
|
|
fs.symlinkSync(curl.stdout.trim(), path.join(f.bin, "curl"));
|
|
const server = http.createServer((_req, res) => res.end(source));
|
|
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
|
|
t.after(() => new Promise(resolve => server.close(resolve)));
|
|
const url = `http://127.0.0.1:${server.address().port}/install.sh`;
|
|
const child = spawn("/bin/sh", ["-c", 'curl -fsS --max-time 5 "$1" | /bin/sh', "pipe-test", url], { env: f.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
let stdout = "", stderr = "";
|
|
child.stdout.on("data", b => { stdout += b; });
|
|
child.stderr.on("data", b => { stderr += b; });
|
|
const timer = setTimeout(() => child.kill("SIGKILL"), 10000);
|
|
t.after(() => clearTimeout(timer));
|
|
const status = await new Promise((resolve, reject) => { child.on("error", reject); child.on("close", resolve); });
|
|
assert.equal(status, 43, stderr);
|
|
assert.match(stdout, /setup native output/);
|
|
assert.match(stderr, /setup native error/);
|
|
assert.match(stderr, /INSTALLED, SETUP NOT COMPLETE \(exit 43\)/);
|
|
assert.ok(!stderr.includes("FAILED [4/4]"), stderr);
|
|
assert.ok(!f.calls().includes("STALE"));
|
|
});
|
|
|
|
// Step 4 is the one failure after which OpenRig itself is installed. The summary says so and that
|
|
// setup is not complete, keeps setup's own status, and names nothing setup's result doesn't establish:
|
|
// every incomplete setup (a sign-in still needed, a real failure beside it, an uncertain provider
|
|
// probe, a crash) reaches the wrapper only as a non-zero status, so it is never called a sign-in here.
|
|
for (const code of [1, 40]) test(`a rig setup exit ${code} reports the CLI installed and setup not complete, with status ${code}`, t => {
|
|
const f = fixture(t, { SETUP_EXIT: String(code) });
|
|
const r = f.run();
|
|
assert.equal(r.status, code, r.stderr);
|
|
assert.ok(f.calls().includes("installed:setup"));
|
|
assert.match(r.stdout, /setup native output/);
|
|
assert.match(r.stderr, /setup native error/);
|
|
assert.ok(!r.stderr.includes("FAILED"), r.stderr);
|
|
assert.ok(r.stderr.includes(`INSTALLED, SETUP NOT COMPLETE (exit ${code})`), r.stderr);
|
|
assert.ok(r.stderr.includes(`The OpenRig CLI is installed: ${path.join(f.prefix, "bin", "rig")}`), r.stderr);
|
|
assert.match(r.stderr, /Node\/SQLite compatibility check: passed\./);
|
|
assert.match(r.stderr, /"Some steps need attention", with a Fix where setup gives one, or shows the error that stopped it/);
|
|
assert.match(r.stderr, /run rig setup again; the CLI does not need reinstalling/);
|
|
assert.ok(!/sign[ -]?in|login/i.test(r.stderr.slice(r.stderr.indexOf("INSTALLED, SETUP NOT COMPLETE"))), "the summary claims no sign-in");
|
|
assert.ok(!r.stdout.includes("Follow the next steps"));
|
|
});
|
|
|
|
test("an incomplete setup after a skipped compatibility check says the check was skipped, not passed", t => {
|
|
const f = fixture(t, { SETUP_EXIT: "1" });
|
|
fs.unlinkSync(f.checker);
|
|
const r = f.run();
|
|
assert.equal(r.status, 1, r.stderr);
|
|
assert.match(r.stderr, /Node\/SQLite compatibility check: SKIPPED \(not checked; see above\)\./);
|
|
assert.ok(!r.stderr.includes("compatibility check: passed"), r.stderr);
|
|
});
|
|
|
|
test("a setup that finishes ready keeps today's closing line and no summary", t => {
|
|
const f = fixture(t);
|
|
const r = f.run();
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.match(r.stdout, /Follow the next steps printed by rig setup/);
|
|
assert.ok(!r.stderr.includes("SETUP NOT COMPLETE"), r.stderr);
|
|
});
|