// Verifies the Phase 6 storage layer end-to-end in real browser // engines: a freshly generated device keypair persists across a page // reload, signs deterministically with the same private key after the // reload, and is wiped by `clearDeviceSession` so the next load // generates a different keypair. The live-gateway round-trip is // covered by Phase 7's e2e once the email-code login flow lands; // this spec deliberately stops at the storage boundary. import { expect, test, type Page } from "@playwright/test"; interface DebugSnapshot { publicKey: number[]; deviceSessionId: string | null; } import type { MapCameraSnapshot, MapPickStateSnapshot, MapPrimitiveSnapshot, } from "../../src/lib/debug-surface.svelte"; // Mirrors the surface mounted by `routes/__debug/store/+page.svelte`. // Other Playwright specs (`game-shell.spec.ts`, `order-composer.spec.ts`, // `cargo-routes.spec.ts`) reuse the global declaration below, so this // interface lists every helper any spec calls — not only those // exercised by this file. interface DebugSurface { ready: true; loadSession(): Promise; clearSession(): Promise; signWithDevice(message: number[]): Promise; setDeviceSessionId(id: string): Promise; verifyWithStoredPublicKey( message: number[], signature: number[], ): Promise; seedOrderDraft( gameId: string, commands: ReadonlyArray<{ kind: "placeholder"; id: string; label: string; }>, ): Promise; clearOrderDraft(gameId: string): Promise; getMapPrimitives(): readonly MapPrimitiveSnapshot[]; getMapPickState(): MapPickStateSnapshot; getMapCamera(): MapCameraSnapshot | null; } declare global { interface Window { __galaxyDebug?: DebugSurface; } } const CANONICAL = Array.from(new TextEncoder().encode("phase-6-canonical")); async function bootDebugPage(page: Page): Promise { await page.goto("/__debug/store"); await expect(page.getByTestId("debug-store-ready")).toBeVisible(); await page.waitForFunction(() => window.__galaxyDebug?.ready === true); } test("device keypair survives reload and produces verifiable signatures", async ({ page, }) => { await bootDebugPage(page); // Wipe any leftover state from a previous Playwright run that // shared the same browser profile. await page.evaluate(() => window.__galaxyDebug!.clearSession()); const first = await page.evaluate(async (canonical) => { const sess = await window.__galaxyDebug!.loadSession(); const signature = await window.__galaxyDebug!.signWithDevice(canonical); return { publicKey: sess.publicKey, signature }; }, CANONICAL); expect(first.publicKey.length).toBe(32); expect(first.signature.length).toBe(64); await page.reload(); await expect(page.getByTestId("debug-store-ready")).toBeVisible(); await page.waitForFunction(() => window.__galaxyDebug?.ready === true); const second = await page.evaluate( async ({ canonical, firstSig }) => { const sess = await window.__galaxyDebug!.loadSession(); const fresh = await window.__galaxyDebug!.signWithDevice(canonical); // Signatures produced before the reload must verify under the // post-reload public key. A pre-reload signature only verifies // when the persisted private key is identical to the original. const prevVerifies = await window.__galaxyDebug!.verifyWithStoredPublicKey( canonical, firstSig, ); const freshVerifies = await window.__galaxyDebug!.verifyWithStoredPublicKey( canonical, fresh, ); return { publicKey: sess.publicKey, signature: fresh, prevVerifies, freshVerifies, }; }, { canonical: CANONICAL, firstSig: first.signature }, ); expect(second.publicKey).toEqual(first.publicKey); expect(second.prevVerifies).toBe(true); expect(second.freshVerifies).toBe(true); }); test("clearDeviceSession forces a fresh keypair on next load", async ({ page, }) => { await bootDebugPage(page); await page.evaluate(() => window.__galaxyDebug!.clearSession()); const before = await page.evaluate(async () => { const sess = await window.__galaxyDebug!.loadSession(); return sess.publicKey; }); await page.evaluate(() => window.__galaxyDebug!.clearSession()); const after = await page.evaluate(async () => { const sess = await window.__galaxyDebug!.loadSession(); return sess.publicKey; }); expect(after).not.toEqual(before); }); test("setDeviceSessionId is observable through loadDeviceSession", async ({ page, }) => { await bootDebugPage(page); await page.evaluate(() => window.__galaxyDebug!.clearSession()); const before = await page.evaluate(async () => { const sess = await window.__galaxyDebug!.loadSession(); return sess.deviceSessionId; }); expect(before).toBeNull(); await page.evaluate(() => window.__galaxyDebug!.setDeviceSessionId("dev-1")); const after = await page.evaluate(async () => { const sess = await window.__galaxyDebug!.loadSession(); return sess.deviceSessionId; }); expect(after).toBe("dev-1"); });