mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
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:
1 parent
2f6f5b7692
commit
4c8e4c8a1f
2 files changed
+91
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user