7f2cc11320ef
make Codex PDF recovery hermetic with a 250-entry multi-type vault (v5.2.59)
- time
- 2026-09-15 00:45 UTC · 1 month ago
- machine
- koh2
- agent
- gpt-5
- session
- hermetic codex recovery
- parent
- cc664d67df06
↓ peach-browser-7f2cc113.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1tests/codex-pdf-restore.spec.ts+193 −49manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "5.2.58",4+ "version": "5.2.59",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "5.2.58",5+ "version": "5.2.59",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "5.2.58",3+ "version": "5.2.59",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",tests/codex-pdf-restore.spec.ts+193 −49
@@ -1,9 +1,9 @@11 /**22 * codex-pdf-restore.spec.ts — end-to-end Codex PDF import test.33 *4- * Runs the real importCodexPDF pipeline against the user's actual Codex PDF5- * inside a live Chrome+extension context. This is the only reliable way to6- * exercise the full chain (pdf.js → canvas render → cell crop → Aztec decode4+ * Builds a Codex PDF from a seeded encrypted vault, then runs the real5+ * importCodexPDF pipeline inside a fresh Chrome+extension context. This is the6+ * only reliable way to exercise the full chain (pdf.js → canvas render → cell crop → Aztec decode77 * → ChunkBuffer → reconstructAndVerify) because every step needs a real DOM.88 *99 * Run: npx playwright test tests/codex-pdf-restore.spec.ts@@ -22,31 +22,149 @@2222 * triggering handlePDFSelected() inside the extension.2323 */2424 25-import { chromium, test, expect } from "@playwright/test";25+import { test, expect } from "@playwright/test";2626 import type { BrowserContext, Page } from "@playwright/test";27-import { fileURLToPath } from "url";28-import { dirname, join } from "path";29-import os from "os";30-import fs from "fs/promises";31-import { existsSync } from "fs";27+import { readFile, readdir } from "node:fs/promises";28+import { join } from "node:path";29+import {30+ EXTENSION_PATH,31+ makeContext,32+ setupVault,33+ VAULT_PASSWORD,34+} from "./helpers/vault";3235 33-const __filename = fileURLToPath(import.meta.url);34-const __dirname = dirname(__filename);35-const EXTENSION_PATH = join(__dirname, "..", "dist-chrome");36+const SEEDED_ENTRY_COUNT = 250;37+const SEEDED_ENTRY_TITLE = "Seeded Codex recovery entry";3638 37-const CODEX_PDF_PATH = "/Users/june/Downloads/peach-codex-2026-08-20.pdf";38-const CURRENT_GEN_PDF_PATH = join(__dirname, "fixtures", "peach-codex-current-gen.pdf");39+async function findCodexGeneratorBundle(): Promise<string> {40+ const assetsPath = join(EXTENSION_PATH, "assets");41+ const viewBundle = (await readdir(assetsPath)).find((name) =>42+ name.startsWith("CodexGenerateView-"),43+ );44+ if (!viewBundle) throw new Error("built Codex generation view was not found");45+ const source = await readFile(join(assetsPath, viewBundle), "utf8");46+ const generatorBundle = source.match(/\.\/((?:generate)-[^"'`]+\.js)/)?.[1];47+ if (!generatorBundle) throw new Error("built Codex generator bundle was not found");48+ return generatorBundle;49+}3950 40-async function makeContext(): Promise<BrowserContext> {41- const userDataDir = join(os.tmpdir(), `peach-codex-test-${Date.now()}`);42- await fs.mkdir(userDataDir, { recursive: true });43- return chromium.launchPersistentContext(userDataDir, {44- headless: false,45- args: [46- `--load-extension=${EXTENSION_PATH}`,47- `--disable-extensions-except=${EXTENSION_PATH}`,48- ],49- });51+async function createSeededCodexFile(): Promise<{52+ name: string;53+ mimeType: string;54+ buffer: Buffer;55+}> {56+ const seedContext = await makeContext("peach-codex-seed");57+ try {58+ const { popup } = await setupVault(seedContext);59+ const seeded = await popup.evaluate(async ({ count, sentinelTitle }) => {60+ const entries = Array.from({ length: count }, (_, index) => {61+ const title = index === 0 ? sentinelTitle : `Seeded Codex entry ${index + 1}`;62+ switch (index % 4) {63+ case 1:64+ return {65+ type: "note",66+ title,67+ notes: `Recovery note ${index + 1}: ${(index * 2654435761).toString(36)}`,68+ };69+ case 2:70+ return {71+ type: "card",72+ title,73+ cardholderName: `Seeded Holder ${index + 1}`,74+ cardNumber: `41111111${String(index).padStart(8, "0")}`,75+ expMonth: String((index % 12) + 1).padStart(2, "0"),76+ expYear: String(2030 + (index % 8)),77+ cvc: String(100 + (index % 900)),78+ network: "visa",79+ last4: String(index).padStart(4, "0").slice(-4),80+ };81+ case 3:82+ return {83+ type: "passkey",84+ title,85+ passkey: {86+ rpId: `passkey-${index + 1}.codex-recovery.test`,87+ rpName: `Seeded Passkey ${index + 1}`,88+ credentialId: `credential-${index + 1}`,89+ publicKeyCose: `cose-${index + 1}`,90+ userHandle: `handle-${index + 1}`,91+ userName: `passkey-user-${index + 1}`,92+ userDisplayName: `Passkey User ${index + 1}`,93+ algorithm: -7,94+ signCount: index,95+ aaguid: "AAAAAAAAAAAAAAAAAAAAAA",96+ backupEligible: true,97+ backupState: false,98+ createdAt: 1_700_000_000_000 + index,99+ },100+ };101+ default:102+ return {103+ type: "login",104+ title,105+ url: `https://account-${index + 1}.codex-recovery.test`,106+ username: `seeded-user-${index + 1}`,107+ password: `seeded-password-${index + 1}-${(index * 2654435761).toString(36)}`,108+ };109+ }110+ });111+ return chrome.runtime.sendMessage({112+ type: "BATCH_ADD_ENTRIES",113+ payload: { entries },114+ });115+ }, { count: SEEDED_ENTRY_COUNT, sentinelTitle: SEEDED_ENTRY_TITLE });116+ if (!seeded.ok || seeded.data.length !== SEEDED_ENTRY_COUNT) {117+ throw new Error(`seeded vault contains ${seeded.data?.length ?? 0} entries`);118+ }119+ const snapshot = await popup.evaluate(async () => {120+ const stored = await chrome.storage.local.get([121+ "peach_vault_a",122+ "peach_vault_b",123+ "peach_last_modified_at",124+ ]);125+ return {126+ slotA: JSON.stringify(stored.peach_vault_a),127+ slotB: JSON.stringify(stored.peach_vault_b),128+ lastModified: String(stored.peach_last_modified_at),129+ };130+ });131+ const generatorBundle = await findCodexGeneratorBundle();132+ const pdfBytes = await popup.evaluate(133+ async ({ bundle, vault, count }) => {134+ const generatorModule = (await import(135+ chrome.runtime.getURL(`assets/${bundle}`)136+ )) as Record<string, unknown>;137+ const generatorApi = Object.values(generatorModule).find(138+ (value): value is {139+ generateCodex: (deps: object) => Promise<{ pdfBytes: Uint8Array }>;140+ } =>141+ typeof value === "object" &&142+ value !== null &&143+ "generateCodex" in value &&144+ typeof value.generateCodex === "function",145+ );146+ if (!generatorApi) throw new Error("Codex generator export was not found");147+ const result = await generatorApi.generateCodex({148+ getSlotA: async () => vault.slotA,149+ getSlotB: async () => vault.slotB,150+ getLastModified: async () => vault.lastModified,151+ getAllEntries: async () =>152+ Array.from({ length: count }, () => ({ deletedAt: null })),153+ getVerifyCode: async () => "PX-SEED-CDEX",154+ isVaultUnlocked: () => true,155+ });156+ return Array.from(result.pdfBytes);157+ },158+ { bundle: generatorBundle, vault: snapshot, count: SEEDED_ENTRY_COUNT },159+ );160+ return {161+ name: "seeded-vault-codex.pdf",162+ mimeType: "application/pdf",163+ buffer: Buffer.from(pdfBytes),164+ };165+ } finally {166+ await seedContext.close();167+ }50168 }51169 52170 async function getExtId(ctx: BrowserContext): Promise<string> {@@ -90,7 +208,15 @@90208 91209 test.describe("Codex PDF restore", () => {92210 let ctx: BrowserContext;211+ let seededCodexFile: Awaited<ReturnType<typeof createSeededCodexFile>>;93212 213+ test.describe.configure({ timeout: 180_000 });214+215+ test.beforeAll(async () => {216+ test.setTimeout(180_000);217+ seededCodexFile = await createSeededCodexFile();218+ });219+94220 test.beforeEach(async () => {95221 ctx = await makeContext();96222 });@@ -99,17 +225,11 @@99225 await ctx.close();100226 });101227 102- test("imports all chunks from the real Codex PDF", async () => {103- // The full import of a large Codex (21+ pages) including scale-detection104- // renders can take up to 3 minutes. Set an explicit test timeout.105- test.setTimeout(180_000);106-228+ test("restores a seeded vault from its generated Codex PDF", async () => {107229 const extId = await getExtId(ctx);108230 const page = await openCodexRestorePage(ctx, extId);109231 110- // CDP-based setInputFiles bypasses the OS dialog and dispatches the111- // change event directly, firing handlePDFSelected inside the extension.112- await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);232+ await page.locator('input[type="file"]').setInputFiles(seededCodexFile);113233 114234 // After handlePDFSelected runs importCodexPDF:115235 // success → phase="ready" → body contains "master password" + "X / Y codes"@@ -135,20 +255,38 @@135255 expect(bodyText).not.toContain("no Codex codes found");136256 expect(bodyText).not.toContain("failed to read");137257 138- const passphraseVisible = await page.locator("#codex-passphrase").isVisible().catch(() => false);139- if (passphraseVisible) {140- console.log("✓ Buffer is ready — passphrase prompt visible.");141- }142- expect(bodyText).toMatch(/master password|codes/);258+ await page.locator("#codex-passphrase").fill(VAULT_PASSWORD);259+ await page.getByRole("button", { name: "restore vault" }).click();260+ await expect(page.getByText("✓ vault restored")).toBeVisible({ timeout: 30_000 });261+262+ const restored = await page.evaluate(async () => {263+ const response = await chrome.runtime.sendMessage({ type: "GET_ENTRIES" });264+ const entries = response.data as { title: string; type?: string }[];265+ return {266+ titles: entries.map((entry) => entry.title),267+ typeCounts: Object.fromEntries(268+ ["login", "note", "card", "passkey"].map((type) => [269+ type,270+ entries.filter((entry) => entry.type === type).length,271+ ]),272+ ),273+ };274+ });275+ expect(restored.titles).toHaveLength(SEEDED_ENTRY_COUNT);276+ expect(restored.titles).toContain(SEEDED_ENTRY_TITLE);277+ expect(restored.typeCounts).toEqual({278+ login: 63,279+ note: 63,280+ card: 62,281+ passkey: 62,282+ });143283 });144284 145285 test("decodes at least one Aztec envelope from page 1", async () => {146- test.setTimeout(120_000);147-148286 const extId = await getExtId(ctx);149287 const page = await openCodexRestorePage(ctx, extId);150288 151- await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);289+ await page.locator('input[type="file"]').setInputFiles(seededCodexFile);152290 153291 await page.waitForFunction(154292 () => {@@ -170,16 +308,10 @@170308 expect(bodyText).not.toContain("failed to read");171309 });172310 173- test("imports the current-generation Codex PDF when the fixture is present", async () => {174- test.setTimeout(180_000);175- if (!existsSync(CURRENT_GEN_PDF_PATH)) {176- test.skip();177- return;178- }179-311+ test("imports the generated current-generation Codex PDF without missing stripes", async () => {180312 const extId = await getExtId(ctx);181313 const page = await openCodexRestorePage(ctx, extId, "codex=restore&codexMetrics=1");182- await page.locator('input[type="file"]').setInputFiles(CURRENT_GEN_PDF_PATH);314+ await page.locator('input[type="file"]').setInputFiles(seededCodexFile);183315 184316 await page.waitForFunction(185317 () => {@@ -193,8 +325,20 @@193325 { timeout: 150_000, polling: 1_000 },194326 );195327 196- const metrics = await page.evaluate(() => (window as any).__codexImportMetrics ?? null);328+ const metrics = await page.evaluate(329+ () =>330+ (331+ window as Window & {332+ __codexImportMetrics?: {333+ decodeRate: number;334+ shortStripeCount: number;335+ perStripeDecoded: number[];336+ };337+ }338+ ).__codexImportMetrics ?? null,339+ );197340 expect(metrics).not.toBeNull();341+ if (!metrics) throw new Error("Codex import metrics were not recorded");198342 expect(metrics.decodeRate).toBeGreaterThanOrEqual(0.98);199343 expect(metrics.shortStripeCount).toBe(0);200344 expect(metrics.perStripeDecoded.length).toBeGreaterThan(0);