Files
openrig/packages/cli/test/cross-host-commands.test.ts
T
mvschwarz ac75777a3c Release 0.4.4
Multi-host + Living Notes.

Multi-host foundation: shareable whole-topology staged spin-up brings
a rig topology up across multiple hosts in stages; VPS product-factory
multi-host hardening ships with rig host verbs (add / list / doctor,
capped at three; one built-in product-factory-vps posture, three-
valued per item); the transport posture is documented (ssh for pane
ops, http-bearer for daemon REST, no cross-transport fallback); the
For-You feed aggregates activity across every registered host; and
rig file moves files across the topology's registered hosts.

Living Notes: durable INTENT / PLAN / DELIVERED signal layer at
mission and slice altitude with agent authorship + timestamps; cheap
composer surfaces make it the first-class place agents record
decisions, plans, and delivered work; the one-structure review
contract reads left-to-right as a single vertical stack (INTENT above,
PLAN + mockup in the middle, DELIVERED with paired proof at the
bottom); a plan change deletes the old plan and writes a new one -
never demotes or stacks multiple competing plans.

Operationalize the SDLC control plane: conventions SSOT ships in
source at docs/reference/sdlc-conventions.md (Living Notes UI section
names, proof-contract format + plannedRef mockup pairing, staged-
approval locks, C1 proof header + closed sets, three role contracts,
curation rule, elastic-middle doctrine, advisory fail-open audit
posture); rig scope slice create scaffolds the convention sections +
proof + PROOF.md + an IMPLEMENTATION-PRD.md skeleton for every
template kind; rig scope audit + rig workspace doctor gain sdlc-
convention advisories (records-and-advises, gates on HIGH only); the
mission-slice-sop skill ships in canonical source with byte-parity
guard; rig proof / rig scope slice create / rig scope slice approve
help + cli-reference teach the SDLC flow.

BREAKING: rig ps default flips to consolidated all-active-rigs
compact projection (the v0.4.0 current-rig-only default is retired -
it hid running rigs from the operator's field of view). --json is
scope-not-shape (still a bare array with existing per-entry keys;
scope widens to all non-archived rigs including stopped ones; one-line
migration for the old fleet firehose: rig ps --nodes -A --full).
-A / --all-rigs keeps exactly ONE meaning (the --nodes fleet widener;
bare -A is a structured teaching error). --nodes names its scope
everywhere (session default local-only; --host <id> --nodes requires
--rig or -A; multi-host fan-out is rollup-only by default; the
explicit ladder --all-hosts --nodes -A --full fans out per-node with
hostId-stamped projected rows). --all-hosts / --hosts --json emits the
shared AggregatedPayload (items + hosts with closed-enum statuses:
ok | unreachable | unsupported-transport | auth-failed).

Operator UX: agent altitude coordination panel scopes multi-agent
state to the right altitude (workspace / mission / slice) so the
coordination surface reads without per-seat drowning; composes with
the workspace observability tabs from 0.4.1. Docs closeout catches
docs/as-built/architecture.md, docs/as-built/cli-reference.md, and
the codemaps up to shipped state.

Migrations: additive only. Existing v0.4.3 databases upgrade by
running rig daemon start.

Behavior change (rig ps default view): the compact all-active-rigs
projection replaces the current-rig-only default. If your call sites
relied on the v0.4.3 default shape, add --full for the rich per-node
projection or --rig <name> for the prior scope.

Public-safety scrub applied inline to packages/ui/twin/corrective/
fixtures-corrective.ts (one demo-fixture role identifier neutralized
to the demo-namespace baseline established at 0.4.1).

See CHANGELOG.md and docs/releases/v0.4.4.md.
2026-07-06 01:20:09 -07:00

590 lines
27 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { sendCommand, type SendDeps } from "../src/commands/send.js";
import { captureCommand, type CaptureDeps } from "../src/commands/capture.js";
import { psCommand, type PsDeps } from "../src/commands/ps.js";
import { whoamiCommand, type WhoamiDeps } from "../src/commands/whoami.js";
import type { CrossHostResult } from "../src/cross-host-executor.js";
import type { HostRegistryLoadResult } from "../src/host-registry.js";
// Capture stdout / stderr / exitCode for command actions.
interface CapturedOutput {
stdoutLines: string[];
stderrLines: string[];
stdoutWrites: string[];
stderrWrites: string[];
exitCode: number | undefined;
}
let captured: CapturedOutput;
let originalLog: typeof console.log;
let originalError: typeof console.error;
let originalStdoutWrite: typeof process.stdout.write;
let originalStderrWrite: typeof process.stderr.write;
beforeEach(() => {
captured = { stdoutLines: [], stderrLines: [], stdoutWrites: [], stderrWrites: [], exitCode: undefined };
originalLog = console.log;
originalError = console.error;
originalStdoutWrite = process.stdout.write.bind(process.stdout);
originalStderrWrite = process.stderr.write.bind(process.stderr);
console.log = (...args: unknown[]) => { captured.stdoutLines.push(args.map(String).join(" ")); };
console.error = (...args: unknown[]) => { captured.stderrLines.push(args.map(String).join(" ")); };
process.stdout.write = ((s: string | Uint8Array) => { captured.stdoutWrites.push(typeof s === "string" ? s : Buffer.from(s).toString("utf-8")); return true; }) as typeof process.stdout.write;
process.stderr.write = ((s: string | Uint8Array) => { captured.stderrWrites.push(typeof s === "string" ? s : Buffer.from(s).toString("utf-8")); return true; }) as typeof process.stderr.write;
process.exitCode = undefined;
});
afterEach(() => {
console.log = originalLog;
console.error = originalError;
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
captured.exitCode = process.exitCode;
process.exitCode = undefined;
});
const KNOWN_REGISTRY: HostRegistryLoadResult = {
ok: true,
registry: {
hosts: [{ id: "vm-a", transport: "ssh", target: "vm-a.local" }],
},
};
function deps(overrides: { run?: (h: never, argv: readonly string[]) => Promise<CrossHostResult>; registry?: HostRegistryLoadResult } = {}): SendDeps & CaptureDeps {
const lifecycleDeps = {} as never;
const clientFactory = (() => ({}) as never) as never;
return {
lifecycleDeps,
clientFactory,
hostRegistryLoader: () => overrides.registry ?? KNOWN_REGISTRY,
crossHostRun: overrides.run ?? (async () => ({ ok: true, failedStep: "none", stdout: "", stderr: "", remoteExitCode: 0 })),
};
}
// ---------------------------------------------------------------------------
// `rig send --host <id>`
// ---------------------------------------------------------------------------
describe("send --host (cross-host short-circuit)", () => {
it("happy path: forwards reconstructed argv to the executor and surfaces remote stdout verbatim", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = sendCommand(deps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: "Sent to dev-impl@my-rig\nVerified: yes\n", stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync(["--host", "vm-a", "dev-impl@my-rig", "hello world", "--verify"], { from: "user" });
expect(captureCalls.argv).toEqual(["rig", "send", "dev-impl@my-rig", "hello world", "--verify"]);
expect(captured.stdoutLines[0]).toBe("[via host=vm-a (vm-a.local)]");
const stdoutText = captured.stdoutWrites.join("");
expect(stdoutText).toContain("Verified: yes");
expect(process.exitCode).toBeUndefined();
});
it("--verify honesty: SSH success + remote 'Verified: no' surfaces in output, NOT collapsed into success+silence", async () => {
const cmd = sendCommand(deps({
run: async () => ({ ok: true, failedStep: "none", stdout: "Sent to dev-impl@my-rig\nVerified: no\n", stderr: "", remoteExitCode: 0 }),
}));
await cmd.parseAsync(["--host", "vm-a", "dev-impl@my-rig", "hi", "--verify"], { from: "user" });
const stdoutText = captured.stdoutWrites.join("");
expect(stdoutText).toContain("Verified: no"); // remote authority surfaced
// ssh layer succeeded; CLI does NOT inject Verified: yes.
expect(stdoutText).not.toMatch(/^Verified: yes$/m);
});
it("forwards --force, --wait-for-idle, --json flags into the remote argv", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = sendCommand(deps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: "{}", stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg", "--force", "--json"], { from: "user" });
expect(captureCalls.argv).toContain("--force");
expect(captureCalls.argv).toContain("--json");
});
it("JSON output wraps the executor result in a cross_host envelope", async () => {
const cmd = sendCommand(deps({
run: async () => ({ ok: true, failedStep: "none", stdout: "{}", stderr: "", remoteExitCode: 0 }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg", "--json"], { from: "user" });
const out = JSON.parse(captured.stdoutLines[0]!);
expect(out.cross_host).toEqual({ host: "vm-a", target: "vm-a.local" });
expect(out.result.ok).toBe(true);
});
it("unknown host id surfaces a friendly error with discoverability hint", async () => {
const cmd = sendCommand(deps());
await cmd.parseAsync(["--host", "vm-unknown", "s@r", "msg"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("unknown host id 'vm-unknown'");
expect(captured.stderrLines.join("\n")).toContain("vm-a"); // known-id hint
expect(process.exitCode).toBe(1);
});
it("registry load failure surfaces the error", async () => {
const cmd = sendCommand(deps({
registry: { ok: false, error: "host registry not found at /nope" },
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("host registry not found");
expect(process.exitCode).toBe(1);
});
it("ssh-unreachable failure → exit 1 with operator-actionable message", async () => {
const cmd = sendCommand(deps({
run: async () => ({ ok: false, failedStep: "ssh-unreachable", sshStderr: "ssh: connect to host: Connection refused" }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("ssh to host=vm-a");
expect(captured.stderrLines.join("\n")).toContain("Verify SSH access");
expect(process.exitCode).toBe(1);
});
it("permission-gate failure → exit 1 with field-note hint", async () => {
const cmd = sendCommand(deps({
run: async () => ({ ok: false, failedStep: "permission-gate", sshStderr: "Permission denied", hint: "See keychain doc" }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("permission/auth gate");
expect(captured.stderrLines.join("\n")).toContain("See keychain doc");
expect(process.exitCode).toBe(1);
});
it("remote-daemon-unreachable → exit 1 with daemon-start hint", async () => {
const cmd = sendCommand(deps({
run: async () => ({ ok: false, failedStep: "remote-daemon-unreachable", stdout: "", stderr: "Daemon not running", remoteExitCode: 1 }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("could not reach the remote daemon");
expect(captured.stderrLines.join("\n")).toContain("rig daemon start");
expect(process.exitCode).toBe(1);
});
it("remote-command-failed → exit 1 with remote stderr surfaced", async () => {
const cmd = sendCommand(deps({
run: async () => ({ ok: false, failedStep: "remote-command-failed", stdout: "", stderr: "session not found", remoteExitCode: 3 }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("remote rig command on host=vm-a failed");
expect(captured.stderrLines.join("\n")).toContain("session not found");
expect(process.exitCode).toBe(1);
});
it("JSON output for failure surfaces failedStep + permission-gate hint under result", async () => {
const cmd = sendCommand(deps({
run: async () => ({ ok: false, failedStep: "permission-gate", sshStderr: "Permission denied", hint: "See keychain doc" }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "msg", "--json"], { from: "user" });
const out = JSON.parse(captured.stdoutLines[0]!);
expect(out.cross_host).toEqual({ host: "vm-a", target: "vm-a.local" });
expect(out.result.ok).toBe(false);
expect(out.result.failedStep).toBe("permission-gate");
expect(out.result.hint).toBe("See keychain doc");
expect(process.exitCode).toBe(1);
});
});
// ---------------------------------------------------------------------------
// `rig capture --host <id>`
// ---------------------------------------------------------------------------
describe("capture --host (cross-host short-circuit)", () => {
it("happy path: forwards reconstructed argv with session + lines", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = captureCommand(deps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: "pane content here\n", stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync(["--host", "vm-a", "dev-impl@my-rig", "--lines", "50"], { from: "user" });
expect(captureCalls.argv).toEqual(["rig", "capture", "dev-impl@my-rig", "--lines", "50"]);
expect(captured.stdoutLines[0]).toBe("[via host=vm-a (vm-a.local)]");
expect(captured.stdoutWrites.join("")).toContain("pane content here");
});
it("multi-target form: forwards --rig and --pod when no session is supplied", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = captureCommand(deps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: "{}", stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync(["--host", "vm-a", "--rig", "my-rig", "--pod", "dev", "--json"], { from: "user" });
expect(captureCalls.argv).toEqual(["rig", "capture", "--rig", "my-rig", "--pod", "dev", "--lines", "20", "--json"]);
});
it("unknown host id surfaces a friendly error", async () => {
const cmd = captureCommand(deps());
await cmd.parseAsync(["--host", "vm-bogus", "s@r"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("unknown host id 'vm-bogus'");
expect(process.exitCode).toBe(1);
});
it("ssh-unreachable failure surfaces actionable error", async () => {
const cmd = captureCommand(deps({
run: async () => ({ ok: false, failedStep: "ssh-unreachable", sshStderr: "Connection refused" }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("ssh to host=vm-a");
expect(process.exitCode).toBe(1);
});
it("JSON output wraps result in cross_host envelope", async () => {
const cmd = captureCommand(deps({
run: async () => ({ ok: true, failedStep: "none", stdout: "{}", stderr: "", remoteExitCode: 0 }),
}));
await cmd.parseAsync(["--host", "vm-a", "s@r", "--json"], { from: "user" });
const out = JSON.parse(captured.stdoutLines[0]!);
expect(out.cross_host).toEqual({ host: "vm-a", target: "vm-a.local" });
expect(out.result.ok).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Compat regression: `rig send` and `rig capture` WITHOUT --host MUST NOT add
// the cross-host annotation and MUST NOT call the executor at all.
// ---------------------------------------------------------------------------
describe("compat regression: no --host means no cross-host annotation", () => {
it("send help text mentions --host (so operators discover it)", () => {
const cmd = sendCommand(deps());
expect(cmd.helpInformation()).toContain("--host");
});
it("capture help text mentions --host", () => {
const cmd = captureCommand(deps());
expect(cmd.helpInformation()).toContain("--host");
});
it("send without --host does NOT call the cross-host executor (registry loader untouched, executor untouched)", async () => {
const loaderSpy = vi.fn();
const runSpy = vi.fn();
const cmd = sendCommand({
lifecycleDeps: makeFailingLifecycleDeps(),
clientFactory: () => ({} as never),
hostRegistryLoader: loaderSpy as never,
crossHostRun: runSpy as never,
});
await cmd.parseAsync(["s@r", "msg"], { from: "user" });
// local path short-circuits at "Daemon not running" without consulting cross-host machinery.
expect(loaderSpy).not.toHaveBeenCalled();
expect(runSpy).not.toHaveBeenCalled();
});
it("capture without --host does NOT call the cross-host executor", async () => {
const loaderSpy = vi.fn();
const runSpy = vi.fn();
const cmd = captureCommand({
lifecycleDeps: makeFailingLifecycleDeps(),
clientFactory: () => ({} as never),
hostRegistryLoader: loaderSpy as never,
crossHostRun: runSpy as never,
});
await cmd.parseAsync(["s@r"], { from: "user" });
expect(loaderSpy).not.toHaveBeenCalled();
expect(runSpy).not.toHaveBeenCalled();
});
});
function makeFailingLifecycleDeps(): never {
// Stub LifecycleDeps that responds with "no daemon" on every probe — the
// local-path branch short-circuits with "Daemon not running" before doing
// anything cross-host.
return {
exists: (_p: string) => false,
readFileText: (_p: string) => "",
spawnDetached: () => ({ pid: 0 }),
isPidAlive: (_p: number) => false,
fetchHealthDetail: async () => ({ ok: false, code: "unreachable" }),
} as never;
}
// ---------------------------------------------------------------------------
// `rig ps --host <id>` (C9b)
// ---------------------------------------------------------------------------
function psDeps(overrides: { run?: (h: never, argv: readonly string[]) => Promise<CrossHostResult>; registry?: HostRegistryLoadResult } = {}): PsDeps {
return {
lifecycleDeps: {} as never,
clientFactory: (() => ({}) as never) as never,
hostRegistryLoader: () => overrides.registry ?? KNOWN_REGISTRY,
crossHostRun: overrides.run ?? (async () => ({ ok: true, failedStep: "none", stdout: "", stderr: "", remoteExitCode: 0 })),
};
}
describe("ps --host (cross-host short-circuit)", () => {
it("argv reconstruction: forwards every shaping flag in operator-declared order", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = psCommand(psDeps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: '[{"rigName":"r1"}]\n', stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync([
"--host", "vm-a", "--nodes", "-A", "--full", "--limit", "20",
"--fields", "rigName,sessionStatus", "--summary", "--filter", "status=running", "--json",
], { from: "user" });
expect(captureCalls.argv).toEqual([
"rig", "ps", "--nodes", "--full", "--all-rigs", "--limit", "20", "--fields", "rigName,sessionStatus",
"--summary", "--filter", "status=running", "--json",
]);
});
it("argv reconstruction: bare `--host vm-a` produces ['rig','ps'] with no extra flags", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = psCommand(psDeps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: "", stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync(["--host", "vm-a"], { from: "user" });
expect(captureCalls.argv).toEqual(["rig", "ps"]);
});
it("verbatim remote stdout passthrough on success — no double-JSON-wrapping", async () => {
const remoteJson = '[{"rigName":"r1","status":"running"},{"rigName":"r2","status":"stopped"}]\n';
const cmd = psCommand(psDeps({
run: async () => ({ ok: true, failedStep: "none", stdout: remoteJson, stderr: "", remoteExitCode: 0 }),
}));
await cmd.parseAsync(["--host", "vm-a", "--json"], { from: "user" });
const stdoutText = captured.stdoutWrites.join("");
// Output is verbatim remote stdout; NOT wrapped in {cross_host:..., result:...}.
expect(stdoutText).toBe(remoteJson);
expect(stdoutText).not.toContain("cross_host");
});
it("ssh-unreachable failure surfaces actionable error and exits 1", async () => {
const cmd = psCommand(psDeps({
run: async () => ({ ok: false, failedStep: "ssh-unreachable", sshStderr: "ssh: connect refused" }),
}));
await cmd.parseAsync(["--host", "vm-a"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("ssh to host=vm-a");
expect(process.exitCode).toBe(1);
});
it("permission-gate failure surfaces hint and exits 1", async () => {
const cmd = psCommand(psDeps({
run: async () => ({ ok: false, failedStep: "permission-gate", sshStderr: "Permission denied", hint: "See keychain doc" }),
}));
await cmd.parseAsync(["--host", "vm-a"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("permission/auth gate");
expect(captured.stderrLines.join("\n")).toContain("See keychain doc");
expect(process.exitCode).toBe(1);
});
it("remote-daemon-unreachable surfaces daemon-start hint", async () => {
const cmd = psCommand(psDeps({
run: async () => ({ ok: false, failedStep: "remote-daemon-unreachable", stdout: "", stderr: "Daemon not running", remoteExitCode: 1 }),
}));
await cmd.parseAsync(["--host", "vm-a"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("could not reach the remote daemon");
expect(process.exitCode).toBe(1);
});
it("remote-command-failed surfaces remote stderr", async () => {
const cmd = psCommand(psDeps({
run: async () => ({ ok: false, failedStep: "remote-command-failed", stdout: "", stderr: "Some rig error", remoteExitCode: 3 }),
}));
await cmd.parseAsync(["--host", "vm-a"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("remote rig command on host=vm-a failed");
expect(process.exitCode).toBe(1);
});
it("unknown host id surfaces friendly error with discoverability hint", async () => {
const cmd = psCommand(psDeps());
await cmd.parseAsync(["--host", "vm-bogus"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("unknown host id 'vm-bogus'");
expect(process.exitCode).toBe(1);
});
it("JSON output for failure surfaces top-level failedStep envelope", async () => {
const cmd = psCommand(psDeps({
run: async () => ({ ok: false, failedStep: "permission-gate", sshStderr: "Permission denied", hint: "See keychain doc" }),
}));
await cmd.parseAsync(["--host", "vm-a", "--json"], { from: "user" });
const out = JSON.parse(captured.stdoutLines[0]!);
expect(out.ok).toBe(false);
expect(out.cross_host).toEqual({ host: "vm-a", target: "vm-a.local" });
expect(out.failedStep).toBe("permission-gate");
expect(out.hint).toBe("See keychain doc");
expect(process.exitCode).toBe(1);
});
it("help text mentions --host (operator discoverability)", () => {
const cmd = psCommand(psDeps());
expect(cmd.helpInformation()).toContain("--host");
});
it("compat regression: ps without --host does NOT call registry loader OR executor", async () => {
const loaderSpy = vi.fn();
const runSpy = vi.fn();
const cmd = psCommand({
lifecycleDeps: makeFailingLifecycleDeps(),
clientFactory: () => ({} as never),
hostRegistryLoader: loaderSpy as never,
crossHostRun: runSpy as never,
});
await cmd.parseAsync([], { from: "user" });
expect(loaderSpy).not.toHaveBeenCalled();
expect(runSpy).not.toHaveBeenCalled();
});
});
// ---------------------------------------------------------------------------
// `rig whoami --host <id>` (C9b')
// ---------------------------------------------------------------------------
function whoamiDeps(overrides: { run?: (h: never, argv: readonly string[]) => Promise<CrossHostResult>; registry?: HostRegistryLoadResult } = {}): WhoamiDeps {
return {
lifecycleDeps: {} as never,
clientFactory: (() => ({}) as never) as never,
hostRegistryLoader: () => overrides.registry ?? KNOWN_REGISTRY,
crossHostRun: overrides.run ?? (async () => ({ ok: true, failedStep: "none", stdout: "", stderr: "", remoteExitCode: 0 })),
};
}
describe("whoami --host (cross-host short-circuit)", () => {
it("argv reconstruction: forwards --node-id, --session, --json in operator-declared order", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = whoamiCommand(whoamiDeps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: "{}", stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync([
"--host", "vm-a", "--node-id", "01ABC", "--session", "foo@rig", "--json",
], { from: "user" });
expect(captureCalls.argv).toEqual([
"rig", "whoami", "--node-id", "01ABC", "--session", "foo@rig", "--json",
]);
});
it("argv reconstruction: bare `--host vm-a` produces ['rig','whoami'] with no extra flags", async () => {
const captureCalls: { argv?: readonly string[] } = {};
const cmd = whoamiCommand(whoamiDeps({
run: async (_h, argv) => {
captureCalls.argv = argv;
return { ok: true, failedStep: "none", stdout: "", stderr: "", remoteExitCode: 0 };
},
}));
await cmd.parseAsync(["--host", "vm-a"], { from: "user" });
expect(captureCalls.argv).toEqual(["rig", "whoami"]);
});
it("cross-host runs BEFORE local identity-source resolution (no local --node-id/--session required)", async () => {
// Critical for whoami: the local resolveIdentitySource() walks env vars and
// tmux pane metadata. Cross-host must NOT require any of that — the remote
// rig has its own identity context.
const cmd = whoamiCommand(whoamiDeps({
run: async () => ({ ok: true, failedStep: "none", stdout: '{"rigName":"remote"}\n', stderr: "", remoteExitCode: 0 }),
}));
await cmd.parseAsync(["--host", "vm-a", "--json"], { from: "user" });
// Should NOT have errored on "Cannot determine identity"; should have
// successfully written remote stdout.
expect(captured.stderrLines.join("\n")).not.toContain("Cannot determine identity");
const stdoutText = captured.stdoutWrites.join("");
expect(stdoutText).toContain("remote");
});
it("verbatim remote stdout passthrough on success — no double-JSON-wrapping", async () => {
const remoteJson = '{"resolvedBy":"node_id","identity":{"rigName":"x"}}\n';
const cmd = whoamiCommand(whoamiDeps({
run: async () => ({ ok: true, failedStep: "none", stdout: remoteJson, stderr: "", remoteExitCode: 0 }),
}));
await cmd.parseAsync(["--host", "vm-a", "--json"], { from: "user" });
const stdoutText = captured.stdoutWrites.join("");
expect(stdoutText).toBe(remoteJson);
expect(stdoutText).not.toContain("cross_host");
});
it("ssh-unreachable failure exits 1 with actionable message", async () => {
const cmd = whoamiCommand(whoamiDeps({
run: async () => ({ ok: false, failedStep: "ssh-unreachable", sshStderr: "Connection refused" }),
}));
await cmd.parseAsync(["--host", "vm-a"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("ssh to host=vm-a");
expect(process.exitCode).toBe(1);
});
it("unknown host id surfaces friendly error", async () => {
const cmd = whoamiCommand(whoamiDeps());
await cmd.parseAsync(["--host", "vm-bogus"], { from: "user" });
expect(captured.stderrLines.join("\n")).toContain("unknown host id 'vm-bogus'");
expect(process.exitCode).toBe(1);
});
it("JSON output for failure surfaces top-level failedStep envelope", async () => {
const cmd = whoamiCommand(whoamiDeps({
run: async () => ({ ok: false, failedStep: "remote-daemon-unreachable", stdout: "", stderr: "Daemon not running", remoteExitCode: 1 }),
}));
await cmd.parseAsync(["--host", "vm-a", "--json"], { from: "user" });
const out = JSON.parse(captured.stdoutLines[0]!);
expect(out.ok).toBe(false);
expect(out.cross_host).toEqual({ host: "vm-a", target: "vm-a.local" });
expect(out.failedStep).toBe("remote-daemon-unreachable");
expect(process.exitCode).toBe(1);
});
it("help text mentions --host", () => {
const cmd = whoamiCommand(whoamiDeps());
expect(cmd.helpInformation()).toContain("--host");
});
it("compat regression: whoami without --host does NOT call registry loader OR executor", async () => {
const loaderSpy = vi.fn();
const runSpy = vi.fn();
const cmd = whoamiCommand({
lifecycleDeps: makeFailingLifecycleDeps(),
clientFactory: () => ({} as never),
hostRegistryLoader: loaderSpy as never,
crossHostRun: runSpy as never,
});
// Passing --node-id avoids the "cannot determine identity" branch; the
// local daemon-down path runs after that (or partial-result; either way
// does NOT consult the cross-host machinery).
await cmd.parseAsync(["--node-id", "abc"], { from: "user" });
expect(loaderSpy).not.toHaveBeenCalled();
expect(runSpy).not.toHaveBeenCalled();
});
});
// ---------------------------------------------------------------------------
// `rig ps --host <id>` — OPR.0.4.0.34 breadth-flag forwarding over SSH
// ---------------------------------------------------------------------------
describe("ps --host (cross-host argv forwarding, OPR.0.4.0.34)", () => {
function psWithCapture(cap: { argv?: readonly string[] }) {
return psCommand(deps({
run: async (_h, argv) => {
cap.argv = argv;
return { ok: true, failedStep: "none", stdout: "[]", stderr: "", remoteExitCode: 0 };
},
}) as unknown as PsDeps);
}
it("forwards -A/--all-rigs into the remote SSH argv (breadth preserved across the hop)", async () => {
const cap: { argv?: readonly string[] } = {};
await psWithCapture(cap).parseAsync(["--host", "vm-a", "-A", "--nodes", "--json"], { from: "user" });
expect(cap.argv).toContain("--all-rigs");
expect(cap.argv).toContain("--nodes");
});
it("forwards --active into the remote SSH argv (also covers --running via normalization)", async () => {
const capRunning: { argv?: readonly string[] } = {};
await psWithCapture(capRunning).parseAsync(["--host", "vm-a", "--running", "--json"], { from: "user" });
expect(capRunning.argv).toContain("--active");
const capActive: { argv?: readonly string[] } = {};
await psWithCapture(capActive).parseAsync(["--host", "vm-a", "--active", "--json"], { from: "user" });
expect(capActive.argv).toContain("--active");
});
});