0f3e59b5732a
test: Codex regression suite — chunk/stripe constants, planning boundaries, deriveShardCounts, AztecCodeReader regression guards, browser-bundle hygiene, module load gate (v3.6.84)
- time
- 2026-05-21 15:59 UTC · 5 months ago
- machine
- koh2
- parent
- f86ad225e14e
↓ peach-browser-0f3e59b5.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/codex/codex.suite.test.ts+223 −0manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22 "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33 "manifest_version": 3,44 "name": "Peach",5- "version": "3.6.83",5+ "version": "3.6.84",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "3.6.83",4+ "version": "3.6.84",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.83",3+ "version": "3.6.84",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/popup/codex/codex.suite.test.ts+223 −0
@@ -0,0 +1,223 @@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).9+10+import { describe, expect, it } from "vitest";11+import { readFileSync } from "node:fs";12+import { resolve } from "node:path";13+14+import {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";24+import { planStripeShardCounts } from "./erasure";25+26+const SRC = (rel: string) => resolve(process.cwd(), rel);27+const sourceOf = (rel: string) => readFileSync(SRC(rel), "utf8");28+29+// ─── Chunk + stripe planning constants ────────────────────────────────────30+31+describe("Codex — Chunk + Stripe planning constants", () => {32+ it("CHUNK_SIZE is 800 bytes (matches Aztec capacity at ECC=25%)", () => {33+ expect(CHUNK_SIZE).toBe(800);34+ });35+36+ 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+ });39+40+ it("CODES_PER_PAGE is 36 (6×6 grid)", () => {41+ expect(CODES_PER_PAGE).toBe(36);42+ });43+44+ 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+ });48+49+ 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+ });58+59+ 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+});64+65+// ─── Stripe planning boundary cases ───────────────────────────────────────66+67+describe("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+ });73+74+ 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+ });79+80+ 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+ });85+86+ 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+ });93+94+ 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+});100+101+// ─── deriveShardCounts shape ──────────────────────────────────────────────102+103+describe("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+ });110+111+ it("data + parity always equals the requested total for valid inputs", () => {112+ for (const total of [3, 10, 50, 100, 199, 220]) {113+ const { dataShards, parityShards } = deriveShardCounts(total);114+ expect(dataShards + parityShards).toBe(total);115+ }116+ });117+});118+119+// ─── Static source assertions — regression guards ────────────────────────120+121+describe("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+ });140+141+ 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+});147+148+describe("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+ ];158+159+ 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+ }171+172+ 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+});179+180+// ─── Manifest / payload shape — v3 envelope ──────────────────────────────181+182+describe("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+ });193+194+ 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+});201+202+// ─── Module load gate — recovery word confirmation renders without203+// awaiting the heavy generate module (v3.6.41 fix) ────────────────────204+205+describe("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+ });216+217+ 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+});