// PCH-02-010 regression tests (Option B only) — the auto-clear must compare // the live clipboard hash (computed inside the offscreen document) against // the parked SHA-256 before wiping, and must stay conservative when the // comparison is unavailable. // // DESTINATION: src/background/clipboard.hash-match.test.ts // Activates only if Option B of PCH-02-010 lands (offscreen GET_CLIPBOARD_HASH // + clearClipboardNow match check + clipboardRead permission). Do NOT // integrate under Option A. Imports are written in destination form; this // file is excluded from the suite while it lives under src/drafts/. import { beforeEach, describe, expect, it, vi } from "vitest"; type StorageShape = Record; async function sha256Hex(value: string): Promise { const digest = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(value), ); return Array.from(new Uint8Array(digest)) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } describe("PCH-02-010 — clipboard clear hash match", () => { let storage: StorageShape; let session: StorageShape; let sendMessage: ReturnType; let alarmsCreate: ReturnType; let alarmsClear: ReturnType; let clipboardWriteText: ReturnType; /** What the mocked offscreen document replies to GET_CLIPBOARD_HASH. */ let clipboardHashReply: { ok: boolean; hash?: string }; let clearMessageCount: () => number; beforeEach(() => { vi.resetModules(); storage = {}; session = {}; clipboardHashReply = { ok: false }; clipboardWriteText = vi.fn(async () => undefined); alarmsCreate = vi.fn(async () => undefined); alarmsClear = vi.fn(async () => true); sendMessage = vi.fn(async (msg: { type?: string }) => { if (msg?.type === "GET_CLIPBOARD_HASH") return clipboardHashReply; if (msg?.type === "CLEAR_CLIPBOARD") return { ok: true }; return { ok: true }; }); clearMessageCount = () => sendMessage.mock.calls.filter( (c: Array<{ type?: string }>) => c[0]?.type === "CLEAR_CLIPBOARD", ).length; const chromeMock = { runtime: { sendMessage }, offscreen: { createDocument: vi.fn(async () => undefined), closeDocument: vi.fn(async () => undefined), Reason: { CLIPBOARD: "CLIPBOARD" }, }, tabs: { query: vi.fn(async () => []) }, scripting: { executeScript: vi.fn(async () => []) }, alarms: { clear: alarmsClear, create: alarmsCreate, onAlarm: { addListener: vi.fn() }, }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() }, }, storage: { local: { get: vi.fn(async () => storage), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), }, session: { get: vi.fn(async (key: string) => ({ [key]: session[key] })), set: vi.fn(async (next: Record) => { Object.assign(session, next); }), remove: vi.fn(async (key: string) => { delete session[key]; }), }, }, } as unknown as typeof chrome; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock; vi.stubGlobal("navigator", { clipboard: { writeText: clipboardWriteText }, }); }); it("clears when the live clipboard hash matches the parked hash", async () => { session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret"); clipboardHashReply = { ok: true, hash: await sha256Hex("hunter2-secret") }; const clipboard = await import("./clipboard"); await clipboard.clearClipboardNow(); expect(clearMessageCount()).toBe(1); expect(session["clipboard-clear-hash"]).toBeUndefined(); }); it("skips the clear when the user already replaced the clipboard, and tears down bookkeeping", async () => { session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret"); clipboardHashReply = { ok: true, hash: await sha256Hex("grocery list") }; const clipboard = await import("./clipboard"); await clipboard.clearClipboardNow(); // No CLEAR_CLIPBOARD message, no fallback write — the user's value survives. expect(clearMessageCount()).toBe(0); expect(clipboardWriteText).not.toHaveBeenCalled(); // The clear is no longer owed: hash gone, alarm dropped. expect(session["clipboard-clear-hash"]).toBeUndefined(); expect(alarmsClear).toHaveBeenCalledWith("peach-clipboard-clear-current"); }); it("clears anyway when the clipboard hash cannot be read (conservative fallback)", async () => { session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret"); clipboardHashReply = { ok: false }; const clipboard = await import("./clipboard"); await clipboard.clearClipboardNow(); expect(clearMessageCount()).toBe(1); expect(session["clipboard-clear-hash"]).toBeUndefined(); }); it("keeps the unconditional clear on the no-offscreen (Firefox) path", async () => { delete (globalThis.chrome as unknown as Record).offscreen; session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret"); const clipboard = await import("./clipboard"); await clipboard.clearClipboardNow(); // No offscreen surface → no hash check possible → straight to the // active-tab/navigator write of "" (pre-fix behaviour preserved). expect(clearMessageCount()).toBe(0); expect(session["clipboard-clear-hash"]).toBeUndefined(); }); });