12345678910111213141516171819202122232425import { test, expect } from "@playwright/test";26import type { BrowserContext, Page } from "@playwright/test";27import { readFile, readdir } from "node:fs/promises";28import { join } from "node:path";29import {30 EXTENSION_PATH,31 makeContext,32 setupVault,33 VAULT_PASSWORD,34} from "./helpers/vault";3536const SEEDED_ENTRY_COUNT = 250;37const SEEDED_ENTRY_TITLE = "Seeded Codex recovery entry";3839async 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}5051async 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 }168}169170async function getExtId(ctx: BrowserContext): Promise<string> {171 const maxWait = 15_000;172 const start = Date.now();173 let sw = ctx.serviceWorkers()[0];174 while (!sw && Date.now() - start < maxWait) {175 await new Promise((r) => setTimeout(r, 500));176 sw = ctx.serviceWorkers()[0];177 }178 if (!sw) throw new Error(`No service worker after ${maxWait}ms`);179 return new URL(sw.url()).host;180}181182async function openCodexRestorePage(183 ctx: BrowserContext,184 extId: string,185 search = "codex=restore",186): Promise<Page> {187 const page = await ctx.newPage();188189 190 page.on("console", (msg) => {191 const text = msg.text();192 if (msg.type() === "error" || text.startsWith("[codex]")) {193 console.log(` [browser:${msg.type()}] ${text}`);194 }195 });196197 198 199 200 await page.goto(`chrome-extension://${extId}/src/popup/index.html?${search}`);201202 203 204 await page.waitForSelector('input[type="file"]', { state: "attached", timeout: 10_000 });205206 return page;207}208209test.describe("Codex PDF restore", () => {210 let ctx: BrowserContext;211 let seededCodexFile: Awaited<ReturnType<typeof createSeededCodexFile>>;212213 test.describe.configure({ timeout: 180_000 });214215 test.beforeAll(async () => {216 test.setTimeout(180_000);217 seededCodexFile = await createSeededCodexFile();218 });219220 test.beforeEach(async () => {221 ctx = await makeContext();222 });223224 test.afterEach(async () => {225 await ctx.close();226 });227228 test("restores a seeded vault from its generated Codex PDF", async () => {229 const extId = await getExtId(ctx);230 const page = await openCodexRestorePage(ctx, extId);231232 await page.locator('input[type="file"]').setInputFiles(seededCodexFile);233234 235 236 237 238 239 await page.waitForFunction(240 () => {241 const body = document.body.textContent ?? "";242 return (243 body.includes("codes") ||244 body.includes("master password") ||245 body.includes("no Codex codes found") ||246 body.includes("failed to read")247 );248 },249 { timeout: 150_000, polling: 1_000 },250 );251252 const bodyText = await page.evaluate(() => document.body.textContent ?? "");253 console.log("Body snippet:", bodyText.replace(/\s+/g, " ").slice(0, 400));254255 expect(bodyText).not.toContain("no Codex codes found");256 expect(bodyText).not.toContain("failed to read");257258 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 });261262 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 });283 });284285 test("decodes at least one Aztec envelope from page 1", async () => {286 const extId = await getExtId(ctx);287 const page = await openCodexRestorePage(ctx, extId);288289 await page.locator('input[type="file"]').setInputFiles(seededCodexFile);290291 await page.waitForFunction(292 () => {293 const body = document.body.textContent ?? "";294 return (295 body.includes("codes") ||296 body.includes("master password") ||297 body.includes("no Codex codes found") ||298 body.includes("failed to read")299 );300 },301 { timeout: 90_000, polling: 500 },302 );303304 const bodyText = await page.evaluate(() => document.body.textContent ?? "");305 console.log("Body snippet:", bodyText.slice(0, 300));306307 expect(bodyText).not.toContain("no Codex codes found");308 expect(bodyText).not.toContain("failed to read");309 });310311 test("imports the generated current-generation Codex PDF without missing stripes", async () => {312 const extId = await getExtId(ctx);313 const page = await openCodexRestorePage(ctx, extId, "codex=restore&codexMetrics=1");314 await page.locator('input[type="file"]').setInputFiles(seededCodexFile);315316 await page.waitForFunction(317 () => {318 const body = document.body.textContent ?? "";319 return (320 body.includes("master password") ||321 body.includes("pdf import needs another pass") ||322 body.includes("failed to read")323 );324 },325 { timeout: 150_000, polling: 1_000 },326 );327328 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 );340 expect(metrics).not.toBeNull();341 if (!metrics) throw new Error("Codex import metrics were not recorded");342 expect(metrics.decodeRate).toBeGreaterThanOrEqual(0.98);343 expect(metrics.shortStripeCount).toBe(0);344 expect(metrics.perStripeDecoded.length).toBeGreaterThan(0);345346 const bodyText = await page.evaluate(() => document.body.textContent ?? "");347 expect(bodyText).not.toContain("failed to read");348 });349});