12345678910111213141516171819202122232425import { chromium, 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-chrome");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 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 });50}5152async function getExtId(ctx: BrowserContext): Promise<string> {53 const maxWait = 15_000;54 const start = Date.now();55 let sw = ctx.serviceWorkers()[0];56 while (!sw && Date.now() - start < maxWait) {57 await new Promise((r) => setTimeout(r, 500));58 sw = ctx.serviceWorkers()[0];59 }60 if (!sw) throw new Error(`No service worker after ${maxWait}ms`);61 return new URL(sw.url()).host;62}6364async function openCodexRestorePage(65 ctx: BrowserContext,66 extId: string,67 search = "codex=restore",68): Promise<Page> {69 const page = await ctx.newPage();7071 72 page.on("console", (msg) => {73 const text = msg.text();74 if (msg.type() === "error" || text.startsWith("[codex]")) {75 console.log(` [browser:${msg.type()}] ${text}`);76 }77 });7879 80 81 82 await page.goto(`chrome-extension://${extId}/src/popup/index.html?${search}`);8384 85 86 await page.waitForSelector('input[type="file"]', { state: "attached", timeout: 10_000 });8788 return page;89}9091test.describe("Codex PDF restore", () => {92 let ctx: BrowserContext;9394 test.beforeEach(async () => {95 ctx = await makeContext();96 });9798 test.afterEach(async () => {99 await ctx.close();100 });101102 test("imports all chunks from the real Codex PDF", async () => {103 104 105 test.setTimeout(180_000);106107 const extId = await getExtId(ctx);108 const page = await openCodexRestorePage(ctx, extId);109110 111 112 await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);113114 115 116 117 118 119 await page.waitForFunction(120 () => {121 const body = document.body.textContent ?? "";122 return (123 body.includes("codes") ||124 body.includes("master password") ||125 body.includes("no Codex codes found") ||126 body.includes("failed to read")127 );128 },129 { timeout: 150_000, polling: 1_000 },130 );131132 const bodyText = await page.evaluate(() => document.body.textContent ?? "");133 console.log("Body snippet:", bodyText.replace(/\s+/g, " ").slice(0, 400));134135 expect(bodyText).not.toContain("no Codex codes found");136 expect(bodyText).not.toContain("failed to read");137138 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/);143 });144145 test("decodes at least one Aztec envelope from page 1", async () => {146 test.setTimeout(120_000);147148 const extId = await getExtId(ctx);149 const page = await openCodexRestorePage(ctx, extId);150151 await page.locator('input[type="file"]').setInputFiles(CODEX_PDF_PATH);152153 await page.waitForFunction(154 () => {155 const body = document.body.textContent ?? "";156 return (157 body.includes("codes") ||158 body.includes("master password") ||159 body.includes("no Codex codes found") ||160 body.includes("failed to read")161 );162 },163 { timeout: 90_000, polling: 500 },164 );165166 const bodyText = await page.evaluate(() => document.body.textContent ?? "");167 console.log("Body snippet:", bodyText.slice(0, 300));168169 expect(bodyText).not.toContain("no Codex codes found");170 expect(bodyText).not.toContain("failed to read");171 });172173 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 }179180 const extId = await getExtId(ctx);181 const page = await openCodexRestorePage(ctx, extId, "codex=restore&codexMetrics=1");182 await page.locator('input[type="file"]').setInputFiles(CURRENT_GEN_PDF_PATH);183184 await page.waitForFunction(185 () => {186 const body = document.body.textContent ?? "";187 return (188 body.includes("master password") ||189 body.includes("pdf import needs another pass") ||190 body.includes("failed to read")191 );192 },193 { timeout: 150_000, polling: 1_000 },194 );195196 const metrics = await page.evaluate(() => (window as any).__codexImportMetrics ?? null);197 expect(metrics).not.toBeNull();198 expect(metrics.decodeRate).toBeGreaterThanOrEqual(0.98);199 expect(metrics.shortStripeCount).toBe(0);200 expect(metrics.perStripeDecoded.length).toBeGreaterThan(0);201202 const bodyText = await page.evaluate(() => document.body.textContent ?? "");203 expect(bodyText).not.toContain("failed to read");204 });205});