feat(crash-cart): C3 unit-B rig crash-cart verb — JSON action (injected emit)

The 'rig crash-cart --json' command: prints ONE JSON = the emit verdict (read-only,
public agent-readable — the 4 rails). UP→state / DOWN→discovery / DOWN+refusal→
STRUCTURED refusal note (never exit-code-only) / UNVERIFIED→evidence. emit + write
injected; the real default LAZY-imports @openrig/daemon/crash-cart at invocation (dep
rail 2: rig startup + other verbs stay free of the daemon load). Exit code is a HINT
(non-zero when not cleanly up); the JSON is the contract.

RED-first: 4/4 (JSON contract per state). Not yet registered in index.ts — that + the
real emit wiring land with the daemon subpath export (packaging ruling A).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Opus 4.8 committed 2026-08-06 10:54:31 +00:00
1 parent 2f6f5b7692
commit 4c8e4c8a1f
2 files changed
+91

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
import { Command } from "commander";
// `rig crash-cart --json` — the daemon-DOWN recovery verdict emit (plan c015d9ed §C3, coupling ruling
// option A). Prints ONE JSON = the 3-state detector verdict + (on DOWN) the discovery — READ-ONLY, a
// public agent-readable surface (the 4 rails). A fail-closed refusal still prints STRUCTURED JSON
// (never exit-code-only). The `emit` is injected: the real wiring imports emitCrashCartState from the
// scoped @openrig/daemon/crash-cart subpath (reuse VERBATIM) — set up alongside this command.
//
// NOTE: this CLI-local CrashCartEmit is the documented parse/print contract; it matches the daemon's
// emit shape (the JSON contract). When the subpath export lands, the real emit is wired as the default.
export type DaemonState = "up" | "down" | "unverified";
export interface CrashCartEmit {
state: DaemonState;
evidence?: { pidState: string; probeResult: string; failedSignal: string };
discovery?: unknown;
refusal?: string;
}
export interface CrashCartCommandDeps {
/** Produce the verdict (the real wiring: emitCrashCartState with live probes + the C2 read). */
emit: () => Promise<CrashCartEmit>;
/** Emit one line (default: stdout). */
write: (line: string) => void;
}
export function crashCartCommand(deps?: Partial<CrashCartCommandDeps>): Command {
const cmd = new Command("crash-cart").description(
"Emit the daemon-down recovery verdict + discovery as JSON (read-only).",
);
cmd.option("--json", "emit JSON (the default machine-readable form)");
cmd.action(async () => {
const emit =
deps?.emit ??
(() => {
throw new Error("crash-cart: real emit wiring pending the @openrig/daemon/crash-cart subpath export");
});
const write = deps?.write ?? ((line: string) => process.stdout.write(line + "\n"));
const result = await emit();
// Rail 3: the JSON is the truth (verdict + discovery, or a structured refusal). Always printed.
write(JSON.stringify(result));
// A non-zero exit is a HINT only (not-cleanly-up); the JSON remains the contract.
process.exitCode = result.state === "up" ? 0 : 1;
});
return cmd;
}
@@ -0,0 +1,44 @@
import { describe, it, expect, vi } from "vitest";
import { crashCartCommand, type CrashCartEmit } from "../src/commands/crash-cart.js";
// Crash-cart C3 unit-B — the `rig crash-cart --json` verb (coupling ruling C, 4 rails). It prints ONE
// JSON = emitCrashCartState's verdict (read-only). A fail-closed refusal still prints STRUCTURED JSON
// (never exit-code-only). emit + write are injected → the action is deterministic; the real-dep
// assembly is glue proven by the real daemon-down run.
function run(emit: () => Promise<CrashCartEmit>) {
const write = vi.fn();
const cmd = crashCartCommand({ emit, write });
return { write, done: cmd.parseAsync(["--json"], { from: "user" }) };
}
describe("crashCartCommand — prints the emit verdict as ONE JSON", () => {
it("UP → JSON {state:'up'}", async () => {
const { write, done } = run(async () => ({ state: "up" }));
await done;
expect(JSON.parse(write.mock.calls[0][0])).toEqual({ state: "up" });
});
it("DOWN → JSON with the discovery", async () => {
const emit: CrashCartEmit = { state: "down", discovery: { header: { lastActivityAt: null, lastBootAt: null, firstBootAt: null, hostId: null, stopReason: null, priorUptimeMs: null }, foundOnHost: [], whereWorkStopped: [] } };
const { write, done } = run(async () => emit);
await done;
expect(JSON.parse(write.mock.calls[0][0])).toEqual(emit);
});
it("DOWN + refusal → STRUCTURED JSON (refusal note, no discovery); still printed, not exit-code-only", async () => {
const { write, done } = run(async () => ({ state: "down", refusal: "a daemon answered /healthz — refusing" }));
await done;
const out = JSON.parse(write.mock.calls[0][0]);
expect(out.state).toBe("down");
expect(out.refusal).toContain("refusing");
expect(out.discovery).toBeUndefined();
expect(write).toHaveBeenCalledTimes(1);
});
it("UNVERIFIED → JSON with the evidence", async () => {
const { write, done } = run(async () => ({ state: "unverified", evidence: { pidState: "alive", probeResult: "timeout", failedSignal: "healthz timed out" } }));
await done;
expect(JSON.parse(write.mock.calls[0][0]).evidence.probeResult).toBe("timeout");
});
});