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
4 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/codex/codex.suite.test.ts+223 −0
manifest.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": {

view file

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",

view file

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": {

view file

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+});

view file