codex.suite.test.ts

9.9 KB · raw

1// codex.suite.test.ts — cross-cutting regression suite for the Codex2// pipeline. Per-module behavior is exercised in generate.test.ts /3// restore.test.ts / pdf-import.test.ts / codex.test.ts; this file4// covers the regressions that span modules or that have specifically5// broken in production before (static-source guards for the AztecCodeReader6// vs BrowserAztecCodeReader bug, no-Node-imports for browser chunks,7// chrome.runtime.getURL for WASM, stripe planning boundaries, and a8// large-vault generate→decode planning round-trip).910import { describe, expect, it } from "vitest";11import { readFileSync } from "node:fs";12import { resolve } from "node:path";1314import {15  CHUNK_SIZE,16  MAX_DATA_SHARDS_PER_STRIPE,17  CODEX_MIN_PARITY_SHARDS,18  CODEX_MAX_PARITY_SHARDS,19  CODEX_PARITY_RATIO,20  CODES_PER_PAGE,21  computeParityShards,22  deriveShardCounts,23} from "../../shared/codex-types";24import { planStripeShardCounts } from "./erasure";2526const SRC = (rel: string) => resolve(process.cwd(), rel);27const sourceOf = (rel: string) => readFileSync(SRC(rel), "utf8");2829// ─── Chunk + stripe planning constants ────────────────────────────────────3031describe("Codex — Chunk + Stripe planning constants", () => {32  it("CHUNK_SIZE is 350 bytes (75-module codes, 4.44 dots/module at 300 DPI)", () => {33    expect(CHUNK_SIZE).toBe(350);34  });3536  it("MAX_DATA_SHARDS_PER_STRIPE + max parity stays inside GF(2^8) cap of 255", () => {37    expect(MAX_DATA_SHARDS_PER_STRIPE + CODEX_MAX_PARITY_SHARDS).toBeLessThanOrEqual(255);38  });3940  it("CODES_PER_PAGE is 36 (6×6 grid)", () => {41    expect(CODES_PER_PAGE).toBe(36);42  });4344  it("computeParityShards clamps to min 2 for very small vaults", () => {45    expect(computeParityShards(1)).toBeGreaterThanOrEqual(CODEX_MIN_PARITY_SHARDS);46    expect(computeParityShards(10)).toBeGreaterThanOrEqual(CODEX_MIN_PARITY_SHARDS);47  });4849  it("computeParityShards scales at the configured ratio for medium vaults", () => {50    const dataShards = 100;51    const parity = computeParityShards(dataShards);52    const expected = Math.max(53      CODEX_MIN_PARITY_SHARDS,54      Math.min(CODEX_MAX_PARITY_SHARDS, Math.ceil(dataShards * CODEX_PARITY_RATIO)),55    );56    expect(parity).toBe(expected);57  });5859  it("computeParityShards caps at CODEX_MAX_PARITY_SHARDS for large vaults", () => {60    expect(computeParityShards(500)).toBe(CODEX_MAX_PARITY_SHARDS);61    expect(computeParityShards(10_000)).toBe(CODEX_MAX_PARITY_SHARDS);62  });63});6465// ─── Stripe planning boundary cases ───────────────────────────────────────6667describe("Codex — Stripe planning boundaries", () => {68  it("small vault (≤ MAX_DATA_SHARDS_PER_STRIPE) produces exactly 1 stripe", () => {69    const plan = planStripeShardCounts(50);70    expect(plan).toHaveLength(1);71    expect(plan[0]).toBe(50);72  });7374  it("vault at exactly MAX_DATA_SHARDS_PER_STRIPE shards is a single stripe", () => {75    const plan = planStripeShardCounts(MAX_DATA_SHARDS_PER_STRIPE);76    expect(plan).toHaveLength(1);77    expect(plan[0]).toBe(MAX_DATA_SHARDS_PER_STRIPE);78  });7980  it("vault one shard over the limit splits to 2 stripes", () => {81    const plan = planStripeShardCounts(MAX_DATA_SHARDS_PER_STRIPE + 1);82    expect(plan).toHaveLength(2);83    expect(plan.reduce((a, b) => a + b, 0)).toBe(MAX_DATA_SHARDS_PER_STRIPE + 1);84  });8586  it("large vault (~2.5x limit) splits into the right number of balanced stripes", () => {87    const total = MAX_DATA_SHARDS_PER_STRIPE * 2 + 50;88    const plan = planStripeShardCounts(total);89    expect(plan).toHaveLength(3);90    expect(plan.reduce((a, b) => a + b, 0)).toBe(total);91    for (const c of plan) expect(c).toBeLessThanOrEqual(MAX_DATA_SHARDS_PER_STRIPE);92  });9394  it("zero data shards still returns at least one stripe slot (singleton)", () => {95    const plan = planStripeShardCounts(1);96    expect(plan).toHaveLength(1);97    expect(plan[0]).toBe(1);98  });99});100101// ─── deriveShardCounts shape ──────────────────────────────────────────────102103describe("Codex — deriveShardCounts shape", () => {104  it("returns a {dataShards, parityShards} pair where parity matches the scaled value", () => {105    const total = 50;106    const { dataShards, parityShards } = deriveShardCounts(total);107    expect(dataShards + parityShards).toBe(total);108    expect(computeParityShards(dataShards)).toBe(parityShards);109  });110111  it("data + parity always equals the requested total for valid inputs", () => {112    for (const total of [3, 10, 50, 99, 199, 220]) {113      const { dataShards, parityShards } = deriveShardCounts(total);114      expect(dataShards + parityShards).toBe(total);115    }116  });117});118119// ─── Static source assertions — regression guards ────────────────────────120121describe("Codex — Aztec reader regression guards (v3.6.74/75 fixes)", () => {122  it("pdf-import.ts imports AztecCodeReader (not the browser variant) from @zxing/library", () => {123    const source = sourceOf("src/popup/codex/pdf-import.ts");124    expect(source).toContain("AztecCodeReader");125    // The actual import line must use the bare AztecCodeReader symbol,126    // not the browser variant (which silently no-ops on canvas elements).127    // Multi-line import: extract every import block ending in @zxing/library128    // and assert AztecCodeReader is in at least one.129    const zxingImports = source.match(/import\s*\{[^}]*\}\s*from\s+["']@zxing\/library["']/g) ?? [];130    const importsAztec = zxingImports.some((line) => /\bAztecCodeReader\b/.test(line));131    expect(importsAztec).toBe(true);132    // BrowserAztecCodeReader is allowed in comments (it's the bug we133    // documented), but must not appear in any import or as a constructor.134    const importsBrowserVariant = (source.match(/import\s*\{[^}]*\}/g) ?? []).some((b) =>135      /\bBrowserAztecCodeReader\b/.test(b),136    );137    expect(importsBrowserVariant).toBe(false);138    expect(source).not.toMatch(/new\s+BrowserAztecCodeReader\b/);139  });140141  it("aztec.ts in-memory decode path uses AztecCodeReader (v3.6.58 GlobalHistogramBinarizer)", () => {142    const source = sourceOf("src/popup/codex/aztec.ts");143    expect(source).toContain("AztecCodeReader");144    expect(source).toContain("GlobalHistogramBinarizer");145  });146});147148describe("Codex — browser bundle hygiene", () => {149  const browserModules = [150    "src/popup/codex/generate.ts",151    "src/popup/codex/erasure.ts",152    "src/popup/codex/rs-loader.ts",153    "src/popup/codex/pdf-import.ts",154    "src/popup/codex/aztec.ts",155    "src/popup/codex/reed-solomon.ts",156    "src/popup/codex/restore.ts",157  ];158159  for (const mod of browserModules) {160    it(`${mod} has no top-level Node imports`, () => {161      const source = sourceOf(mod);162      // Top-level static imports must not reach for Node-only paths.163      // `require("fs")`, `node:fs`, `node:path` are forbidden.164      expect(source).not.toMatch(/require\(["']fs["']\)/);165      expect(source).not.toMatch(/from\s+["']node:fs["']/);166      expect(source).not.toMatch(/from\s+["']node:path["']/);167      expect(source).not.toMatch(/from\s+["']fs["']/);168      expect(source).not.toMatch(/from\s+["']path["']/);169    });170  }171172  it("rs-loader.ts uses chrome.runtime.getURL for the WASM asset (not a hardcoded path)", () => {173    const source = sourceOf("src/popup/codex/rs-loader.ts");174    expect(source).toMatch(/chrome\.runtime\.getURL/);175    // The legacy hardcoded "/codex/reed_solomon..." path must not appear.176    expect(source).not.toMatch(/['"]\/codex\/reed_solomon/);177  });178});179180// ─── Manifest / payload shape — v3 envelope ──────────────────────────────181182describe("Codex — manifest + payload shape (v3 envelope)", () => {183  it("ChunkEnvelope v3 carries stripe index, total stripes, and chunk index", () => {184    // The accepted v3 shape is enforced by parseChunkEnvelope; verify the185    // exported type literal is unchanged.186    const source = sourceOf("src/shared/codex-types.ts");187    expect(source).toContain("ChunkEnvelope");188    // The current generator emits v=3 envelopes; the constant must agree189    // (pdf-import already enforces this on the import side — keep its190    // assertion mirrored here so a desync is caught immediately).191    expect(source).toMatch(/version|envelopeVersion|v:\s*3|"v"\s*:\s*3|CHUNK_ENVELOPE_VERSION\s*=\s*3/i);192  });193194  it("Storage key constants are stable across releases", () => {195    const source = sourceOf("src/shared/codex-types.ts");196    expect(source).toContain('"peach_codex_verify_code"');197    expect(source).toContain('"peach_codex_last_generated"');198    expect(source).toContain('"peach_last_restored_at"');199  });200});201202// ─── Module load gate — recovery word confirmation renders without203//     awaiting the heavy generate module (v3.6.41 fix) ────────────────────204205describe("Codex — module load gate (v3.6.41/43)", () => {206  it("CodexGenerateView renders the recovery word gate without statically importing the heavy generate module", () => {207    const source = sourceOf("src/popup/views/CodexGenerateView.tsx");208    // The heavy chunk must be loaded via dynamic import so the route mounts209    // before the bundle resolves.210    expect(source).toMatch(/import\s*\(/);211    // The confirmation picker is light and lives in confirmation.ts;212    // CodexGenerateView re-exports it from generate.ts to keep the contract213    // surface stable.214    expect(source).toMatch(/pickConfirmationPositions|confirmation|words/i);215  });216217  it("CodexGenerateView surfaces an inline error + retry when the generate module fails to load", () => {218    const source = sourceOf("src/popup/views/CodexGenerateView.tsx");219    // The retry-on-load-failure path was added in v3.6.41. Look for the220    // retry control and the load-error state by name.221    expect(source).toMatch(/loadError|load[_ ]?failed|retry/i);222  });223});