Merge OPR.0.4.0.34 ps current-rig default

This commit is contained in:
mvschwarz committed 2026-06-20 19:01:30 -07:00
commit c8399f6dae
7 files changed
+456 -50

No files matched your search

+100 -45
View File
@@ -6,6 +6,7 @@ import type { StatusDeps } from "./status.js";
import { loadHostRegistry, resolveHost, hostDisplayTarget, resolveRemoteBearer, classifyHttpFailedStep, classifyHttpError, type HttpHostEntry } from "../host-registry.js";
import { runCrossHostCommand, type RunCrossHostCommandOpts } from "../cross-host-executor.js";
import { emitCrossHostError, emitCrossHostFailure } from "../cross-host-cli-helpers.js";
import { readOpenRigEnv } from "../openrig-compat.js";
interface PsEntry {
rigId: string;
@@ -77,12 +78,25 @@ interface NodeEntry {
state?: "critical" | "warning" | "low" | "unknown";
sampledAt: string | null;
};
/** OPR.0.4.0.34 — resume summary from the daemon node-inventory. resumeToken
* is the SECRET: surfaced as a present-boolean in compact, value only in --full. */
resumeType?: string | null;
resumeToken?: string | null;
/** OPR.0.4.0.34 — startup-completion timestamp; the compact `lastActivity`
* fallback when no agentActivity sample exists. */
startupCompletedAt?: string | null;
[key: string]: unknown;
}
// L3-followup: human-output budgets bound default terminal output for hosts
// with realistic agent counts. `--full` opts out. JSON output remains
// unbounded by default for back-compat (Decision C hybrid).
function extractRigName(sessionName: string): string | undefined {
const atIdx = sessionName.lastIndexOf("@");
if (atIdx < 0 || atIdx === sessionName.length - 1) return undefined;
return sessionName.slice(atIdx + 1);
}
const HUMAN_RIG_BUDGET = 50;
const HUMAN_NODE_BUDGET = 100;
@@ -178,6 +192,8 @@ interface PsCliOptions {
allHosts?: boolean;
hosts?: string;
active?: boolean;
running?: boolean;
allRigs?: boolean;
rig?: string;
session?: string;
/** OPR.0.3.3.19 - include archived rigs (default excludes them). Parity
@@ -390,19 +406,40 @@ function needsAttention(node: NodeEntry): boolean {
|| node.agentActivity?.state === "needs_input";
}
// OPR.0.4.0.34 — the compact orch field set (PRD FR-4). Carries the
// source-available identity + state + resume-summary fields an orchestrator
// needs at a glance, WITHOUT leaking the resume token value or resumeCommand
// (security). recoveryGuidance/currentUsage stay on the node detail (slice 26).
function compactNodeProjection(nodes: NodeEntry[]): Array<Record<string, unknown>> {
return nodes.map((n) => {
const attention = needsAttention(n);
const compact: Record<string, unknown> = {
// identity (rigId/rigName + logicalId + session) — disambiguates under -A.
rigId: n.rigId,
rigName: n.rigName,
logicalId: n.logicalId,
canonicalSessionName: n.canonicalSessionName,
// lifecycle + session/startup state.
sessionStatus: n.sessionStatus,
startupStatus: n.startupStatus,
lifecycleState: n.lifecycleState,
// activity (state always; short reason only when attention — keep it lean).
agentActivity: attention
? { state: n.agentActivity?.state ?? "unknown", reason: n.agentActivity?.reason }
: { state: n.agentActivity?.state ?? "unknown" },
// work counts.
hasAssignedWork: n.hasAssignedWork,
pendingWorkCount: n.pendingWorkCount,
// resume summary — type + PRESENT boolean only (never the token value).
resumeType: n.resumeType ?? null,
resumeTokenPresent: Boolean(n.resumeToken),
// updated/age proxy. The node-list emits no dedicated `updatedAt`; the
// freshest signal is agentActivity.sampledAt, then startupCompletedAt,
// else null (documented FR-4 fallback — not a fabricated timestamp).
lastActivity: n.agentActivity?.sampledAt ?? n.startupCompletedAt ?? null,
};
// held/attention reason (short) when present.
if (n.heldReason) compact.heldReason = n.heldReason;
if (attention && n.latestError) {
compact.latestError = n.latestError;
}
@@ -479,56 +516,57 @@ export function psCommand(depsOverride?: PsDeps): Command {
const cmd = new Command("ps")
.description("List rigs and their status")
.addHelpText("after", `
Default frontier source: use 'rig queue list' for pending work items.
'rig ps' is an observation tool showing fleet state, not a task dispatch surface.
Default: current-rig scope (derived from OPENRIG_SESSION_NAME's @<rig> suffix).
Shows ALL states (stopped, recoverable, attention, idle — non-running IS the
actionable signal for topology/readiness). Use -A/--all-rigs for fleet breadth.
Compact defaults: 'rig ps --nodes' shows a compact summary per node
(rig, session, lifecycle, activity state, reason when attention, queue counts).
Use '--full' (or '--verbose') for the complete per-node LIST payload
(contextUsage scalars, resume commands, agent references). Note (OPR.0.4.0.26):
the node-list payload intentionally carries recoveryGuidance: null and
contextUsage.currentUsage: null even with --full; fetch the full recovery
guidance and currentUsage from the single-node detail
(/api/rigs/:rigId/nodes/:logicalId) or 'rig whoami'.
(rig, session, lifecycle, activity state, reason when attention, queue counts,
resume type + present indicator). Resume token values and resumeCommand are
excluded from compact output (security); use --full to see them.
Compact field set (--nodes default): rigName, canonicalSessionName,
lifecycleState, agentActivity.state, agentActivity.reason (when attention),
hasAssignedWork, pendingWorkCount, latestError (when attention).
Use '--full' (or '--verbose') for the uncompacted per-node payload (contextUsage
scalars, resume commands/tokens, agent references). Note (OPR.0.4.0.26): the
node-list payload carries recoveryGuidance: null and contextUsage.currentUsage:
null even with --full; fetch the full recovery guidance + currentUsage from the
single-node detail (/api/rigs/:rigId/nodes/:logicalId) or 'rig whoami'.
Examples:
rig ps Show all rigs (human; truncated to ${HUMAN_RIG_BUDGET} with footer)
rig ps --full Show all rigs without truncation
rig ps --json JSON output (bare array; back-compat)
rig ps Current rig summary (compact)
rig ps -A All rigs (fleet breadth)
rig ps --full Current rig, full detail
rig ps --json Current rig, compact JSON
rig ps -A --json All rigs, compact JSON
rig ps --json --limit 20 Bounded JSON envelope
rig ps --json --summary Aggregate-only JSON (no per-rig entries)
rig ps --json --summary Aggregate-only JSON
rig ps --json --fields rigName,status,lifecycleState
Project JSON to named fields
rig ps --filter lifecycleState=attention_required
Show only rigs needing attention
rig ps --filter status=running Show only running rigs
rig ps --filter name-prefix=demo Filter by rig-name prefix
rig ps --nodes Per-node compact summary (human; truncated to ${HUMAN_NODE_BUDGET})
rig ps --nodes --json Per-node compact summary (JSON; bare array)
rig ps --nodes Current rig per-node compact summary
rig ps --nodes -A All rigs per-node compact summary
rig ps --nodes --json Current rig per-node compact JSON
rig ps --nodes --json --full Complete per-node LIST payload (guidance/currentUsage relocated to node detail)
rig ps --nodes --json --rig openrig-build Compact nodes filtered to one rig
rig ps --nodes --json --session dev1-impl@myrig Filter to a single session
rig ps --nodes --json --rig openrig-build Compact nodes for a specific rig
rig ps --nodes --json --session dev1-impl@myrig Filter to a single session (within current rig; use -A for cross-rig)
rig ps --nodes --json --limit 50 Bounded per-node JSON envelope
rig ps --include-archived Include archived rigs (marked with *); hidden by default
rig ps --nodes --active Show only nodes whose agentActivity.state == running (PL-019)
rig ps --nodes --active Only nodes with agentActivity.state=running
rig ps --nodes --running Same as --active
rig ps --nodes --filter agentActivity.state=running
Same as --active (the explicit form)
rig ps --host vm-claude-test --nodes --json Run on a remote host via single-hop ssh
rig ps --host vm-claude-test --nodes --json Remote host (no current-rig default)
rig ps --include-archived Include archived rigs (marked with *); hidden by default
JSON output entries include both \`name\` and \`rigName\` (alias) for forward
compatibility; agent code should prefer \`rigName\` (matches per-node JSON).
--rig <name> overrides the current-rig default. -A/--all-rigs disables it.
--session <name> filters within the effective rig scope; use -A if the target
session is in a different rig.
--rig <name> filters to nodes whose rigName matches (convenience; composes with
compact and --full). --session <name> filters to the node whose
canonicalSessionName matches. Both narrow the node set before projection.
--active/--running narrow to agentActivity.state=running. Cannot combine with --filter.
--filter accepts: status, lifecycleState, name-prefix, name, agentActivity.state.
Other keys are rejected. agentActivity.state is node-level (use with --nodes);
allowed values: running, needs_input, idle, unknown.
--filter accepts: status, lifecycleState, name-prefix, name, agentActivity.state,
contextUsage.percent, contextUsage.state. Other keys are rejected.
--fields accepts (rig-level): rigId, name, rigName, nodeCount, runningCount,
activeCount, hasWorkCount, status, lifecycleState, uptime, latestSnapshot.
@@ -536,17 +574,10 @@ activeCount, hasWorkCount, status, lifecycleState, uptime, latestSnapshot.
podNamespace, canonicalSessionName, nodeKind, runtime, sessionStatus,
startupStatus, restoreOutcome, lifecycleState, tmuxAttachCommand,
resumeCommand, latestError, terminalActive, hasAssignedWork,
pendingWorkCount, agentActivity. Other keys are rejected.
\`name\` is rig-level only; for node entries use \`rigName\`. Nested-field
projection (e.g. \`agentActivity.state\`) is not supported in this slice; pass
\`agentActivity\` to project the whole object and read the nested value
downstream.
pendingWorkCount, agentActivity, contextUsage, heldReason.
--host runs the same command on a remote host declared in ~/.openrig/hosts.yaml
via single-hop ssh (CLI-side shell-out; daemon untouched). The remote rig's
output is what counts and is surfaced verbatim on success; failure is
distinguished into ssh-unreachable / permission-gate / remote-daemon-unreachable
/ remote-command-failed.
--host runs on a remote host declared in ~/.openrig/hosts.yaml (no current-rig
default applied; the remote host's rigs are shown).
Exit codes:
0 Success
@@ -556,14 +587,16 @@ Exit codes:
cmd
.option("--json", "JSON output for agents")
.option("--nodes", "Show per-node detail for all rigs")
.option("--full", "Show all node-list fields per node (uncompacted rows; recoveryGuidance/currentUsage live on the node detail, not the list)")
.option("--nodes", "Show per-node detail (current rig; -A for all rigs)")
.option("--full", "Show all node-list fields per node (uncompacted rows; node-list recoveryGuidance/currentUsage live on the node detail, not the list)")
.option("--verbose", "Alias for --full")
.option("--limit <n>", "Limit number of entries (rigs or nodes)")
.option("--fields <list>", "Comma-separated field list to project (JSON only)")
.option("--summary", "Emit aggregate-only output (counts by status/lifecycle)")
.option("--filter <key=value>", "Filter entries; supported keys: status, lifecycleState, name-prefix, name, agentActivity.state")
.option("--active", "Shortcut for --filter agentActivity.state=running (PL-019)")
.option("--running", "Alias for --active")
.option("-A, --all-rigs", "Show all rigs (default is current rig only)")
.option("--rig <name>", "Show only nodes belonging to the named rig")
.option("--session <name>", "Show only the node matching this canonical session name")
.option("--include-archived", "Include archived rigs (default hides them); parity with 'rig stream list --include-archived'")
@@ -572,6 +605,13 @@ Exit codes:
.option("--hosts <ids>", "Fan out to specific hosts (comma-separated)")
.action(async (opts: PsCliOptions) => {
if (opts.verbose) opts.full = true;
if (opts.running) opts.active = true;
const isRemote = !!(opts.host || opts.allHosts || opts.hosts);
if (!opts.allRigs && !opts.rig && !isRemote) {
const sessionName = readOpenRigEnv("OPENRIG_SESSION_NAME", "RIGGED_SESSION_NAME");
const callerRig = sessionName ? extractRigName(sessionName) : undefined;
if (callerRig) opts.rig = callerRig;
}
const deps = getDepsF();
if (opts.allHosts || opts.hosts) {
@@ -664,8 +704,12 @@ Exit codes:
const all = res.data;
const rigScoped = opts.rig
? all.filter((e) => (e.rigName ?? e.name) === opts.rig)
: all;
// Apply CLI-side filter (Amendment A: prefer CLI shaping).
const filtered = parsedFilter ? applyRigFilter(all, parsedFilter) : all;
const filtered = parsedFilter ? applyRigFilter(rigScoped, parsedFilter) : rigScoped;
// Summary mode short-circuits per-entry output.
if (opts.summary) {
@@ -770,8 +814,12 @@ async function handleNodes(
return;
}
const effectiveRigs = opts.rig
? rigRes.data.filter((r) => (r.rigName ?? r.name) === opts.rig)
: rigRes.data;
const allNodes: NodeEntry[] = [];
for (const rig of rigRes.data) {
for (const rig of effectiveRigs) {
const nodesRes = await client.get<NodeEntry[]>(`/api/rigs/${encodeURIComponent(rig.rigId)}/nodes`, requestHeaders ? { headers: requestHeaders } : undefined);
if (nodesRes.status >= 400) {
console.error(`Warning: failed to fetch nodes for rig "${rig.rigName ?? rig.name}" (HTTP ${nodesRes.status}). List rigs with: rig ps`);
@@ -1013,10 +1061,17 @@ async function runCrossHostPs(
const argv: string[] = ["rig", "ps"];
if (opts.nodes) argv.push("--nodes");
if (opts.full) argv.push("--full");
// OPR.0.4.0.34: forward the breadth flag so `--host h -A` keeps all-rigs
// breadth across the hop (the current-rig default is local-only and never
// applied to a remote call, but an explicit -A must still reach the remote).
if (opts.allRigs) argv.push("--all-rigs");
if (opts.limit !== undefined) argv.push("--limit", opts.limit);
if (opts.fields !== undefined) argv.push("--fields", opts.fields);
if (opts.summary) argv.push("--summary");
if (opts.filter !== undefined) argv.push("--filter", opts.filter);
// OPR.0.4.0.34: opts.active is the normalized form (set by --active OR
// --running). Forward it so the state filter survives the hop.
if (opts.active) argv.push("--active");
if (opts.rig !== undefined) argv.push("--rig", opts.rig);
if (opts.session !== undefined) argv.push("--session", opts.session);
if (opts.includeArchived) argv.push("--include-archived");
@@ -555,3 +555,35 @@ describe("whoami --host (cross-host short-circuit)", () => {
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");
});
});
+8 -2
View File
@@ -170,8 +170,11 @@ const NODES_BY_RIG: Record<string, unknown[]> = {
describe("OPR.0.4.0.25 — rig ps token-safe defaults", () => {
let server: http.Server;
let port: number;
let savedSessionName: string | undefined;
beforeAll(async () => {
savedSessionName = process.env.OPENRIG_SESSION_NAME;
delete process.env.OPENRIG_SESSION_NAME;
server = http.createServer((req, res) => {
const m = req.url?.match(/^\/api\/rigs\/([^/]+)\/nodes(?:\?.*)?$/);
if (m && req.method === "GET") {
@@ -190,7 +193,10 @@ describe("OPR.0.4.0.25 — rig ps token-safe defaults", () => {
port = (server.address() as { port: number }).port;
});
afterAll(() => server.close());
afterAll(() => {
server.close();
if (savedSessionName !== undefined) process.env.OPENRIG_SESSION_NAME = savedSessionName;
});
function makeCmd(): Command {
const prog = new Command();
@@ -378,7 +384,7 @@ describe("OPR.0.4.0.25 — rig ps token-safe defaults", () => {
expect(helpOutput).toContain("--full");
expect(helpOutput).toContain("--rig");
expect(helpOutput).toContain("--session");
expect(helpOutput).toContain("rig queue");
expect(helpOutput).toContain("current-rig");
expect(helpOutput).toContain("compact");
});
+295
View File
@@ -0,0 +1,295 @@
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from "vitest";
import http from "node:http";
import { Command } from "commander";
import { psCommand } from "../src/commands/ps.js";
import { DaemonClient } from "../src/client.js";
import { STATE_FILE, type LifecycleDeps, type DaemonState } from "../src/daemon-lifecycle.js";
import type { StatusDeps } from "../src/commands/status.js";
function mockLifecycleDeps(overrides?: Partial<LifecycleDeps>): LifecycleDeps {
return {
spawn: vi.fn(() => ({ pid: 1, unref: vi.fn() }) as never),
fetch: vi.fn(async () => ({ ok: true })),
kill: vi.fn(() => true),
readFile: vi.fn(() => null),
writeFile: vi.fn(),
removeFile: vi.fn(),
exists: vi.fn(() => false),
mkdirp: vi.fn(),
openForAppend: vi.fn(() => 3),
isProcessAlive: vi.fn(() => true),
...overrides,
};
}
function captureLogs(fn: () => Promise<void>): Promise<{ logs: string[]; exitCode: number | undefined }> {
return new Promise(async (resolve) => {
const logs: string[] = [];
const origLog = console.log;
const origErr = console.error;
const origExitCode = process.exitCode;
process.exitCode = undefined;
console.log = (...args: unknown[]) => logs.push(args.join(" "));
console.error = (...args: unknown[]) => logs.push(args.join(" "));
try { await fn(); } finally { console.log = origLog; console.error = origErr; }
const exitCode = process.exitCode;
process.exitCode = origExitCode;
resolve({ logs, exitCode });
});
}
function runningDeps(port: number): StatusDeps {
return {
lifecycleDeps: mockLifecycleDeps({
exists: vi.fn((p: string) => p === STATE_FILE),
readFile: vi.fn((p: string) => {
if (p === STATE_FILE) return JSON.stringify({ pid: 123, port, db: "test.sqlite", startedAt: "2026-06-20T00:00:00Z" } as DaemonState);
return null;
}),
fetch: vi.fn(async () => ({ ok: true })),
}),
clientFactory: (baseUrl) => new DaemonClient(baseUrl),
};
}
const PS_DATA = [
{ rigId: "rig-a", name: "openrig-delivery", rigName: "openrig-delivery", nodeCount: 3, runningCount: 3, status: "running", lifecycleState: "running", uptime: "2h", latestSnapshot: null },
{ rigId: "rig-b", name: "openrig-comms", rigName: "openrig-comms", nodeCount: 2, runningCount: 1, status: "partial", lifecycleState: "degraded", uptime: "1h", latestSnapshot: null },
];
function makeNode(rigId: string, rigName: string, logicalId: string, opts?: {
lifecycleState?: string;
sessionStatus?: string;
resumeType?: string;
resumeToken?: string;
resumeCommand?: string;
heldReason?: string;
startupCompletedAt?: string;
}) {
return {
rigId,
rigName,
logicalId,
podId: "pod-1",
canonicalSessionName: `${logicalId.replace(".", "-")}@${rigName}`,
nodeKind: "agent",
runtime: "claude-code",
sessionStatus: opts?.sessionStatus ?? "running",
startupStatus: "ready",
restoreOutcome: "n-a",
lifecycleState: opts?.lifecycleState ?? "running",
tmuxAttachCommand: null,
resumeCommand: opts?.resumeCommand ?? null,
latestError: null,
hasAssignedWork: false,
pendingWorkCount: 0,
resumeType: opts?.resumeType ?? null,
resumeToken: opts?.resumeToken ?? null,
heldReason: opts?.heldReason ?? null,
startupCompletedAt: opts?.startupCompletedAt ?? null,
};
}
const NODES_BY_RIG: Record<string, unknown[]> = {
"rig-a": [
makeNode("rig-a", "openrig-delivery", "dev1.driver", {
resumeType: "codex_id",
resumeToken: "019e376e-secret-token-do-not-leak",
resumeCommand: "codex resume 019e376e-secret-token-do-not-leak",
heldReason: "operator hold",
startupCompletedAt: "2026-06-20T23:00:00.000Z",
}),
makeNode("rig-a", "openrig-delivery", "dev1.qa"),
makeNode("rig-a", "openrig-delivery", "dev1.guard", { lifecycleState: "recoverable", sessionStatus: "detached" }),
],
"rig-b": [
makeNode("rig-b", "openrig-comms", "orch1.lead"),
makeNode("rig-b", "openrig-comms", "content1.writer", { lifecycleState: "attention_required" }),
],
};
describe("OPR.0.4.0.34 — rig ps current-rig default", () => {
let server: http.Server;
let port: number;
let savedSession: string | undefined;
beforeAll(async () => {
server = http.createServer((req, res) => {
const m = req.url?.match(/^\/api\/rigs\/([^/]+)\/nodes/);
if (m && req.method === "GET") {
const rigId = decodeURIComponent(m[1]!);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(NODES_BY_RIG[rigId] ?? []));
} else if (req.url?.startsWith("/api/ps") && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(PS_DATA));
} else {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "not found" }));
}
});
await new Promise<void>((resolve) => server.listen(0, resolve));
port = (server.address() as { port: number }).port;
});
afterAll(() => server.close());
afterEach(() => {
if (savedSession === undefined) delete process.env.OPENRIG_SESSION_NAME;
else process.env.OPENRIG_SESSION_NAME = savedSession;
savedSession = undefined;
});
function makeCmd(): Command {
const prog = new Command();
prog.exitOverride();
prog.addCommand(psCommand(runningDeps(port)));
return prog;
}
function setSession(name: string) {
savedSession = process.env.OPENRIG_SESSION_NAME;
process.env.OPENRIG_SESSION_NAME = name;
}
// AC-1: rig-level defaults to current rig
it("AC-1: bare rig ps --json returns only the caller's rig", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--json"]);
});
const parsed = JSON.parse(logs.join(""));
expect(Array.isArray(parsed)).toBe(true);
expect(parsed.length).toBe(1);
expect(parsed[0].rigName ?? parsed[0].name).toBe("openrig-delivery");
});
// AC-1: node-level defaults to current rig
it("AC-1: bare rig ps --nodes --json returns only the current rig's nodes", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json"]);
});
const nodes = JSON.parse(logs.join(""));
expect(Array.isArray(nodes)).toBe(true);
expect(nodes.length).toBe(3);
for (const n of nodes) {
expect(n.rigName).toBe("openrig-delivery");
}
});
// AC-1: -A restores fleet breadth
it("AC-1: -A shows all rigs", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--json", "-A"]);
});
const parsed = JSON.parse(logs.join(""));
expect(parsed.length).toBe(2);
});
// AC-1: explicit --rig overrides caller default
it("AC-1: explicit --rig overrides caller default", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--json", "--rig", "openrig-comms"]);
});
const parsed = JSON.parse(logs.join(""));
expect(parsed.length).toBe(1);
expect(parsed[0].rigName ?? parsed[0].name).toBe("openrig-comms");
});
// AC-2: all-states preserved (non-running nodes visible by default)
it("AC-2: non-running nodes appear in default (all-states)", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json"]);
});
const nodes = JSON.parse(logs.join(""));
const detached = nodes.find((n: Record<string, unknown>) => n.canonicalSessionName === "dev1-guard@openrig-delivery");
expect(detached).toBeDefined();
});
// AC-4: compact row has resumeType + resumeTokenPresent, NOT the token value
it("AC-4: compact includes resumeType+resumeTokenPresent, excludes token value and resumeCommand", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json"]);
});
const nodes = JSON.parse(logs.join(""));
const driver = nodes.find((n: Record<string, unknown>) => n.canonicalSessionName === "dev1-driver@openrig-delivery");
expect(driver).toBeDefined();
expect(driver.resumeType).toBe("codex_id");
expect(driver.resumeTokenPresent).toBe(true);
expect(driver.resumeToken).toBeUndefined();
expect(driver.resumeCommand).toBeUndefined();
const output = logs.join("");
expect(output).not.toContain("019e376e-secret-token-do-not-leak");
});
// OPR.0.4.0.34 guard fix-back: compact rows must carry the full FR-4 orch
// field set (source-available), not an underspecified subset.
it("FR-4: compact rows carry the orch field set (rigId/logicalId/sessionStatus/startupStatus/lastActivity/heldReason) with no secret leak", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json"]);
});
const nodes = JSON.parse(logs.join(""));
const driver = nodes.find((n: Record<string, unknown>) => n.canonicalSessionName === "dev1-driver@openrig-delivery");
expect(driver).toBeDefined();
// Required FR-4 orch identity + state fields, all present in the compact row.
for (const key of [
"rigId", "rigName", "logicalId", "canonicalSessionName",
"sessionStatus", "startupStatus", "lifecycleState",
"agentActivity", "hasAssignedWork", "pendingWorkCount",
"resumeType", "resumeTokenPresent", "lastActivity",
]) {
expect(driver, `compact row missing FR-4 key: ${key}`).toHaveProperty(key);
}
expect(driver.rigId).toBe("rig-a");
expect(driver.logicalId).toBe("dev1.driver");
expect(driver.sessionStatus).toBe("running");
expect(driver.startupStatus).toBe("ready");
// updated/age proxy: agentActivity.sampledAt then startupCompletedAt then null.
expect(driver.lastActivity).toBe("2026-06-20T23:00:00.000Z");
// held/attention reason surfaced (short).
expect(driver.heldReason).toBe("operator hold");
// Secret absence holds even with the wider field set.
expect(driver.resumeToken).toBeUndefined();
expect(driver.resumeCommand).toBeUndefined();
});
// AC-4: --full includes the token value
it("AC-4: --full includes resumeToken and resumeCommand", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json", "--full"]);
});
const nodes = JSON.parse(logs.join(""));
const driver = nodes.find((n: Record<string, unknown>) => n.canonicalSessionName === "dev1-driver@openrig-delivery");
expect(driver.resumeToken).toBe("019e376e-secret-token-do-not-leak");
expect(driver.resumeCommand).toBe("codex resume 019e376e-secret-token-do-not-leak");
});
// AC-5: --running is alias for --active
it("AC-5: --running is alias for --active", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs: activeLogs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json", "--active"]);
});
const { logs: runningLogs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json", "--running"]);
});
expect(activeLogs.join("")).toBe(runningLogs.join(""));
});
// AC-5: --running + --filter conflict
it("AC-5: --running + --filter is rejected", async () => {
setSession("dev1-driver@openrig-delivery");
const { logs, exitCode } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "ps", "--nodes", "--json", "--running", "--filter", "status=running"]);
});
expect(exitCode).toBe(1);
expect(logs.some((l) => l.includes("cannot be combined"))).toBe(true);
});
});
@@ -88,8 +88,11 @@ describe("PL-012 ps --filter contextUsage.* + CTX field", () => {
let server: http.Server;
let port: number;
let nodesByRig: Record<string, unknown[]>;
let savedSessionName: string | undefined;
beforeAll(async () => {
savedSessionName = process.env.OPENRIG_SESSION_NAME;
delete process.env.OPENRIG_SESSION_NAME;
nodesByRig = {};
server = http.createServer((req, res) => {
const m = req.url?.match(/^\/api\/rigs\/([^/]+)\/nodes(?:\?.*)?$/);
@@ -107,7 +110,10 @@ describe("PL-012 ps --filter contextUsage.* + CTX field", () => {
await new Promise<void>((resolve) => server.listen(0, resolve));
port = (server.address() as { port: number }).port;
});
afterAll(() => server.close());
afterAll(() => {
server.close();
if (savedSessionName !== undefined) process.env.OPENRIG_SESSION_NAME = savedSessionName;
});
function makeCmd(): Command {
const prog = new Command();
+7 -1
View File
@@ -156,8 +156,11 @@ describe("PL-019 ps --filter agentActivity.state + --active", () => {
let server: http.Server;
let port: number;
let nodesByRig: Record<string, unknown[]>;
let savedSessionName: string | undefined;
beforeAll(async () => {
savedSessionName = process.env.OPENRIG_SESSION_NAME;
delete process.env.OPENRIG_SESSION_NAME;
nodesByRig = {};
server = http.createServer((req, res) => {
const m = req.url?.match(/^\/api\/rigs\/([^/]+)\/nodes(?:\?.*)?$/);
@@ -176,7 +179,10 @@ describe("PL-019 ps --filter agentActivity.state + --active", () => {
port = (server.address() as { port: number }).port;
});
afterAll(() => server.close());
afterAll(() => {
server.close();
if (savedSessionName !== undefined) process.env.OPENRIG_SESSION_NAME = savedSessionName;
});
function makeCmd(): Command {
const prog = new Command();
+7 -1
View File
@@ -57,8 +57,11 @@ describe("Ps CLI", () => {
let port: number;
let psData: unknown[];
let nodesData: Record<string, unknown[]>;
let savedSessionName: string | undefined;
beforeAll(async () => {
savedSessionName = process.env.OPENRIG_SESSION_NAME;
delete process.env.OPENRIG_SESSION_NAME;
psData = [];
nodesData = {};
server = http.createServer(async (req, res) => {
@@ -83,7 +86,10 @@ describe("Ps CLI", () => {
port = (server.address() as { port: number }).port;
});
afterAll(() => { server.close(); });
afterAll(() => {
server.close();
if (savedSessionName !== undefined) process.env.OPENRIG_SESSION_NAME = savedSessionName;
});
function makeCmd(): Command {
const prog = new Command();