12345678910import { 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");28293031describe("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});64656667describe("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});100101102103describe("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});118119120121describe("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 126 127 128 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 133 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 163 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 176 expect(source).not.toMatch(/['"]\/codex\/reed_solomon/);177 });178});179180181182describe("Codex — manifest + payload shape (v3 envelope)", () => {183 it("ChunkEnvelope v3 carries stripe index, total stripes, and chunk index", () => {184 185 186 const source = sourceOf("src/shared/codex-types.ts");187 expect(source).toContain("ChunkEnvelope");188 189 190 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});201202203204205describe("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 209 210 expect(source).toMatch(/import\s*\(/);211 212 213 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 220 221 expect(source).toMatch(/loadError|load[_ ]?failed|retry/i);222 });223});