1import { beforeEach, describe, expect, it, vi } from "vitest";2import { deriveKey } from "../crypto/argon2";3456789type StorageShape = Record<string, unknown>;1011describe("vault session escrow refresh (PCH-01-008)", () => {12 let local: StorageShape;13 let session: StorageShape;1415 beforeEach(() => {16 vi.resetModules();17 local = {};18 session = {};19 const area = (store: StorageShape) => ({20 get: vi.fn(async (key?: string | string[]) => {21 if (!key) return store;22 if (typeof key === "string") return { [key]: store[key] };23 return key.reduce<Record<string, unknown>>((r, k) => { r[k] = store[k]; return r; }, {});24 }),25 set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(store, next); }),26 remove: vi.fn(async (keys: string | string[]) => {27 for (const k of Array.isArray(keys) ? keys : [keys]) delete store[k];28 }),29 });30 (globalThis as { chrome?: typeof chrome }).chrome = {31 storage: { local: area(local), session: area(session) },32 alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } },33 idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } },34 } as unknown as typeof chrome;35 vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("network disabled"); }));36 });3738 it("change_password_updates_session_escrow", async () => {39 const v1 = await import("./vault");40 await v1.createVault("old-pass-abc123");41 await v1.addEntry({ type: "login", title: "Acct", password: "pw" });42 expect(await v1.changePassword("old-pass-abc123", "new-pass-xyz789")).toBe(true);4344 45 46 await Promise.resolve();47 vi.resetModules();48 const v2 = await import("./vault");49 expect(await v2.restoreFromSession()).toBe(true);50 expect(v2.getEntries().some((e) => e.title === "Acct")).toBe(true);51 });5253 it("change_password_with_new_key_updates_session_escrow", async () => {54 const v1 = await import("./vault");55 await v1.createVault("orig-pass-abc123");56 await v1.addEntry({ type: "login", title: "Recovered", password: "pw" });5758 const newSalt = crypto.getRandomValues(new Uint8Array(16));59 const newKey = await deriveKey("reset-pass-xyz789", newSalt);60 await v1.changePasswordWithNewKey(newSalt, newKey);6162 await Promise.resolve();63 vi.resetModules();64 const v2 = await import("./vault");65 expect(await v2.restoreFromSession()).toBe(true);66 expect(v2.getEntries().some((e) => e.title === "Recovered")).toBe(true);67 });6869 it("restoreFromSession rehydrates rootKey so background sync survives a SW restart", async () => {70 71 72 73 74 75 76 77 78 const v1 = await import("./vault");79 await v1.createVault("strong-pass-abc123");80 81 82 83 await v1.unlock("strong-pass-abc123");84 const rootBeforeDead = v1.getRootKeyHex();85 expect(rootBeforeDead).not.toBeNull();8687 88 89 await Promise.resolve();90 vi.resetModules();91 const v2 = await import("./vault");92 expect(await v2.restoreFromSession()).toBe(true);93 94 95 expect(v2.getRootKeyHex()).not.toBeNull();96 expect(v2.getRootKeyHex()).toBe(rootBeforeDead);97 });9899 it("lock() clears the MV3 session key so a manual lock survives a SW restart", async () => {100 101 102 103 104 105 106 107 108 109 const v1 = await import("./vault");110 await v1.createVault("strong-pass-abc123");111 await v1.unlock("strong-pass-abc123");112 const sessionKeyBeforeLock = await chrome.storage.session.get("peach_session_dk");113 expect(sessionKeyBeforeLock.peach_session_dk).toBeDefined();114115 116 v1.lock();117 expect(v1.isUnlocked()).toBe(false);118 119 120 await Promise.resolve();121 const sessionKeyAfterLock = await chrome.storage.session.get("peach_session_dk");122 expect(sessionKeyAfterLock.peach_session_dk).toBeUndefined();123124 125 126 vi.resetModules();127 const v2 = await import("./vault");128 expect(await v2.restoreFromSession()).toBe(false);129 expect(v2.isUnlocked()).toBe(false);130 expect(v2.getRootKeyHex()).toBeNull();131 });132});