Files
openrig/packages/cli/test/command-status-evidence.test.ts
Mike Schwarz bf579d5f89 fix(cli): send the real request to a busy daemon; refuse only one confirmed down (#1650)
Commands no longer refuse a daemon that is busy but alive. The CLI's status check before each command refused whenever it couldn't confirm the daemon was healthy, so right after a slow request an ordinary command such as `rig ps --nodes -A`, `rig capture` or `rig context profile` failed in about 128 ms with "Daemon did not respond — it may be busy or stopped". Now a daemon that isn't confirmed down gets the real request, under its normal timeout. A daemon confirmed down (stopped, stale, or a refused probe) keeps the clear "not running" message and start hint, and a probe the OS blocks and a live sibling home keep their own guidance.

When a write then times out, it says the outcome is unknown and to check before any retry, including in the commands that catch their own errors (context, agent-image, specs, scope and fork). Each write is sent once and never retried. `rig start`'s UI health read is bounded at 5 seconds.

`rig up` and `rig start` start a daemon when one is stopped or stale, or for a live sibling home or a blocked probe, as before. They no longer start one for a busy daemon of this home, which used to get a false "already running", or past an explicit HOME-MOVED marker. GitHub bundle imports still require a verified local daemon, and `rig status --json` is unchanged.
2026-10-20 22:06:43 -07:00

61 lines
3.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { bootstrapCommand } from "../src/commands/bootstrap.js";
import { discoverCommand } from "../src/commands/discover.js";
import { workflowCommand } from "../src/commands/workflow.js";
import { workspaceCommand } from "../src/commands/workspace.js";
import { getDaemonStatus, type DaemonStatus, type LifecycleDeps } from "../src/daemon-lifecycle.js";
vi.mock("../src/daemon-lifecycle.js", async () => ({
...await vi.importActual<Record<string, unknown>>("../src/daemon-lifecycle.js"),
getDaemonStatus: vi.fn(),
}));
beforeEach(() => { process.exitCode = undefined; });
afterEach(() => { vi.restoreAllMocks(); process.exitCode = undefined; });
const entries = [
["bootstrap", bootstrapCommand, ["./fixture.yaml", "--plan"]],
["discover", discoverCommand, []],
["workflow", workflowCommand, ["list"]],
["workspace", workspaceCommand, ["doctor"]],
] as const;
describe("command daemon status evidence", () => {
for (const [name, makeCommand, args] of entries) {
// Changed: an inconclusive status used to refuse ("state not confirmed"). A daemon that may be busy
// gets the command's real request instead; the request's own answer or timeout decides.
it.each<DaemonStatus>([
{ state: "unverified", host: "127.0.0.1", port: 7433 },
{ state: "running", healthy: false, host: "127.0.0.1", port: 7433 },
])(`${name} makes its real request when the daemon may be busy, never asserting down`, async (status) => {
vi.mocked(getDaemonStatus).mockResolvedValue(status);
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const clientFactory = vi.fn(() => { throw new Error("stop at the request"); });
await makeCommand({ lifecycleDeps: {} as LifecycleDeps, clientFactory }).parseAsync(["node", "rig", ...args]).catch(() => {});
expect(clientFactory).toHaveBeenCalledWith("http://127.0.0.1:7433");
expect(error.mock.calls.flat().join("\n")).not.toContain("state not confirmed");
expect(error.mock.calls.flat().join("\n")).not.toContain("Daemon not running.");
});
it(`${name} preserves positively stopped status`, async () => {
vi.mocked(getDaemonStatus).mockResolvedValue({ state: "stopped" });
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const clientFactory = vi.fn();
await makeCommand({ lifecycleDeps: {} as LifecycleDeps, clientFactory }).parseAsync(["node", "rig", ...args]);
expect(error.mock.calls.flat().join("\n")).toContain("Daemon not running.");
expect(process.exitCode).toBe(1);
expect(clientFactory).not.toHaveBeenCalled();
});
it(`${name} retains the selected home and live-sibling diagnostic`, async () => {
vi.mocked(getDaemonStatus).mockResolvedValue({ state: "unverified", siblingHint: { resolvedHome: "selected-home", siblingHome: "live-sibling" } });
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const clientFactory = vi.fn();
await makeCommand({ lifecycleDeps: {} as LifecycleDeps, clientFactory }).parseAsync(["node", "rig", ...args]);
expect(error.mock.calls.flat().join("\n")).toContain("resolved selected-home, live sibling live-sibling");
expect(process.exitCode).toBe(1);
expect(clientFactory).not.toHaveBeenCalled();
});
}
});