feat(cli): Restore-Packet M3 — rig restore-packet read + validate actual implementations

Closes M3 dispatch qitem-20260502023319-d997e182. M2 marshal-acceptance gate PASSED at predecessor c7b74fa (cumulative across M2a + M2b + M2c-CLI + M2c-Daemon).

M3 SCOPE:
- read: replaces M3-pending stub with real implementation. Default human output prints restore-instructions.md body + summary metadata (source_session_id, source_rig, source_cwd, source_runtime, target_rig, target_runtime, generator_version, bounded_latest_transcript bound + message_count, omitted_classes) + transcript digest (byte size of transcript.md when present, or absence-line per contract § 1 parity rule). --json mode emits raw restore-summary.json verbatim (round-trippable per contract § 8).
- validate: replaces M3-pending stub with real implementation. Schema validation via existing validateRestoreSummary. Packet-shape check: 4 required files per contract § 1 (restore-instructions.md, transcript-latest.md, touched-files.md, restore-summary.json). Parity check (per contract § 1 + § 8): full_transcript summary key ↔ transcript.md presence; either-side-only is a required-field-class error. redaction_policy_id enum check ONLY (per contract § 4 + § 8: validator does NOT scan transcript content for residual credential patterns; v0 deliberate behavior matches Velocity precedent). --json mode emits { valid, errors: [{field,value,rule,severity}] }. Exit code matrix per contract § 8: required-field violations → exit nonzero; parity violations → exit nonzero; optional-field malformations → exit 0 with WARNING-level output; no errors → exit 0.

SCHEMA-VALIDATOR UPDATE:
- packages/cli/src/restore-packet/schema-validator.ts: differentiates severity per error. Errors at instancePath beginning with /full_transcript receive severity "warning" (full_transcript is the only top-level optional field in v0; nested malformations are optional-field issues). All other errors stay severity "error". `valid: false` semantics preserved on ANY ajv violation (preserves M2c writer pre-rename guard); the M3 validate command consults errors[].severity to decide the exit code per contract § 8.

NEW PACKET-SHAPE CONST:
- REQUIRED_PACKET_FILES const at packages/cli/src/commands/restore-packet.ts: ["restore-instructions.md","transcript-latest.md","touched-files.md","restore-summary.json"] per contract § 1. Single source of truth for the validate packet-shape check (and used by read for transcript-presence detection).

TESTS (TDD red-then-green; 14 new cases at packages/cli/test/restore-packet.test.ts):

read tests (4 cases):
- read human output: prints restore-instructions body + summary metadata + transcript digest line.
- read --json: stdout round-trippable to restore-summary.json (deep-equal check).
- read missing restore-summary.json: explicit error, exit nonzero.
- read packet without transcript.md AND without full_transcript key: success with "transcript.md absent" line.

validate tests (10 cases):
- validate ACCEPT well-formed packet WITH transcript.md + full_transcript key (parity respected).
- validate ACCEPT well-formed packet WITHOUT transcript.md AND WITHOUT full_transcript key (parity via absence).
- validate REJECT packet with full_transcript key but transcript.md absent (parity violation, exit nonzero).
- validate REJECT packet with transcript.md present but full_transcript key omitted (parity violation, exit nonzero).
- validate REJECT packet with required field missing (per-field error reported; source_session_id deletion case).
- validate REJECT packet missing one of 4 required files (touched-files.md deletion case).
- validate --json: machine-readable shape { valid: boolean, errors: [...] }.
- validate optional-field malformation (full_transcript.line_count wrong type): exit 0 with WARNING text per contract § 8 canonical rule.
- validate redaction_policy_id ENUM-ONLY check: packet with redaction_policy_id="openrig-v0" + transcript.md containing FAKETESTSTRING-NOT-A-REAL-CREDENTIAL-prefixed string → PASSES validate (deliberate v0 behavior per contract § 4 + § 8; matches Velocity precedent).
- validate redaction_policy_id WRONG enum value → REJECT.

All tests use createProgram().parseAsync against fixtures built via the M2c-CLI write action surface (Quality Lesson v12 carry-forward).

RE-VERIFICATION COMMANDS (all green; node v22.22.1 per better-sqlite3 native module 127):

cd packages/cli:
- npx vitest run test/restore-packet.test.ts → 100/100 PASS (86 baseline + 14 new M3).
- npx tsc --noEmit → exit 0.
- npm run build → exit 0; dist/schemas/restore-summary.schema.json present.

Compiled-output runtime checks (Quality Lesson v10 + v12):
- node -e '<createProgram parseAsync ["read", target, "--json"]>' → JSON.parse stdout yields summary; round-trip OK; source_session_id correct.
- node -e '<createProgram parseAsync ["validate", target, "--json"]>' → JSON.parse stdout yields { valid: true, errors: [] }.

Per-package honest baselines (Quality Lesson v8):
- packages/cli: 744 total / 740 PASS / 4 FAIL named flakes (chatroom wait timing, config-store env pollution, two preflight host-port). Baseline: 726 → 740 = +14 new tests; 4 flakes unchanged.
- packages/daemon: 2242 total / 2242 PASS / 0 FAIL. M3 did NOT touch daemon code; no spillover regression.

ENVIRONMENT NOTES:
- Node version: v22.22.1 (matches better-sqlite3 native module 127 per M2c-Daemon learning).
- All test commands run on host node; no Mode 3 Tart VM needed (write-side adapters already proven; read/validate touch only fs + JSON + schema-validator).

M3 MARSHAL-ACCEPTANCE GATE (per IMPL § M3 line 186-191):
ALL CRITERIA MET:
- All M2 tests still green: DONE (no regression).
- All M3 read/validate tests green: DONE (14/14).
- Per-field error reporting verified: DONE (source_session_id deletion test).
- Parity check tests (both directions) pass: DONE (parity-1 + parity-2 cases).
- redaction_policy_id enum-only test passes (deliberate v0 behavior preserved): DONE.
- Exit code matrix verified: DONE (required-field error → nonzero; parity → nonzero; warning-only → 0; clean → 0).
- Compiled-output runtime check for both read and validate passes: DONE.
- Posture full QA/testing window: DONE.
- Full CLI suite numbers match 726 + N new / 4 named flakes: DONE.
- Daemon suite remains 2242/2242 (no spillover): DONE.

BOUNDARY DISCIPLINE:
- ONLY 3 files changed (verified via git diff --cached --stat):
  packages/cli/src/commands/restore-packet.ts (+174/-8; read + validate impls + REQUIRED_PACKET_FILES const)
  packages/cli/src/restore-packet/schema-validator.ts (+9/-5; severity differentiation for full_transcript-path errors)
  packages/cli/test/restore-packet.test.ts (+312/-0; 14 new M3 cases)
- M2 surfaces UNTOUCHED: write CLI (M2c-CLI R2 deriveProvenance + adapter logic frozen); Codex/Claude parsers; redaction; runtime-detect; omitted-records; types; schema embed; packet-writer; daemon full-read route; daemon redaction module; daemon route tests.
- M4/M5/M6 surfaces UNTOUCHED.
- Velocity prior-art .mjs UNCHANGED.
- Unrelated dirty work (docs/*, ps.ts, codex-runtime-adapter, daemon SKILL.md) NOT staged.
- No real auth tokens, device codes, or credentials in fixtures (synthetic patterns use FAKETESTSTRING-NOT-A-REAL-CREDENTIAL prefix per dispatch guidance).

CUMULATIVE QUALITY-LESSON CARRY-FORWARD (v1-v9 + v10-v12 candidates):
- v6, v7 refined (M2c-Daemon's auth-NONE-per-orch-decision is a clean v7-refined posture-chain example).
- v8 (honest baseline match per-package).
- v9 (no real-looking credential strings; fixture uses obviously-synthetic FAKETESTSTRING-NOT-A-REAL-CREDENTIAL prefix).
- v10 (asset packaging): read + validate modules importable from compiled dist/.
- v11 (provider transcript parser nested-content coverage): preserved at M2b R2 + M2c regression suite.
- v12 (writer-internal round trips not enough for CLI source adapters): exercised at M3 — all 14 new tests use createProgram().parseAsync against fixture packets built via M2c write action.

Posture: full QA/testing window (matches dispatch + IMPL § M3 line 191).
Dispatch: qitem-20260502023319-d997e182.
Predecessor: openrig c7b74fa (M2 marshal-gate PASS).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-01 19:40:01 -07:00
1 parent c7b74fa08f
commit a5132dad87
3 files changed
+495 -13

No files matched your search

+170 -12
View File
@@ -10,8 +10,8 @@
// M3: real read + validate implementations replacing the stubs.
import { Command } from "commander";
import { readFileSync } from "node:fs";
import { resolve as resolvePath } from "node:path";
import { readFileSync, existsSync, statSync } from "node:fs";
import { resolve as resolvePath, join as joinPath } from "node:path";
import { DaemonClient } from "../client.js";
import { getDaemonStatus, getDaemonUrl, type LifecycleDeps } from "../daemon-lifecycle.js";
import { realDeps } from "./daemon.js";
@@ -19,8 +19,21 @@ import { detectRuntime } from "../restore-packet/runtime-detect.js";
import { parseCodexJsonl } from "../restore-packet/codex-jsonl-parser.js";
import { parseClaudeTranscript } from "../restore-packet/claude-transcript-parser.js";
import { writePacket, type WritePacketOptions } from "../restore-packet/packet-writer.js";
import {
validateRestoreSummary,
type ValidationError,
} from "../restore-packet/schema-validator.js";
import type { SourceRuntime, StructuredTranscript } from "../restore-packet/types.js";
// Per M1 contract § 1: 4 required packet files. Used by validate's
// packet-shape check and by read for transcript-presence detection.
const REQUIRED_PACKET_FILES = [
"restore-instructions.md",
"transcript-latest.md",
"touched-files.md",
"restore-summary.json",
] as const;
export interface RestorePacketDeps {
lifecycleDeps?: LifecycleDeps;
clientFactory?: (url: string) => DaemonClient;
@@ -348,22 +361,167 @@ export function restorePacketCommand(depsOverride?: RestorePacketDeps): Command
.description("Render a restore packet contents (human or --json)")
.argument("<packet-dir>", "Packet directory path")
.option("--json", "Emit machine-readable JSON output")
.action(async (packetDir: string, _opts: { json?: boolean }) => {
console.error(
`rig restore-packet read: M3 implementation pending; packet-dir was '${packetDir}'.`,
);
process.exitCode = 2;
.action(async (packetDir: string, opts: { json?: boolean }) => {
const dir = resolvePath(packetDir);
const summaryPath = joinPath(dir, "restore-summary.json");
if (!existsSync(summaryPath)) {
console.error(
`rig restore-packet read: restore-summary.json not found at ${summaryPath}.`,
);
process.exitCode = 2;
return;
}
let summary: Record<string, unknown>;
try {
summary = JSON.parse(readFileSync(summaryPath, "utf-8")) as Record<string, unknown>;
} catch (err) {
console.error(
`rig restore-packet read: restore-summary.json is not valid JSON: ${(err as Error).message}.`,
);
process.exitCode = 2;
return;
}
if (opts.json) {
// Per IMPL § M3: --json emits raw restore-summary.json content
// verbatim; round-trippable.
console.log(JSON.stringify(summary, null, 2));
return;
}
// Human output: restore-instructions body + summary metadata + transcript digest.
const instructionsPath = joinPath(dir, "restore-instructions.md");
if (existsSync(instructionsPath)) {
console.log(readFileSync(instructionsPath, "utf-8"));
} else {
console.log("(restore-instructions.md absent in packet directory)");
}
console.log("--- Summary metadata ---");
const metadataKeys = [
"source_session_id",
"source_rig",
"source_cwd",
"source_runtime",
"target_rig",
"target_runtime",
"generator_version",
];
for (const k of metadataKeys) {
const v = summary[k];
console.log(`${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
}
const blt = summary.bounded_latest_transcript as { bound?: number; message_count?: number } | undefined;
if (blt) {
console.log(`bounded_latest_transcript.bound: ${blt.bound}`);
console.log(`bounded_latest_transcript.message_count: ${blt.message_count}`);
}
const omittedClasses = summary.omitted_classes;
console.log(`omitted_classes: ${Array.isArray(omittedClasses) ? omittedClasses.join(",") || "(none)" : "(unknown)"}`);
// Transcript digest line (per IMPL § M3 + contract § 1 parity rule).
const transcriptPath = joinPath(dir, "transcript.md");
const hasFullTranscriptKey = Object.prototype.hasOwnProperty.call(summary, "full_transcript");
if (existsSync(transcriptPath)) {
const sz = statSync(transcriptPath).size;
console.log(`transcript.md: ${sz} bytes`);
if (!hasFullTranscriptKey) {
console.log("(parity warning: transcript.md present but full_transcript summary key absent)");
}
} else {
if (hasFullTranscriptKey) {
console.log("(parity warning: full_transcript summary key present but transcript.md absent)");
} else {
console.log("transcript.md absent (full_transcript key absent in summary; valid per parity rule)");
}
}
});
cmd.command("validate")
.description("Validate a restore packet against the v0 schema")
.argument("<packet-dir>", "Packet directory path")
.option("--json", "Emit machine-readable validation report")
.action(async (packetDir: string, _opts: { json?: boolean }) => {
console.error(
`rig restore-packet validate: M3 implementation pending; packet-dir was '${packetDir}'.`,
);
process.exitCode = 2;
.action(async (packetDir: string, opts: { json?: boolean }) => {
const dir = resolvePath(packetDir);
const errors: ValidationError[] = [];
// Packet-shape check: 4 required files per contract § 1.
for (const required of REQUIRED_PACKET_FILES) {
const p = joinPath(dir, required);
if (!existsSync(p)) {
errors.push({
field: required,
value: "<missing>",
rule: `packet-shape: required file missing at ${p}`,
severity: "error",
});
}
}
// Schema validation if restore-summary.json exists.
let summary: Record<string, unknown> | null = null;
const summaryPath = joinPath(dir, "restore-summary.json");
if (existsSync(summaryPath)) {
try {
summary = JSON.parse(readFileSync(summaryPath, "utf-8")) as Record<string, unknown>;
} catch (err) {
errors.push({
field: "restore-summary.json",
value: "<invalid-json>",
rule: `restore-summary.json parse failed: ${(err as Error).message}`,
severity: "error",
});
}
if (summary) {
const schemaResult = validateRestoreSummary(summary);
for (const e of schemaResult.errors) errors.push(e);
}
}
// Parity check (per contract § 1 + § 8): full_transcript summary key
// ↔ transcript.md presence.
if (summary) {
const hasFullTranscriptKey = Object.prototype.hasOwnProperty.call(summary, "full_transcript");
const transcriptExists = existsSync(joinPath(dir, "transcript.md"));
if (hasFullTranscriptKey && !transcriptExists) {
errors.push({
field: "full_transcript",
value: "<key-present-file-absent>",
rule: "parity: full_transcript summary key present but transcript.md absent at packet directory",
severity: "error",
});
}
if (!hasFullTranscriptKey && transcriptExists) {
errors.push({
field: "transcript.md",
value: "<file-present-key-absent>",
rule: "parity: transcript.md present but full_transcript summary key absent",
severity: "error",
});
}
}
const hasErrorSeverity = errors.some((e) => e.severity === "error");
const valid = !hasErrorSeverity;
if (opts.json) {
console.log(JSON.stringify({ valid, errors }, null, 2));
} else {
if (errors.length === 0) {
console.log(`valid: packet at ${dir} passed all checks.`);
} else {
console.log(`${valid ? "valid (with warnings)" : "invalid"}: ${errors.length} issue(s) at ${dir}`);
for (const e of errors) {
const tag = e.severity === "warning" ? "WARNING" : "ERROR";
console.log(` [${tag}] ${e.field}: ${e.rule} (value: ${e.value})`);
}
}
}
// Per contract § 8: required-field violations / parity violations →
// exit nonzero. Optional-field warnings → exit 0.
if (hasErrorSeverity) {
process.exitCode = 2;
}
});
return cmd;
@@ -237,12 +237,24 @@ export function validateRestoreSummary(summary: unknown): ValidationResult {
let field = instancePath.replace(/^\//, "").replace(/\//g, ".");
if (missing) field = field ? `${field}.${missing}` : missing;
if (additional) field = additional;
// Per M1 contract § 8 + IMPL § M3: optional-field malformations are
// severity "warning"; required-field violations are severity "error".
// The only top-level optional field in v0 is `full_transcript`.
// Errors whose path begins at `/full_transcript` come from validating
// the optional object's contents, not from a missing-required check
// on a required field.
const isOptionalFieldError = instancePath.startsWith("/full_transcript");
return {
field: field || "<root>",
value: truncateValue(err.data),
rule: `${err.keyword}: ${err.message ?? ""}`.trim(),
severity: "error",
severity: isOptionalFieldError ? "warning" : "error",
};
});
// `valid` is false on ANY ajv violation (preserves M2c writer
// semantics: any malformation should block atomic rename). The M3
// validate command consults `errors[].severity` to decide exit code
// per M1 contract § 8 (required-field-class errors → exit nonzero;
// optional-field warnings → exit 0 with warning text).
return { valid: false, errors };
}
+312
View File
@@ -1685,3 +1685,315 @@ describe("M2c-Daemon final regression — CLI-surface redaction + omitted round-
p.path === "/Users/wrandom/code/projects/openrig-hub")).toBe(true);
});
});
// ─────────────────────────────────────────────────────────────────────
// M3 — `rig restore-packet read` + `validate` actual implementations.
//
// Per dispatch qitem-20260502023319-d997e182 + IMPL § M3 line 154-191 +
// M1 contract § 1 (packet shape) + § 4 (redaction enum-only) + § 8
// (validate behavior + exit-code matrix). Predecessor: M2c-Daemon
// ACCEPTED at openrig c7b74fa.
// ─────────────────────────────────────────────────────────────────────
describe("M3 restore-packet read + validate", () => {
let tmpRoot: string;
beforeEach(async () => {
const fs = await import("node:fs");
const os = await import("node:os");
const path = await import("node:path");
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "restore-packet-m3-"));
});
afterEach(async () => {
const fs = await import("node:fs");
fs.rmSync(tmpRoot, { recursive: true, force: true });
});
async function buildValidPacket(targetDir: string): Promise<void> {
const { createProgram } = await import("../src/index.js");
const fs = await import("node:fs");
const path = await import("node:path");
const src = path.join(tmpRoot, `src-${path.basename(targetDir)}.jsonl`);
fs.writeFileSync(src, [
JSON.stringify({
type: "session_meta",
payload: { cwd: "/Users/wrandom/code/projects/openrig-hub", id: "m3-driver@m3-rig" },
}),
JSON.stringify({
type: "response_item",
payload: { type: "message", role: "user", content: "M3 fixture packet" },
}),
].join("\n"), "utf8");
const program = createProgram();
program.exitOverride();
await program.parseAsync([
"node", "rig", "restore-packet", "write",
"--source-jsonl", src,
"--target", targetDir,
"--target-rig", "openrig-velocity-claude",
"--target-runtime", "claude-code",
"--current-work-summary", "M3 fixture packet for read/validate tests.",
"--authority-boundaries", "M3 test fixture only.",
]);
}
async function captureCli(argv: string[]): Promise<{ exitCode: number | undefined; stdout: string[]; stderr: string[] }> {
const { createProgram } = await import("../src/index.js");
const stdout: string[] = [];
const stderr: string[] = [];
const origLog = console.log;
const origErr = console.error;
const origExit = process.exitCode;
console.log = (...args: unknown[]) => stdout.push(args.join(" "));
console.error = (...args: unknown[]) => stderr.push(args.join(" "));
process.exitCode = undefined;
try {
const program = createProgram();
program.exitOverride();
try {
await program.parseAsync(argv);
} catch (err) {
if (err instanceof Error && process.exitCode === undefined) {
process.exitCode = 2;
}
if (err instanceof Error) stderr.push(err.message);
}
} finally {
console.log = origLog;
console.error = origErr;
}
const exitCode = process.exitCode;
process.exitCode = origExit;
return { exitCode, stdout, stderr };
}
it("read: human output prints restore-instructions body + summary metadata + transcript digest", async () => {
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-read-human");
await buildValidPacket(target);
const { exitCode, stdout } = await captureCli([
"node", "rig", "restore-packet", "read", target,
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
const out = stdout.join("\n");
expect(out).toContain("# Restore Instructions");
expect(out).toContain("source_session_id");
expect(out).toContain("m3-driver@m3-rig");
expect(out).toContain("source_rig");
expect(out).toContain("m3-rig");
expect(out).toContain("target_rig");
expect(out).toContain("openrig-velocity-claude");
expect(out).toContain("generator_version");
expect(out).toContain("[email protected]");
expect(out).toMatch(/transcript\.md/);
});
it("read --json: stdout is round-trippable to restore-summary.json", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-read-json");
await buildValidPacket(target);
const { exitCode, stdout } = await captureCli([
"node", "rig", "restore-packet", "read", target, "--json",
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
const stdoutSummary = JSON.parse(stdout.join("\n"));
const onDisk = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
expect(stdoutSummary).toEqual(onDisk);
});
it("read: missing restore-summary.json fails with explicit error", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-read-missing-summary");
await buildValidPacket(target);
fs.rmSync(path.join(target, "restore-summary.json"));
const { exitCode, stderr } = await captureCli([
"node", "rig", "restore-packet", "read", target,
]);
expect(exitCode).not.toBe(0);
const errText = stderr.join("\n");
expect(errText).toMatch(/restore-summary\.json|missing|not found/i);
});
it("read: packet without transcript.md AND without full_transcript summary key prints absence line", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-read-no-transcript");
await buildValidPacket(target);
fs.rmSync(path.join(target, "transcript.md"));
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
delete summary.full_transcript;
fs.writeFileSync(path.join(target, "restore-summary.json"), JSON.stringify(summary, null, 2));
const { exitCode, stdout } = await captureCli([
"node", "rig", "restore-packet", "read", target,
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
expect(stdout.join("\n")).toMatch(/transcript\.md absent/i);
});
it("validate: ACCEPT well-formed packet WITH transcript.md + full_transcript key (parity respected)", async () => {
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-with-transcript");
await buildValidPacket(target);
const { exitCode, stdout } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
expect(stdout.join("\n")).toMatch(/valid|ok|pass/i);
});
it("validate: ACCEPT well-formed packet WITHOUT transcript.md AND WITHOUT full_transcript key (parity via absence)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-no-transcript");
await buildValidPacket(target);
fs.rmSync(path.join(target, "transcript.md"));
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
delete summary.full_transcript;
fs.writeFileSync(path.join(target, "restore-summary.json"), JSON.stringify(summary, null, 2));
const { exitCode } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
});
it("validate: REJECT packet with full_transcript key but transcript.md absent (parity violation)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-parity-1");
await buildValidPacket(target);
fs.rmSync(path.join(target, "transcript.md"));
const { exitCode, stdout, stderr } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode).not.toBe(0);
const out = stdout.concat(stderr).join("\n");
expect(out).toMatch(/parity|transcript\.md|full_transcript/i);
});
it("validate: REJECT packet with transcript.md present but full_transcript key omitted (parity violation)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-parity-2");
await buildValidPacket(target);
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
delete summary.full_transcript;
fs.writeFileSync(path.join(target, "restore-summary.json"), JSON.stringify(summary, null, 2));
const { exitCode, stdout, stderr } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode).not.toBe(0);
const out = stdout.concat(stderr).join("\n");
expect(out).toMatch(/parity|transcript\.md|full_transcript/i);
});
it("validate: REJECT packet with required field missing (per-field error reported)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-missing-field");
await buildValidPacket(target);
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
delete summary.source_session_id;
fs.writeFileSync(path.join(target, "restore-summary.json"), JSON.stringify(summary, null, 2));
const { exitCode, stdout, stderr } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode).not.toBe(0);
const out = stdout.concat(stderr).join("\n");
expect(out).toMatch(/source_session_id/);
});
it("validate: REJECT packet missing one of the 4 required files", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-missing-file");
await buildValidPacket(target);
fs.rmSync(path.join(target, "touched-files.md"));
const { exitCode, stdout, stderr } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode).not.toBe(0);
const out = stdout.concat(stderr).join("\n");
expect(out).toMatch(/touched-files\.md|required file|missing/i);
});
it("validate --json: machine-readable shape { valid: boolean, errors: [...] }", async () => {
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-json");
await buildValidPacket(target);
const { exitCode, stdout } = await captureCli([
"node", "rig", "restore-packet", "validate", target, "--json",
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
const report = JSON.parse(stdout.join("\n"));
expect(report).toHaveProperty("valid");
expect(report).toHaveProperty("errors");
expect(report.valid).toBe(true);
expect(Array.isArray(report.errors)).toBe(true);
});
it("validate: optional-field malformation (full_transcript wrong type) -> exit 0 with WARNING (per contract § 8)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-warning");
await buildValidPacket(target);
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
summary.full_transcript = { path: "transcript.md", line_count: "not-an-integer" };
fs.writeFileSync(path.join(target, "restore-summary.json"), JSON.stringify(summary, null, 2));
const { exitCode, stdout, stderr } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
const out = stdout.concat(stderr).join("\n");
expect(out).toMatch(/warning|warn/i);
expect(out).toMatch(/full_transcript|line_count/i);
});
it("validate: redaction_policy_id ENUM-ONLY check (deliberate v0 behavior; matches Velocity precedent per contract § 4 + § 8)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-enum-only");
await buildValidPacket(target);
fs.writeFileSync(
path.join(target, "transcript.md"),
"# Transcript\n\ntoken FAKETESTSTRING-NOT-A-REAL-CREDENTIAL-x123 was visible in logs\n",
);
const { exitCode } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode === undefined || exitCode === 0).toBe(true);
});
it("validate: redaction_policy_id WRONG enum value -> REJECT", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const target = path.join(tmpRoot, "packet-validate-bad-enum");
await buildValidPacket(target);
const summary = JSON.parse(fs.readFileSync(path.join(target, "restore-summary.json"), "utf-8"));
summary.redaction_policy_id = "not-a-real-policy";
fs.writeFileSync(path.join(target, "restore-summary.json"), JSON.stringify(summary, null, 2));
const { exitCode, stdout, stderr } = await captureCli([
"node", "rig", "restore-packet", "validate", target,
]);
expect(exitCode).not.toBe(0);
const out = stdout.concat(stderr).join("\n");
expect(out).toMatch(/redaction_policy_id|enum/i);
});
});