// PB-26-107: passive page traffic must not refresh the auto-lock timer. // // Every inbound message used to call touchAutoLock(), which reschedules the // autolock alarm. The content script fires AUTOFILL_QUERY and ML_PREDICT_BATCH // on every `focusin` — an event browsers deliver for programmatic // element.focus() too — so a page running // setInterval(() => passwordField.focus(), 1000) // pinned the timer open for as long as the tab existed. Idle auto-lock is the // control a shared-machine user relies on, and it silently never fired. // // Both passive types are now excluded from the refresh; genuinely user-initiated // traffic still reschedules it. import { beforeEach, describe, expect, it, vi } from "vitest"; type StorageShape = Record; function makeChromeStorageMock() { const storage: StorageShape = {}; const chrome = { storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return { ...storage }; const keys = Array.isArray(key) ? key : [key]; return keys.reduce((acc, k) => { acc[k] = storage[k]; return acc; }, {}); }), set: vi.fn(async (next: StorageShape) => { Object.assign(storage, next); }), remove: vi.fn(async () => {}), }, session: { get: vi.fn(async () => ({})), set: vi.fn(async () => {}), remove: vi.fn(async () => {}), }, }, alarms: { clear: vi.fn(), create: vi.fn(), onAlarm: { addListener: vi.fn() } }, idle: { setDetectionInterval: vi.fn(), onStateChanged: { addListener: vi.fn() } }, runtime: { id: "test-extension-id", getURL: vi.fn((path: string) => `chrome-extension://test-extension-id/${path}`), }, } as unknown as typeof globalThis.chrome; return { chrome }; } const pageSender = { tab: { url: "https://example.com/login" }, url: "https://example.com/login", } as unknown as chrome.runtime.MessageSender; describe("PB-26-107: passive page traffic does not refresh auto-lock", () => { let vault: typeof import("./vault"); let messages: typeof import("./messages"); beforeEach(async () => { vi.resetModules(); (globalThis as Record).chrome = makeChromeStorageMock().chrome; vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("no network"); })); vi.stubGlobal("__PEACH_E2E_PASSKEYS__", false); vault = await import("./vault"); messages = await import("./messages"); await vault.createVault("test-autolock-pass"); }); function alarmCreations(): number { return (chrome.alarms.create as unknown as ReturnType).mock.calls.length; } it("content-script GET_SETTINGS does not reschedule the lock alarm", async () => { (chrome.alarms.create as unknown as ReturnType).mockClear(); await messages.handleMessage({ type: "GET_SETTINGS" }, pageSender); expect(alarmCreations()).toBe(0); }); it("repeated AUTOFILL_QUERY does not reschedule the lock alarm", async () => { (chrome.alarms.create as unknown as ReturnType).mockClear(); for (let i = 0; i < 50; i += 1) { await messages.handleMessage( { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } }, pageSender, ); } expect(alarmCreations()).toBe(0); }); it("repeated ML_PREDICT_BATCH does not reschedule the lock alarm", async () => { (chrome.alarms.create as unknown as ReturnType).mockClear(); for (let i = 0; i < 50; i += 1) { await messages.handleMessage( { type: "ML_PREDICT_BATCH", payload: { fields: [] } }, pageSender, ); } expect(alarmCreations()).toBe(0); }); it("page TOUCH_AUTO_LOCK is rejected without rescheduling the lock alarm", async () => { (chrome.alarms.create as unknown as ReturnType).mockClear(); await expect( messages.handleMessage({ type: "TOUCH_AUTO_LOCK", payload: {} }, pageSender), ).resolves.toMatchObject({ ok: false, error: "invalid_sender" }); expect(alarmCreations()).toBe(0); }); it("an extension-page TOUCH_AUTO_LOCK reschedules the lock alarm", async () => { (chrome.alarms.create as unknown as ReturnType).mockClear(); await messages.handleMessage( { type: "TOUCH_AUTO_LOCK", payload: {} }, { url: "chrome-extension://test-extension-id/popup.html" }, ); expect(alarmCreations()).toBe(1); }); // A focus loop interleaved with genuine activity must not be able to build a // longer session than the activity alone would. it("a focus loop between real actions adds nothing to the deadline", async () => { (chrome.alarms.create as unknown as ReturnType).mockClear(); for (let i = 0; i < 20; i += 1) { await messages.handleMessage( { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } }, pageSender, ); } await messages.handleMessage( { type: "TOUCH_AUTO_LOCK", payload: {} }, { url: "chrome-extension://test-extension-id/popup.html" }, ); for (let i = 0; i < 20; i += 1) { await messages.handleMessage( { type: "AUTOFILL_QUERY", payload: { domain: "example.com", kind: "login" } }, pageSender, ); } expect(alarmCreations()).toBe(1); }); });