123456789101112import { beforeEach, describe, expect, it, vi } from "vitest";1314type StorageShape = Record<string, unknown>;1516async function sha256Hex(value: string): Promise<string> {17 const digest = await crypto.subtle.digest(18 "SHA-256",19 new TextEncoder().encode(value),20 );21 return Array.from(new Uint8Array(digest))22 .map((b) => b.toString(16).padStart(2, "0"))23 .join("");24}2526describe("PCH-02-010 — clipboard clear hash match", () => {27 let storage: StorageShape;28 let session: StorageShape;29 let sendMessage: ReturnType<typeof vi.fn>;30 let alarmsCreate: ReturnType<typeof vi.fn>;31 let alarmsClear: ReturnType<typeof vi.fn>;32 let clipboardWriteText: ReturnType<typeof vi.fn>;33 34 let clipboardHashReply: { ok: boolean; hash?: string };35 let clearMessageCount: () => number;3637 beforeEach(() => {38 vi.resetModules();39 storage = {};40 session = {};41 clipboardHashReply = { ok: false };42 clipboardWriteText = vi.fn(async () => undefined);43 alarmsCreate = vi.fn(async () => undefined);44 alarmsClear = vi.fn(async () => true);4546 sendMessage = vi.fn(async (msg: { type?: string }) => {47 if (msg?.type === "GET_CLIPBOARD_HASH") return clipboardHashReply;48 if (msg?.type === "CLEAR_CLIPBOARD") return { ok: true };49 return { ok: true };50 });51 clearMessageCount = () =>52 sendMessage.mock.calls.filter(53 (c: Array<{ type?: string }>) => c[0]?.type === "CLEAR_CLIPBOARD",54 ).length;5556 const chromeMock = {57 runtime: { sendMessage },58 offscreen: {59 createDocument: vi.fn(async () => undefined),60 closeDocument: vi.fn(async () => undefined),61 Reason: { CLIPBOARD: "CLIPBOARD" },62 },63 tabs: { query: vi.fn(async () => []) },64 scripting: { executeScript: vi.fn(async () => []) },65 alarms: {66 clear: alarmsClear,67 create: alarmsCreate,68 onAlarm: { addListener: vi.fn() },69 },70 idle: {71 setDetectionInterval: vi.fn(),72 onStateChanged: { addListener: vi.fn() },73 },74 storage: {75 local: {76 get: vi.fn(async () => storage),77 set: vi.fn(async (next: Record<string, unknown>) => {78 Object.assign(storage, next);79 }),80 },81 session: {82 get: vi.fn(async (key: string) => ({ [key]: session[key] })),83 set: vi.fn(async (next: Record<string, unknown>) => {84 Object.assign(session, next);85 }),86 remove: vi.fn(async (key: string) => {87 delete session[key];88 }),89 },90 },91 } as unknown as typeof chrome;9293 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock;94 vi.stubGlobal("navigator", {95 clipboard: { writeText: clipboardWriteText },96 });97 });9899 it("clears when the live clipboard hash matches the parked hash", async () => {100 session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret");101 clipboardHashReply = { ok: true, hash: await sha256Hex("hunter2-secret") };102103 const clipboard = await import("./clipboard");104 await clipboard.clearClipboardNow();105106 expect(clearMessageCount()).toBe(1);107 expect(session["clipboard-clear-hash"]).toBeUndefined();108 });109110 it("skips the clear when the user already replaced the clipboard, and tears down bookkeeping", async () => {111 session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret");112 clipboardHashReply = { ok: true, hash: await sha256Hex("grocery list") };113114 const clipboard = await import("./clipboard");115 await clipboard.clearClipboardNow();116117 118 expect(clearMessageCount()).toBe(0);119 expect(clipboardWriteText).not.toHaveBeenCalled();120 121 expect(session["clipboard-clear-hash"]).toBeUndefined();122 expect(alarmsClear).toHaveBeenCalledWith("peach-clipboard-clear-current");123 });124125 it("clears anyway when the clipboard hash cannot be read (conservative fallback)", async () => {126 session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret");127 clipboardHashReply = { ok: false };128129 const clipboard = await import("./clipboard");130 await clipboard.clearClipboardNow();131132 expect(clearMessageCount()).toBe(1);133 expect(session["clipboard-clear-hash"]).toBeUndefined();134 });135136 it("keeps the unconditional clear on the no-offscreen (Firefox) path", async () => {137 delete (globalThis.chrome as unknown as Record<string, unknown>).offscreen;138 session["clipboard-clear-hash"] = await sha256Hex("hunter2-secret");139140 const clipboard = await import("./clipboard");141 await clipboard.clearClipboardNow();142143 144 145 expect(clearMessageCount()).toBe(0);146 expect(session["clipboard-clear-hash"]).toBeUndefined();147 });148});