mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
feat(cli): Restore-Packet M3 — rig restore-packet read + validate actual implementations
Closes M3 dispatch qitem-20260502023319-d997e182. M2 marshal-acceptance gate PASSED at predecessorc7b74fa(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: openrigc7b74fa(M2 marshal-gate PASS). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
c7b74fa08f
commit
a5132dad87
3 files changed
+495
-13
No files matched your search
@@ -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 };
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user