12345678910111213141516171819202122232425import { test, expect } from "@playwright/test";26import type { BrowserContext, Page } from "@playwright/test";27import { fileURLToPath } from "url";28import { dirname, join } from "path";29import os from "os";30import fs from "fs/promises";31import { existsSync } from "fs";3233const __filename = fileURLToPath(import.meta.url);34const __dirname = dirname(__filename);35const EXTENSION_PATH = join(__dirname, "..", "dist");3637const CODEX_PDF_PATH = "/Users/june/Downloads/peach-codex-2026-08-20.pdf";38const CURRENT_GEN_PDF_PATH = join(__dirname, "fixtures", "peach-codex-current-gen.pdf");3940async function makeContext(): Promise<BrowserContext> {41 const { chromium } = await import("playwright");42 const userDataDir = join(os.tmpdir(), `peach-codex-test-${Date.now()}`);43 await fs.mkdir(userDataDir, { recursive: true });44 return chromium.launchPersistentContext(userDataDir, {45 headless: false,46 args: [47 `--load-extension=${EXTENSION_PATH}`,48 `--disable-extensions-except=${EXTENSION_PATH}`,49 ],50 });51}5253async function getExtId(ctx: BrowserContext): Promise<string> {54 const maxWait = 15_000;55 const start = Date.now();56 let sw = ctx.serviceWorkers()[0];57 while (!sw && Date.now() - start < maxWait) {58 await new Promise((r) => setTimeout(r, 500));59 sw = ctx.serviceWorkers()[0];60 }61 if (!sw) throw new Error(`No service worker after ${maxWait}ms`);62 return new URL(sw.url()).host;63}6465async function openCodexRestorePage(66 ctx: BrowserContext,67 extId: string,68 search = "codex=restore",69): Promise<Page> {70 const page = await ctx.newPage();7172 73 page.on("console", (msg) => {74 const text = msg.text();75 if (msg.type() === "error" || text.startsWith("[codex]")) {76 console.log(` [browser:${msg.type()}] ${text}`);77 }78 });7980 81 82 83 await page.goto(`chrome-extension://${extId}/src/popup/index.html?${search}`);8485 86 87 await page.waitForSelector('input[type="file"]', { state: "attached", timeout: 10_000 });8889 return page;90}9192test.describe("Codex PDF restore", () => {93 let ctx: BrowserContext;9495 test.beforeEach(async () => {96 ctx = await makeContext();97 });9899 test.afterEach(async () => {100 await ctx.close();101 });102103 test("imports all chunks from the real Codex PDF", async () => {104 105 106 test.setTimeout(180_000);107108 const extId = await getExtId(ctx);109 const page = await openCodexRestorePage(ctx, extId);110111 112 113 await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);114115 116 117 118 119 120 await page.waitForFunction(121 () => {122 const body = document.body.textContent ?? "";123 return (124 body.includes("codes") ||125 body.includes("master password") ||126 body.includes("no Codex codes found") ||127 body.includes("failed to read")128 );129 },130 { timeout: 150_000, polling: 1_000 },131 );132133 const bodyText = await page.evaluate(() => document.body.textContent ?? "");134 console.log("Body snippet:", bodyText.replace(/\s+/g, " ").slice(0, 400));135136 expect(bodyText).not.toContain("no Codex codes found");137 expect(bodyText).not.toContain("failed to read");138139 const passphraseVisible = await page.locator("#codex-passphrase").isVisible().catch(() => false);140 if (passphraseVisible) {141 console.log("✓ Buffer is ready — passphrase prompt visible.");142 }143 expect(bodyText).toMatch(/master password|codes/);144 });145146 test("decodes at least one Aztec envelope from page 1", async () => {147 test.setTimeout(120_000);148149 const extId = await getExtId(ctx);150 const page = await openCodexRestorePage(ctx, extId);151152 await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);153154 await page.waitForFunction(155 () => {156 const body = document.body.textContent ?? "";157 return (158 body.includes("codes") ||159 body.includes("master password") ||160 body.includes("no Codex codes found") ||161 body.includes("failed to read")162 );163 },164 { timeout: 90_000, polling: 500 },165 );166167 const bodyText = await page.evaluate(() => document.body.textContent ?? "");168 console.log("Body snippet:", bodyText.slice(0, 300));169170 expect(bodyText).not.toContain("no Codex codes found");171 expect(bodyText).not.toContain("failed to read");172 });173174 test("imports the current-generation Codex PDF when the fixture is present", async () => {175 test.setTimeout(180_000);176 if (!existsSync(CURRENT_GEN_PDF_PATH)) {177 test.skip();178 return;179 }180181 const extId = await getExtId(ctx);182 const page = await openCodexRestorePage(ctx, extId, "codex=restore&codexMetrics=1");183 await page.locator('input[type="file"]').setInputFiles(CURRENT_GEN_PDF_PATH);184185 await page.waitForFunction(186 () => {187 const body = document.body.textContent ?? "";188 return (189 body.includes("master password") ||190 body.includes("pdf import needs another pass") ||191 body.includes("failed to read")192 );193 },194 { timeout: 150_000, polling: 1_000 },195 );196197 const metrics = await page.evaluate(() => (window as any).__codexImportMetrics ?? null);198 expect(metrics).not.toBeNull();199 expect(metrics.decodeRate).toBeGreaterThanOrEqual(0.98);200 expect(metrics.shortStripeCount).toBe(0);201 expect(metrics.perStripeDecoded.length).toBeGreaterThan(0);202203 const bodyText = await page.evaluate(() => document.body.textContent ?? "");204 expect(bodyText).not.toContain("failed to read");205 });206});