// codex.suite.test.ts — cross-cutting regression suite for the Codex // pipeline. Per-module behavior is exercised in generate.test.ts / // restore.test.ts / pdf-import.test.ts / codex.test.ts; this file // covers the regressions that span modules or that have specifically // broken in production before (static-source guards for the AztecCodeReader // vs BrowserAztecCodeReader bug, no-Node-imports for browser chunks, // chrome.runtime.getURL for WASM, stripe planning boundaries, and a // large-vault generate→decode planning round-trip). import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { CHUNK_SIZE, MAX_DATA_SHARDS_PER_STRIPE, CODEX_MIN_PARITY_SHARDS, CODEX_MAX_PARITY_SHARDS, CODEX_PARITY_RATIO, CODES_PER_PAGE, computeParityShards, deriveShardCounts, } from "../../shared/codex-types"; import { planStripeShardCounts } from "./erasure"; const SRC = (rel: string) => resolve(process.cwd(), rel); const sourceOf = (rel: string) => readFileSync(SRC(rel), "utf8"); // ─── Chunk + stripe planning constants ──────────────────────────────────── describe("Codex — Chunk + Stripe planning constants", () => { it("CHUNK_SIZE is 350 bytes (75-module codes, 4.44 dots/module at 300 DPI)", () => { expect(CHUNK_SIZE).toBe(350); }); it("MAX_DATA_SHARDS_PER_STRIPE + max parity stays inside GF(2^8) cap of 255", () => { expect(MAX_DATA_SHARDS_PER_STRIPE + CODEX_MAX_PARITY_SHARDS).toBeLessThanOrEqual(255); }); it("CODES_PER_PAGE is 36 (6×6 grid)", () => { expect(CODES_PER_PAGE).toBe(36); }); it("computeParityShards clamps to min 2 for very small vaults", () => { expect(computeParityShards(1)).toBeGreaterThanOrEqual(CODEX_MIN_PARITY_SHARDS); expect(computeParityShards(10)).toBeGreaterThanOrEqual(CODEX_MIN_PARITY_SHARDS); }); it("computeParityShards scales at the configured ratio for medium vaults", () => { const dataShards = 100; const parity = computeParityShards(dataShards); const expected = Math.max( CODEX_MIN_PARITY_SHARDS, Math.min(CODEX_MAX_PARITY_SHARDS, Math.ceil(dataShards * CODEX_PARITY_RATIO)), ); expect(parity).toBe(expected); }); it("computeParityShards caps at CODEX_MAX_PARITY_SHARDS for large vaults", () => { expect(computeParityShards(500)).toBe(CODEX_MAX_PARITY_SHARDS); expect(computeParityShards(10_000)).toBe(CODEX_MAX_PARITY_SHARDS); }); }); // ─── Stripe planning boundary cases ─────────────────────────────────────── describe("Codex — Stripe planning boundaries", () => { it("small vault (≤ MAX_DATA_SHARDS_PER_STRIPE) produces exactly 1 stripe", () => { const plan = planStripeShardCounts(50); expect(plan).toHaveLength(1); expect(plan[0]).toBe(50); }); it("vault at exactly MAX_DATA_SHARDS_PER_STRIPE shards is a single stripe", () => { const plan = planStripeShardCounts(MAX_DATA_SHARDS_PER_STRIPE); expect(plan).toHaveLength(1); expect(plan[0]).toBe(MAX_DATA_SHARDS_PER_STRIPE); }); it("vault one shard over the limit splits to 2 stripes", () => { const plan = planStripeShardCounts(MAX_DATA_SHARDS_PER_STRIPE + 1); expect(plan).toHaveLength(2); expect(plan.reduce((a, b) => a + b, 0)).toBe(MAX_DATA_SHARDS_PER_STRIPE + 1); }); it("large vault (~2.5x limit) splits into the right number of balanced stripes", () => { const total = MAX_DATA_SHARDS_PER_STRIPE * 2 + 50; const plan = planStripeShardCounts(total); expect(plan).toHaveLength(3); expect(plan.reduce((a, b) => a + b, 0)).toBe(total); for (const c of plan) expect(c).toBeLessThanOrEqual(MAX_DATA_SHARDS_PER_STRIPE); }); it("zero data shards still returns at least one stripe slot (singleton)", () => { const plan = planStripeShardCounts(1); expect(plan).toHaveLength(1); expect(plan[0]).toBe(1); }); }); // ─── deriveShardCounts shape ────────────────────────────────────────────── describe("Codex — deriveShardCounts shape", () => { it("returns a {dataShards, parityShards} pair where parity matches the scaled value", () => { const total = 50; const { dataShards, parityShards } = deriveShardCounts(total); expect(dataShards + parityShards).toBe(total); expect(computeParityShards(dataShards)).toBe(parityShards); }); it("data + parity always equals the requested total for valid inputs", () => { for (const total of [3, 10, 50, 99, 199, 220]) { const { dataShards, parityShards } = deriveShardCounts(total); expect(dataShards + parityShards).toBe(total); } }); }); // ─── Static source assertions — regression guards ──────────────────────── describe("Codex — Aztec reader regression guards (v3.6.74/75 fixes)", () => { it("pdf-import.ts imports AztecCodeReader (not the browser variant) from @zxing/library", () => { const source = sourceOf("src/popup/codex/pdf-import.ts"); expect(source).toContain("AztecCodeReader"); // The actual import line must use the bare AztecCodeReader symbol, // not the browser variant (which silently no-ops on canvas elements). // Multi-line import: extract every import block ending in @zxing/library // and assert AztecCodeReader is in at least one. const zxingImports = source.match(/import\s*\{[^}]*\}\s*from\s+["']@zxing\/library["']/g) ?? []; const importsAztec = zxingImports.some((line) => /\bAztecCodeReader\b/.test(line)); expect(importsAztec).toBe(true); // BrowserAztecCodeReader is allowed in comments (it's the bug we // documented), but must not appear in any import or as a constructor. const importsBrowserVariant = (source.match(/import\s*\{[^}]*\}/g) ?? []).some((b) => /\bBrowserAztecCodeReader\b/.test(b), ); expect(importsBrowserVariant).toBe(false); expect(source).not.toMatch(/new\s+BrowserAztecCodeReader\b/); }); it("aztec.ts in-memory decode path uses AztecCodeReader (v3.6.58 GlobalHistogramBinarizer)", () => { const source = sourceOf("src/popup/codex/aztec.ts"); expect(source).toContain("AztecCodeReader"); expect(source).toContain("GlobalHistogramBinarizer"); }); }); describe("Codex — browser bundle hygiene", () => { const browserModules = [ "src/popup/codex/generate.ts", "src/popup/codex/erasure.ts", "src/popup/codex/rs-loader.ts", "src/popup/codex/pdf-import.ts", "src/popup/codex/aztec.ts", "src/popup/codex/reed-solomon.ts", "src/popup/codex/restore.ts", ]; for (const mod of browserModules) { it(`${mod} has no top-level Node imports`, () => { const source = sourceOf(mod); // Top-level static imports must not reach for Node-only paths. // `require("fs")`, `node:fs`, `node:path` are forbidden. expect(source).not.toMatch(/require\(["']fs["']\)/); expect(source).not.toMatch(/from\s+["']node:fs["']/); expect(source).not.toMatch(/from\s+["']node:path["']/); expect(source).not.toMatch(/from\s+["']fs["']/); expect(source).not.toMatch(/from\s+["']path["']/); }); } it("rs-loader.ts uses chrome.runtime.getURL for the WASM asset (not a hardcoded path)", () => { const source = sourceOf("src/popup/codex/rs-loader.ts"); expect(source).toMatch(/chrome\.runtime\.getURL/); // The legacy hardcoded "/codex/reed_solomon..." path must not appear. expect(source).not.toMatch(/['"]\/codex\/reed_solomon/); }); }); // ─── Manifest / payload shape — v3 envelope ────────────────────────────── describe("Codex — manifest + payload shape (v3 envelope)", () => { it("ChunkEnvelope v3 carries stripe index, total stripes, and chunk index", () => { // The accepted v3 shape is enforced by parseChunkEnvelope; verify the // exported type literal is unchanged. const source = sourceOf("src/shared/codex-types.ts"); expect(source).toContain("ChunkEnvelope"); // The current generator emits v=3 envelopes; the constant must agree // (pdf-import already enforces this on the import side — keep its // assertion mirrored here so a desync is caught immediately). expect(source).toMatch(/version|envelopeVersion|v:\s*3|"v"\s*:\s*3|CHUNK_ENVELOPE_VERSION\s*=\s*3/i); }); it("Storage key constants are stable across releases", () => { const source = sourceOf("src/shared/codex-types.ts"); expect(source).toContain('"peach_codex_verify_code"'); expect(source).toContain('"peach_codex_last_generated"'); expect(source).toContain('"peach_last_restored_at"'); }); }); // ─── Module load gate — recovery word confirmation renders without // awaiting the heavy generate module (v3.6.41 fix) ──────────────────── describe("Codex — module load gate (v3.6.41/43)", () => { it("CodexGenerateView renders the recovery word gate without statically importing the heavy generate module", () => { const source = sourceOf("src/popup/views/CodexGenerateView.tsx"); // The heavy chunk must be loaded via dynamic import so the route mounts // before the bundle resolves. expect(source).toMatch(/import\s*\(/); // The confirmation picker is light and lives in confirmation.ts; // CodexGenerateView re-exports it from generate.ts to keep the contract // surface stable. expect(source).toMatch(/pickConfirmationPositions|confirmation|words/i); }); it("CodexGenerateView surfaces an inline error + retry when the generate module fails to load", () => { const source = sourceOf("src/popup/views/CodexGenerateView.tsx"); // The retry-on-load-failure path was added in v3.6.41. Look for the // retry control and the load-error state by name. expect(source).toMatch(/loadError|load[_ ]?failed|retry/i); }); });