/** * T8 — legacy v1 → v2 envelope compatibility for replay reads. * * Verifies that pre-T8 audit rows replay through `legacyV1ToV2` to a v2 * envelope that produces the same Decision under an unchanged policy. * The `intentHash` will NOT match the stored v1 row — they were computed * under different recipes — but the Decision will, which is what the * replay harness actually probes. */ import { describe, expect, it } from "vitest"; import { legacyV1ToV2, recordedAuthoritySnapshotFromRow, } from "../src/legacy-v1-compat.js"; import type { IntentAuditRow } from "../src/postgres-sink.js"; function v1Row(overrides?: Partial): IntentAuditRow { return { intent_hash: "v1hash".repeat(11) + "ab", session_id: "s-1", kind: "order.confirm", principal: "llm", taint: "TRUSTED", decision_kind: "EXECUTE", refusal_kind: null, refusal_code: null, decision_basis: ["business:rule_satisfied"], resource_version: "v-7", envelope_jsonb: JSON.stringify({ version: 1, kind: "order.confirm", payload: { orderId: "ord_1" }, actor: { principal: "llm", sessionId: "s-1" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", intentHash: "v1hash".repeat(11) + "ab", }), decision_jsonb: JSON.stringify({ kind: "EXECUTE", basis: [{ category: "business", code: "rule_satisfied" }], }), recorded_at: "2026-04-01T10:00:01.000Z", duration_ms: 5, partition_month: "2026-04", record_version: 1, plan_jsonb: null, nonce: null, ...overrides, }; } describe("legacyV1ToV2", () => { it("synthesizes a v2 envelope from a v1 row", () => { const row = v1Row(); const env = legacyV1ToV2(row); expect(env.version).toBe(2); expect(env.kind).toBe("order.confirm"); expect(env.taint).toBe("TRUSTED"); expect(env.actor.sessionId).toBe("s-1"); }); it("uses the row.nonce column when present (v2 row read via the legacy helper)", () => { const row = v1Row({ nonce: "explicit-nonce-123", record_version: 2 }); const env = legacyV1ToV2(row); expect(env.nonce).toBe("explicit-nonce-123"); }); it("falls back to the envelope's stored nonce when row.nonce is null", () => { const row = v1Row({ nonce: null, envelope_jsonb: JSON.stringify({ version: 2, kind: "x.do", payload: {}, actor: { principal: "llm", sessionId: "s" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", nonce: "envelope-stored-nonce", intentHash: "h", }), }); const env = legacyV1ToV2(row); expect(env.nonce).toBe("envelope-stored-nonce"); }); it("synthesizes nonce from createdAt for true v1 rows (no nonce anywhere)", () => { const row = v1Row(); // record_version = 1, nonce column null, no envelope.nonce const env = legacyV1ToV2(row); expect(env.nonce).toBe("2026-04-01T10:00:00.000Z"); }); it("preserves createdAt as descriptive metadata even though it's no longer hashed", () => { const env = legacyV1ToV2(v1Row()); expect(env.createdAt).toBe("2026-04-01T10:00:00.000Z"); }); it("computes a v2 intentHash that does NOT match the stored v1 hash (different recipe)", () => { const row = v1Row(); const env = legacyV1ToV2(row); // The v1 hash was computed over (version, kind, payload, createdAt, // actor, taint). The v2 hash is over (version, kind, payload, nonce, // actor, taint). Different recipes → different hashes. expect(env.intentHash).not.toBe(row.intent_hash); // But the v2 hash is deterministic and reproducible. const env2 = legacyV1ToV2(row); expect(env2.intentHash).toBe(env.intentHash); }); // 031 — v3 resource-refs threading. it("reconstructs a no-resource-refs row with NO resourceRefs key (drop-safe)", () => { const env = legacyV1ToV2(v1Row()); expect("resourceRefs" in env).toBe(false); }); it("threads stored resourceRefs through faithfully for a v3 row", () => { const row = v1Row({ record_version: 4, nonce: "real-nonce", envelope_jsonb: JSON.stringify({ version: 2, kind: "pix.charge.refund", payload: { chargeId: "chg_1" }, actor: { principal: "llm", sessionId: "s-1" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", nonce: "real-nonce", origin: "LLM", resourceRefs: { account: "acct_7", owner: "user_42" }, intentHash: "h", }), }); const env = legacyV1ToV2(row); expect(env.resourceRefs).toEqual({ account: "acct_7", owner: "user_42" }); // The refs are bound into the recomputed hash: dropping them changes it. const noRefsRow = v1Row({ record_version: 4, nonce: "real-nonce", envelope_jsonb: JSON.stringify({ version: 2, kind: "pix.charge.refund", payload: { chargeId: "chg_1" }, actor: { principal: "llm", sessionId: "s-1" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", nonce: "real-nonce", origin: "LLM", intentHash: "h", }), }); expect(legacyV1ToV2(noRefsRow).intentHash).not.toBe(env.intentHash); }); }); describe("legacyV1ToV2 — v2+ nonce integrity guard (DataReviewer-010)", () => { // A v2+ envelope (envelope_jsonb) WITHOUT a stored nonce, used to force // the "no usable nonce anywhere" path. createdAt is present but must NOT // be substituted for v2+ rows. const v2EnvelopeNoNonce = JSON.stringify({ version: 2, kind: "order.confirm", payload: { orderId: "ord_1" }, actor: { principal: "llm", sessionId: "s-1" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", intentHash: "h", }); it("throws for a v2 row whose nonce column AND stored nonce are both null", () => { const row = v1Row({ record_version: 2, nonce: null, envelope_jsonb: v2EnvelopeNoNonce, }); expect(() => legacyV1ToV2(row)).toThrow(/missing a nonce/); }); it("throws for a v2 row whose nonce column is an empty string and stored nonce is absent", () => { const row = v1Row({ record_version: 2, nonce: "", envelope_jsonb: v2EnvelopeNoNonce, }); expect(() => legacyV1ToV2(row)).toThrow(/v2\+ record MUST carry the nonce/); }); it("throws for v3 and v4 rows with no usable nonce (guard covers all v2+ versions)", () => { for (const v of [3, 4] as const) { const row = v1Row({ record_version: v, nonce: null, envelope_jsonb: v2EnvelopeNoNonce, }); expect(() => legacyV1ToV2(row)).toThrow(/missing a nonce/); } }); it("does NOT throw for a v2 row that carries a real nonce column (round-trips)", () => { const row = v1Row({ record_version: 2, nonce: "real-nonce-abc" }); const env = legacyV1ToV2(row); expect(env.nonce).toBe("real-nonce-abc"); }); it("does NOT throw for a v2 row when the stored envelope nonce is present (column null)", () => { const row = v1Row({ record_version: 2, nonce: null, envelope_jsonb: JSON.stringify({ version: 2, kind: "x.do", payload: {}, actor: { principal: "llm", sessionId: "s" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", nonce: "envelope-stored-nonce", intentHash: "h", }), }); const env = legacyV1ToV2(row); expect(env.nonce).toBe("envelope-stored-nonce"); }); it("a v1 row with no nonce anywhere still uses the legacy createdAt fallback (no throw)", () => { // record_version defaults to 1 in v1Row(); the guard is keyed on >= 2, // so genuine pre-T8 v1 rows keep synthesizing nonce from createdAt. const env = legacyV1ToV2(v1Row()); expect(env.nonce).toBe("2026-04-01T10:00:00.000Z"); }); }); // 033 — degrade-safe read of the recorded authority snapshot from a stored row. describe("recordedAuthoritySnapshotFromRow — 033 degrade-safe legacy read", () => { it("returns undefined for a legacy row that carries no recorded snapshot", () => { // The recorded snapshot is record-level and 033-new; older rows lack it. expect(recordedAuthoritySnapshotFromRow(v1Row())).toBeUndefined(); }); it("returns undefined for unreadable/garbage envelope JSON (never throws)", () => { const row = v1Row({ envelope_jsonb: "{not-json" }); expect(() => recordedAuthoritySnapshotFromRow(row)).not.toThrow(); expect(recordedAuthoritySnapshotFromRow(row)).toBeUndefined(); }); it("returns a structurally-valid recorded snapshot when one is present", () => { const snapshot = { graph: { edges: [ { principal: "user_42", relationship: "owns", resource: "acct_7", permits: { actions: ["pix.charge.refund"] } }, ], }, snapshotHash: "a".repeat(64), }; const row = v1Row({ record_version: 5, nonce: "real-nonce", envelope_jsonb: JSON.stringify({ version: 2, kind: "pix.charge.refund", payload: {}, actor: { principal: "llm", sessionId: "s-1" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", nonce: "real-nonce", intentHash: "h", // A future writer may carry the recorded snapshot in the persisted JSON; // the degrade-safe reader extracts it verbatim. authoritySnapshot: snapshot, }), }); expect(recordedAuthoritySnapshotFromRow(row)).toEqual(snapshot); }); it("degrades to undefined on a malformed snapshot blob (missing graph.edges / snapshotHash)", () => { const row = v1Row({ envelope_jsonb: JSON.stringify({ version: 1, kind: "x", payload: {}, actor: { principal: "llm", sessionId: "s" }, taint: "TRUSTED", createdAt: "2026-04-01T10:00:00.000Z", intentHash: "h", authoritySnapshot: { graph: { notEdges: [] } }, // wrong shape }), }); expect(recordedAuthoritySnapshotFromRow(row)).toBeUndefined(); }); });