From c580ba940a1a29c586db47ad7df5234d51e0cc4a Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 13 Oct 2026 08:43:11 -0700 Subject: [PATCH] fix(browser): treat an unusable cache list as incomplete storage coverage (#2762) Squash merge of https://github.com/morluto/rea/pull/2762. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- src/browser/CdpCaptureStorage.test.ts | 84 +++++++++++++++++++++++++++ src/browser/CdpCaptureStorage.ts | 10 ++-- 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 src/browser/CdpCaptureStorage.test.ts diff --git a/src/browser/CdpCaptureStorage.test.ts b/src/browser/CdpCaptureStorage.test.ts new file mode 100644 index 000000000..51bb49a08 --- /dev/null +++ b/src/browser/CdpCaptureStorage.test.ts @@ -0,0 +1,84 @@ +import { expect, it } from "vitest"; + +import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; +import { captureStorage } from "./CdpCaptureStorage.js"; +import { CdpCommandRejection } from "./CdpCommandRejection.js"; +import type { CdpConnection } from "./CdpConnection.js"; + +const NO_CACHE_COMMAND = Symbol("unsupported"); + +const captureWithCacheResult = async ( + cacheResult: unknown, +): Promise>["value"]> => { + const connection = { + send(method: string): Promise { + switch (method) { + case "Storage.getUsageAndQuota": + return Promise.resolve({ usage: 1, quota: 10 }); + case "DOMStorage.getDOMStorageItems": + return Promise.resolve({ entries: [] }); + case "IndexedDB.requestDatabaseNames": + return Promise.resolve({ databaseNames: [] }); + case "Network.getCookies": + return Promise.resolve({ cookies: [] }); + case "CacheStorage.requestCacheNames": + return cacheResult === NO_CACHE_COMMAND + ? Promise.reject( + new CdpCommandRejection( + "inspect_web_page", + method, + -32601, + "Method not found", + ), + ) + : Promise.resolve(cacheResult); + case "CacheStorage.requestEntries": + return Promise.resolve({ cacheDataEntries: [], returnCount: 0 }); + default: + return Promise.reject(new Error(`Unexpected command: ${method}`)); + } + }, + } as unknown as CdpConnection; + const { value } = await captureStorage( + { + connection, + sessionId: undefined, + input: inspectWebPageInputSchema.parse({ + cdp_endpoint: "http://127.0.0.1:9222", + target_id: "fixture-page", + include_storage_keys: true, + include_storage_fingerprints: true, + }), + }, + "https://example.test/", + [], + ); + return value; +}; + +it.each([ + ["an observed empty cache list", { caches: [] }, true], + ["a response without a cache list", {}, false], + ["a non-array cache list", { caches: "nope" }, false], + ["a cache list with an unusable entry", { caches: [5] }, false], + ["an unsupported cache command", NO_CACHE_COMMAND, false], +] as const)( + "reports fingerprint completeness for %s", + async (_label, cacheResult, complete) => { + const value = await captureWithCacheResult(cacheResult); + expect(value.cache_names).toEqual([]); + expect(value.fingerprints_complete).toBe(complete); + }, +); + +it.each([false, true])( + "preserves valid cache evidence beside an unusable entry: %s", + async (mixed) => { + const cache = { cacheName: "fixture-cache", cacheId: "fixture-cache-id" }; + const value = await captureWithCacheResult({ + caches: mixed ? [cache, 5] : [cache], + }); + expect(value.cache_names).toEqual(["fixture-cache"]); + expect(value.fingerprints_complete).toBe(!mixed); + }, +); diff --git a/src/browser/CdpCaptureStorage.ts b/src/browser/CdpCaptureStorage.ts index 8ddcea9a8..1e88ca74a 100644 --- a/src/browser/CdpCaptureStorage.ts +++ b/src/browser/CdpCaptureStorage.ts @@ -60,8 +60,8 @@ export const captureStorage = async ( limitations, ) : undefined; - const cacheValues = recordsValue(recordValue(cacheRaw)?.caches); - const caches = capturedCaches(cacheValues); + const cacheField = recordValue(cacheRaw)?.caches; + const caches = capturedCaches(recordsValue(cacheField)); const fingerprints = context.input.include_storage_fingerprints ? await captureStorageFingerprints({ context, @@ -78,8 +78,10 @@ export const captureStorage = async ( indexedRaw !== undefined && Array.isArray(indexedValues) && indexed.length === indexedValues.length && - cacheRaw !== undefined && - caches.length === cacheValues.length; + // Like IndexedDB names, a missing, non-array or partly unusable cache + // list is an incomplete enumeration rather than an empty one. + Array.isArray(cacheField) && + caches.length === cacheField.length; return { value: { origin,