clipboard.hash-match.test.ts

5.5 KB · raw

1// PCH-02-010 regression tests (Option B only) — the auto-clear must compare2// the live clipboard hash (computed inside the offscreen document) against3// the parked SHA-256 before wiping, and must stay conservative when the4// comparison is unavailable.5//6// DESTINATION: src/background/clipboard.hash-match.test.ts7// Activates only if Option B of PCH-02-010 lands (offscreen GET_CLIPBOARD_HASH8// + clearClipboardNow match check + clipboardRead permission). Do NOT9// integrate under Option A. Imports are written in destination form; this10// file is excluded from the suite while it lives under src/drafts/.1112import { 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  /** What the mocked offscreen document replies to GET_CLIPBOARD_HASH. */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    // No CLEAR_CLIPBOARD message, no fallback write — the user's value survives.118    expect(clearMessageCount()).toBe(0);119    expect(clipboardWriteText).not.toHaveBeenCalled();120    // The clear is no longer owed: hash gone, alarm dropped.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    // No offscreen surface → no hash check possible → straight to the144    // active-tab/navigator write of "" (pre-fix behaviour preserved).145    expect(clearMessageCount()).toBe(0);146    expect(session["clipboard-clear-hash"]).toBeUndefined();147  });148});