Files
openrig/packages/cli/test/proof.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

259 lines
10 KiB
TypeScript

// OPR.0.4.4.19 FR-8 + FR-11 — rig proof add: C1 header validation at drop
// time, D2 attestation echo, contract + C8 advisories (advise-never-block).
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import YAML from "yaml";
import {
proofCommand,
validateC1Header,
parseProofContract,
C1_ARTIFACT_TYPES,
C1_VERDICTS,
} from "../src/commands/proof.js";
describe("validateC1Header (pure)", () => {
const valid = {
slice: "OPR.0.4.4.19",
candidate_sha: "abc1234",
artifact_type: "qa",
verdict: "CLEAR",
money_evidence: "park->resolve walk transitions row shows decision text",
};
it("accepts the five required fields with closed-set values", () => {
expect(validateC1Header(valid).ok).toBe(true);
});
it("names every missing field", () => {
const r = validateC1Header({ artifact_type: "qa" });
expect(r.ok).toBe(false);
expect(r.missing).toEqual(["slice", "candidate_sha", "verdict", "money_evidence"]);
});
it("rejects out-of-set artifact_type/verdict naming the allowed values (BR-4 closed sets)", () => {
const r = validateC1Header({ ...valid, artifact_type: "designer", verdict: "SHIP-IT" });
expect(r.ok).toBe(false);
expect(r.invalid).toHaveLength(2);
expect(r.invalid[0]!.allowed).toEqual(C1_ARTIFACT_TYPES);
expect(r.invalid[1]!.allowed).toEqual(C1_VERDICTS);
});
});
describe("parseProofContract (pure)", () => {
it("returns null when no ## Proof contract section exists (zero-noise degrade)", () => {
expect(parseProofContract("# PRD\n\n## Acceptance\n- [ ] thing\n")).toBeNull();
});
it("parses checkbox items until the next section", () => {
const prd = [
"# PRD",
"## Proof contract",
"- [ ] the live park->resolve walk with the transitions row shown",
"- [x] approve run showing frontmatter + audit row together",
"- plain item without checkbox",
"## Next section",
"- [ ] NOT a contract item",
].join("\n");
expect(parseProofContract(prd)).toEqual([
"the live park->resolve walk with the transitions row shown",
"approve run showing frontmatter + audit row together",
"plain item without checkbox",
]);
});
});
describe("rig proof add (fs-level, temp workspace)", () => {
let workRoot: string;
let sliceDir: string;
let logs: string[];
let errs: string[];
beforeEach(() => {
workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "proof-test-"));
const missionDir = path.join(workRoot, "missions", "release-x", "slices", "19-signal-layer");
fs.mkdirSync(missionDir, { recursive: true });
fs.writeFileSync(path.join(workRoot, "missions", "release-x", "README.md"), "---\nid: OPR.X\n---\n# m\n");
sliceDir = missionDir;
fs.writeFileSync(
path.join(sliceDir, "README.md"),
"---\nid: OPR.X.19\nstatus: building\n---\n# slice\n",
);
logs = [];
errs = [];
vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => { logs.push(a.join(" ")); });
vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => { errs.push(a.join(" ")); });
process.exitCode = undefined;
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(workRoot, { recursive: true, force: true });
process.exitCode = undefined;
});
async function run(args: string[]): Promise<void> {
const cmd = proofCommand();
cmd.exitOverride();
await cmd.parseAsync(["node", "proof", "--workspace", workRoot, ...args]);
}
const baseArgs = (extra: string[] = []) => [
"add", "19-signal-layer", "--mission", "release-x",
"--artifact-type", "qa", "--verdict", "CLEAR",
"--candidate-sha", "abc1234",
"--money-evidence", "one line of money",
"--body", "evidence body",
"--name", "qa-clear.md",
...extra,
];
it("happy drop: writes proof/<name> with valid YAML frontmatter and echoes the parsed header; exit 0", async () => {
await run(baseArgs());
const target = path.join(sliceDir, "proof", "qa-clear.md");
expect(fs.existsSync(target)).toBe(true);
const content = fs.readFileSync(target, "utf8");
const fm = content.split("---")[1]!;
const parsed = YAML.parse(fm) as Record<string, unknown>;
expect(parsed.slice).toBe("OPR.X.19");
expect(parsed.candidate_sha).toBe("abc1234");
expect(parsed.artifact_type).toBe("qa");
expect(parsed.verdict).toBe("CLEAR");
expect(content).toContain("evidence body");
expect(logs.join("\n")).toContain("Parsed C1 header");
expect(process.exitCode).toBeUndefined();
});
it("out-of-set verdict is rejected naming the allowed values; nothing written; exit 1", async () => {
await run([
"add", "19-signal-layer", "--mission", "release-x",
"--artifact-type", "qa", "--verdict", "SHIP-IT",
"--candidate-sha", "abc1234", "--money-evidence", "m", "--body", "b",
]);
expect(process.exitCode).toBe(1);
expect(errs.join("\n")).toContain("closed set");
expect(fs.existsSync(path.join(sliceDir, "proof"))).toBe(false);
});
it("D2: evidences + self_check are parsed and echoed; unknown refs warn (never reject)", async () => {
fs.writeFileSync(
path.join(sliceDir, "IMPLEMENTATION-PRD.md"),
"# PRD\n## Proof contract\n- [ ] item one\n- [ ] item two\n",
);
await run(baseArgs(["--evidences", "1,bogus-ref", "--self-check", "I looked; the walk shows the decision text"]));
expect(process.exitCode).toBeUndefined();
const content = fs.readFileSync(path.join(sliceDir, "proof", "qa-clear.md"), "utf8");
expect(content).toContain("self_check");
expect(errs.join("\n")).toContain("bogus-ref");
});
it("contract advisory: declared contract + no covered item/self_check => drop SUCCEEDS with advisory naming uncovered items", async () => {
fs.writeFileSync(
path.join(sliceDir, "IMPLEMENTATION-PRD.md"),
"# PRD\n## Proof contract\n- [ ] item one\n",
);
await run(baseArgs());
expect(process.exitCode).toBeUndefined();
expect(fs.existsSync(path.join(sliceDir, "proof", "qa-clear.md"))).toBe(true);
expect(errs.join("\n")).toContain("ADVISORY (D2");
expect(errs.join("\n")).toContain("item one");
});
it("zero noise: NO contract declared => no contract advisory", async () => {
await run(baseArgs());
expect(errs.join("\n")).not.toContain("ADVISORY (D2");
});
it("C8: ux-change slice + no video => drop succeeds with the screencast advisory; exit 0", async () => {
fs.writeFileSync(
path.join(sliceDir, "README.md"),
"---\nid: OPR.X.19\nstatus: building\nux-change: true\n---\n# slice\n",
);
await run(baseArgs());
expect(process.exitCode).toBeUndefined();
expect(errs.join("\n")).toContain("ADVISORY (C8");
expect(errs.join("\n")).toContain("agent-browser-screencast");
});
it("C8 zero noise: no ux-change flag => no video advisory; existing video also silences it", async () => {
await run(baseArgs());
expect(errs.join("\n")).not.toContain("ADVISORY (C8");
// Now flag the slice AND plant a video — advisory stays silent.
fs.writeFileSync(
path.join(sliceDir, "README.md"),
"---\nid: OPR.X.19\nux-change: true\n---\n# slice\n",
);
fs.writeFileSync(path.join(sliceDir, "proof", "walk.mp4"), "fake video bytes");
errs.length = 0;
await run(baseArgs(["--name", "qa-clear-2.md"]));
expect(errs.join("\n")).not.toContain("ADVISORY (C8");
});
});
// rev1-r2 BLOCKING regression (candidate a7dedd93 review): --name must be a
// filename, never a path — a traversal name can never escape proof/.
describe("rig proof add --name traversal rejection (rev1-r2 fixback)", () => {
let workRoot: string;
let sliceDir: string;
let errs: string[];
beforeEach(() => {
workRoot = fs.mkdtempSync(path.join(os.tmpdir(), "proof-trav-"));
sliceDir = path.join(workRoot, "missions", "release-x", "slices", "19-signal-layer");
fs.mkdirSync(sliceDir, { recursive: true });
fs.writeFileSync(path.join(workRoot, "missions", "release-x", "README.md"), "---\nid: OPR.X\n---\n# m\n");
fs.writeFileSync(path.join(sliceDir, "README.md"), "---\nid: OPR.X.19\nstatus: building\n---\n# ORIGINAL README BODY\n");
errs = [];
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => { errs.push(a.join(" ")); });
process.exitCode = undefined;
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(workRoot, { recursive: true, force: true });
process.exitCode = undefined;
});
async function runAdd(name: string): Promise<void> {
const cmd = proofCommand();
cmd.exitOverride();
await cmd.parseAsync([
"node", "proof", "--workspace", workRoot,
"add", "19-signal-layer", "--mission", "release-x",
"--artifact-type", "qa", "--verdict", "CLEAR",
"--candidate-sha", "abc1234", "--money-evidence", "m",
"--body", "traversal attempt body",
"--name", name,
]);
}
it("--name ../README.md is REJECTED and the slice README is NOT modified", async () => {
const readmePath = path.join(sliceDir, "README.md");
const before = fs.readFileSync(readmePath, "utf8");
await runAdd("../README.md");
expect(process.exitCode).toBe(1);
expect(errs.join("\n")).toContain("not a plain filename");
expect(fs.readFileSync(readmePath, "utf8")).toBe(before);
// Nothing landed in proof/ either.
const proofDir = path.join(sliceDir, "proof");
expect(!fs.existsSync(proofDir) || fs.readdirSync(proofDir).length === 0).toBe(true);
});
it("other escape shapes are rejected: nested path, backslash, absolute, dot-dot", async () => {
for (const name of ["sub/dir.md", "..\\\\evil.md", "/tmp/abs.md", ".."]) {
process.exitCode = undefined;
await runAdd(name);
expect(process.exitCode, `name '${name}' must be rejected`).toBe(1);
}
});
it("a plain filename still drops normally after the fix", async () => {
await runAdd("qa-clear.md");
expect(process.exitCode).toBeUndefined();
expect(fs.existsSync(path.join(sliceDir, "proof", "qa-clear.md"))).toBe(true);
});
});