diff --git a/packages/cli/test/setup-output.test.ts b/packages/cli/test/setup-output.test.ts index 1804a759b..6a0446831 100644 --- a/packages/cli/test/setup-output.test.ts +++ b/packages/cli/test/setup-output.test.ts @@ -113,3 +113,60 @@ it.each([ const result = JSON.parse(logs[0]!) as SetupResult; expect(result.steps.find(step => step.id === "ghostty_install")?.message).toContain(expected); }); + +// The installer's step 4 consumes only `rig setup`'s exit status and prints setup's own output. These five +// cases pin that boundary: an incomplete setup exits 1 and lists what is left (each with its Fix) under +// "Some steps need attention"; a ready one exits 0 with "Setup complete.". A failed provider probe is listed +// as "not ready to launch" with the probe's own error, whatever the cause, so the installer names no sign-in. +describe("the installer's setup boundary", () => { + const run = async (exec: (cmd: string) => string, flags: string[] = []) => { + process.exitCode = undefined; + vi.stubGlobal("fetch", vi.fn(async () => new Response(null, { status: 503 }))); + const logs: string[] = []; + vi.spyOn(console, "log").mockImplementation((...args) => { logs.push(args.map(String).join(" ")); }); + const deps: SetupDeps = { platform: "darwin", env: {}, exec: vi.fn(exec), readFile: () => null, exists: () => false, writeFile: vi.fn(), mkdirp: vi.fn() }; + await new Command().addCommand(setupCommand(deps)).parseAsync(["node", "rig", "setup", ...flags]); + const output = logs.join("\n"); + return { exitCode: process.exitCode, output, attention: output.slice(output.indexOf("Some steps need attention:")) }; + }; + const ok = (cmd: string) => cmd === "cmux capabilities --json" ? '{"capabilities":[]}' : "available"; + + it("a sign-in only: exit 1, the provider listed with its Fix", async () => { + const r = await run((cmd) => { if (cmd === "claude auth status") throw new Error("Not logged in"); return ok(cmd); }); + expect(r.exitCode).toBe(1); + expect(r.attention).toContain("claude_auth: Claude Code is installed but not ready to launch: Not logged in"); + expect(r.attention).toContain("Fix: Run `claude auth login`"); + }); + + it("a real failure beside a sign-in: exit 1, both listed", async () => { + const r = await run((cmd) => { + if (cmd === "claude auth status") throw new Error("Not logged in"); + if (cmd === "codex --version" || cmd === "npm install -g @openai/codex") throw new Error("npm ERR! network"); + return ok(cmd); + }); + expect(r.exitCode).toBe(1); + expect(r.attention).toContain("claude_auth:"); + expect(r.attention).toContain("codex_install:"); + }); + + it("ready: exit 0 and Setup complete", async () => { + const r = await run(ok); + expect(r.exitCode).toBeUndefined(); + expect(r.output).toContain("Setup complete."); + expect(r.output).not.toContain("Some steps need attention:"); + }); + + it("an unselected provider is not a failure: exit 0 with only the chosen provider checked", async () => { + const r = await run((cmd) => { if (cmd.startsWith("codex")) throw new Error("codex absent"); return ok(cmd); }, ["--providers", "claude"]); + expect(r.exitCode).toBeUndefined(); + expect(r.output).toContain("Setup complete."); + expect(r.output).toContain("codex_auth: Not selected"); + }); + + it("an uncertain probe is listed with its own error, not as a confirmed missing login", async () => { + const r = await run((cmd) => { if (cmd === "claude auth status") throw new Error("Command timed out after 10000ms"); return ok(cmd); }); + expect(r.exitCode).toBe(1); + expect(r.attention).toContain("claude_auth: Claude Code is installed but not ready to launch: Command timed out after 10000ms"); + expect(r.attention).not.toMatch(/not logged in/i); + }); +}); diff --git a/scripts/install.sh b/scripts/install.sh index d6799dabd..abac7e614 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -21,6 +21,20 @@ install_step() { fi } +# Step 4 is the one failure after which OpenRig itself is installed: say so, and that setup is +# not complete, keeping setup's own status. Its output above names what is left; this summary +# names nothing it cannot see (a failed provider check is not proof of a missing sign-in). +install_setup_incomplete() { + install_exit=$1 + printf '\nINSTALLED, SETUP NOT COMPLETE (exit %s).\n' "$install_exit" >&2 + printf 'The OpenRig CLI is installed: %s\n' "$install_rig" >&2 + printf 'Node/SQLite compatibility check: %s.\n' "$install_abi" >&2 + printf '%s\n' 'rig setup did not finish ready. Its output above lists what is left under' \ + '"Some steps need attention", with a Fix where setup gives one, or shows the error that stopped it.' \ + 'Fix those, then run rig setup again; the CLI does not need reinstalling.' >&2 + exit "$install_exit" +} + install_main() { case "$#:$1" in 0:|1:--dry-run) ;; @@ -84,11 +98,17 @@ install_main() { if [ -f "$install_checker" ]; then install_step 2/4 node "$install_checker" printf '\n%s\n' 'Node/SQLite compatibility check passed.' + install_abi=passed else printf '\nNode/SQLite compatibility check SKIPPED: installed checker not found at %s\n' "$install_checker" >&2 + install_abi='SKIPPED (not checked; see above)' fi install_step 3/4 "$install_rig" setup --dry-run - install_step 4/4 "$install_rig" setup + printf '\n[%s] %s\n' 4/4 "$install_rig setup" + # Keep native output and the actual status, as install_step does. + if "$install_rig" setup; then :; else + install_setup_incomplete "$?" + fi printf '\n%s\n' 'Follow the next steps printed by rig setup: start the daemon, open the OpenRig view, then tell the operator what you want to build.' } diff --git a/scripts/install.test.mjs b/scripts/install.test.mjs index c3a08632d..4830de2b7 100644 --- a/scripts/install.test.mjs +++ b/scripts/install.test.mjs @@ -91,7 +91,6 @@ for (const [variable, code, step, command] of [ ["ROOT_EXIT", 45, 2, "npm root -g"], ["ABI_EXIT", 44, 2, "check-abi.mjs"], ["PREVIEW_EXIT", 39, 3, "setup --dry-run"], - ["SETUP_EXIT", 40, 4, "setup"], ]) test(`${command} failure preserves status and native output`, t => { const f = fixture(t, { [variable]: String(code) }); const r = f.run(); @@ -170,6 +169,45 @@ test("the actual HTTP download piped to sh preserves setup failure", async t => assert.equal(status, 43, stderr); assert.match(stdout, /setup native output/); assert.match(stderr, /setup native error/); - assert.match(stderr, /FAILED \[4\/4\].*exit 43/); + 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); +});