[GH-ISSUE #2660] fix(browser): incomplete cache enumeration reports complete fingerprints #962

Closed
opened 2026-10-15 18:29:56 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2660

Summary

When storage-key and fingerprint capture are selected, an admitted cache-name response that contains no cache array produces an empty cache inventory with fingerprints_complete set to true. The capture gives callers no indication that cache enumeration was incomplete. This affects REA's interpretation of a malformed producer result; no real Chromium instance was observed producing that result.

Runtime or environment

Verified source revision: a2d8b94dd5. The focused source test used Node 24.18.0, npm 11.16.0, and Vitest 5.0.3. The published package and an actual browser session were not executed.

Current-upstream source recheck: 3ae826ceba. The relevant producer, transport, capture projection, contract, and comparison code are unchanged from the verified revision. This recheck did not rerun the source fixture.

Steps to reproduce

In a source checkout with its pinned dependencies installed, save this test as src/browser/cacheInventoryProjection.test.ts and run it with the repository's source-test command. The test uses the real inspection input schema and production storage parser, with a tiny inert command-result fixture. It does not connect to or start a browser. All four cases were executed in the verified source checkout.

import { describe, expect, it } from "vitest";
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
import type { CdpConnection } from "./CdpConnection.js";
import { CdpCommandRejection } from "./CdpCommandRejection.js";
import { captureStorage } from "./CdpCaptureStorage.js";

describe("cache inventory projection only", () => {
  for (const [label, cacheResult, expected] of [
    ["valid empty inventory", { caches: [] }, true],
    ["missing required field", {}, true],
    ["non-array required field", { caches: "nope" }, true],
    ["unsupported method", undefined, false],
  ] as const) {
    it(label, async () => {
      const calls: string[] = [];
      const connection = {
        async send(method: string): Promise<unknown> {
          calls.push(method);
          switch (method) {
            case "Storage.getUsageAndQuota": return { usage: 1, quota: 10 };
            case "DOMStorage.getDOMStorageItems": return { entries: [] };
            case "IndexedDB.requestDatabaseNames": return { databaseNames: [] };
            case "Network.getCookies": return { cookies: [] };
            case "CacheStorage.requestCacheNames":
              if (cacheResult === undefined)
                throw new CdpCommandRejection("inspect_web_page", method, -32601, "Method not found");
              return cacheResult;
            default: throw new Error(`Unexpected command: ${method}`);
          }
        },
      } as unknown as CdpConnection;
      const limitations: string[] = [];
      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/", limitations);
      expect(value.fingerprints_complete).toBe(expected);
      expect(value.cache_names).toEqual([]);
      expect(calls).toEqual([
        "Storage.getUsageAndQuota", "DOMStorage.getDOMStorageItems",
        "DOMStorage.getDOMStorageItems", "IndexedDB.requestDatabaseNames",
        "CacheStorage.requestCacheNames", "Network.getCookies",
      ]);
      console.log(JSON.stringify({ label, cache_names: value.cache_names,
        fingerprints_complete: value.fingerprints_complete, limitations }));
    });
  }
});
npm run test:local -- src/browser/cacheInventoryProjection.test.ts

The assertions deliberately describe the current result so that this observation test passes on the failing baseline. The missing-field case is the smallest tested cache response. The non-array case and valid empty array distinguish lost enumeration from an observed empty inventory; the method-not-found control exercises the already handled unavailability path.

Expected behavior

An inspection must preserve the distinction between an observed empty cache inventory and a response that never supplies an inventory. The browser guide states that incomplete values are reported, and that missing evidence is never equivalence in capture comparisons. For an admitted response without a valid cache array, the capture should report incomplete coverage or an explicit observation failure rather than assert complete fingerprints. A valid empty array should continue to count as complete when all other selected storage observations are complete.

Actual behavior

The executed source seam returned these projections:

{"label":"valid empty inventory","cache_names":[],"fingerprints_complete":true}
{"label":"missing required field","cache_names":[],"fingerprints_complete":true}
{"label":"non-array required field","cache_names":[],"fingerprints_complete":true}
{"label":"unsupported method","cache_names":[],"fingerprints_complete":false}

The malformed cases had only the ordinary cookie URL-scope limitation. No limitation identified the missing cache enumeration. The focused run passed four baseline-observation tests in one source adapter file and exited 0.

Static inspection establishes that both malformed fixtures are admitted by the public inspection path and that their resulting completeness flag can satisfy the storage-comparability gate. No complete inspect_web_page invocation or downstream comparison was executed, and occurrence in a real browser remains unknown. CDP declares caches as a required array, so these fixtures represent producer malformation, not conforming browser output.

Evidence

  • Expected source: docs/browser-observation.md:158-162 documents per-value validation and reporting of incomplete capture values; line 22 promises that missing or truncated comparison evidence is never equivalence. The selected storage options are documented at line 179.
  • Failure source: The executed captureStorage fixture above returned complete fingerprints for both malformed cache-name results. src/browser/CdpCaptureStorage.ts:55-94 and src/browser/CdpCaptureValues.ts:15-21 show the missing/non-array field becoming an accepted empty inventory.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Static applicability references: src/browser/CdpBrowserProvider.ts:123-131 and 334-365; src/browser/CdpPageCapture.ts:149-183 and 332; src/browser/CdpConnection.ts:240-260; src/browser/CdpOptionalCommand.ts:15-21; src/domain/browserObservationSchemas.ts:348. Consumer reference: src/domain/webCaptureDiff.ts:654-663.

Related: #2554 concerns an omitted IndexedDB record value, and #2608 concerns comparison of complete key inventories without selected fingerprints. Both have distinct triggers and restoration checks. #377 and its parent #65 introduced storage comparison; their closure does not establish correction of this malformed-cache-inventory result.

Restoration check

With the same valid selected input and otherwise complete empty inventories, neither malformed cache response should produce a successful claim of complete storage fingerprints. An explicit failure or a retained result with incomplete coverage restores the distinction. Keep the valid empty-array control complete and the method-not-found control incomplete. If malformed evidence is retained, comparing two otherwise identical captures containing it must not classify their storage evidence as unchanged solely because both cache lists are empty. Verify that consumer consequence separately from the parser assertion.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The observed parser converts missing cache enumeration into a complete empty inventory, which can satisfy the storage comparison gate and conceal uncertainty. This is bounded to admitted malformed producer results; real-browser occurrence and frequency were not measured.
  • Workaround status: unverified
  • Workaround basis: No caller-side mitigation was tested. The normal caller does not supply the cache-name response, and disabling fingerprint capture would forgo the selected content-comparison evidence rather than recover it.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2660 ## Summary When storage-key and fingerprint capture are selected, an admitted cache-name response that contains no cache array produces an empty cache inventory with fingerprints_complete set to true. The capture gives callers no indication that cache enumeration was incomplete. This affects REA's interpretation of a malformed producer result; no real Chromium instance was observed producing that result. ## Runtime or environment Verified source revision: a2d8b94dd50dab8d966239dd63ab84718aa8b4b4. The focused source test used Node 24.18.0, npm 11.16.0, and Vitest 5.0.3. The published package and an actual browser session were not executed. Current-upstream source recheck: 3ae826ceba6f25b08bce50bcb51001209bc16b57. The relevant producer, transport, capture projection, contract, and comparison code are unchanged from the verified revision. This recheck did not rerun the source fixture. ## Steps to reproduce In a source checkout with its pinned dependencies installed, save this test as src/browser/cacheInventoryProjection.test.ts and run it with the repository's source-test command. The test uses the real inspection input schema and production storage parser, with a tiny inert command-result fixture. It does not connect to or start a browser. All four cases were executed in the verified source checkout. ```ts import { describe, expect, it } from "vitest"; import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; import type { CdpConnection } from "./CdpConnection.js"; import { CdpCommandRejection } from "./CdpCommandRejection.js"; import { captureStorage } from "./CdpCaptureStorage.js"; describe("cache inventory projection only", () => { for (const [label, cacheResult, expected] of [ ["valid empty inventory", { caches: [] }, true], ["missing required field", {}, true], ["non-array required field", { caches: "nope" }, true], ["unsupported method", undefined, false], ] as const) { it(label, async () => { const calls: string[] = []; const connection = { async send(method: string): Promise<unknown> { calls.push(method); switch (method) { case "Storage.getUsageAndQuota": return { usage: 1, quota: 10 }; case "DOMStorage.getDOMStorageItems": return { entries: [] }; case "IndexedDB.requestDatabaseNames": return { databaseNames: [] }; case "Network.getCookies": return { cookies: [] }; case "CacheStorage.requestCacheNames": if (cacheResult === undefined) throw new CdpCommandRejection("inspect_web_page", method, -32601, "Method not found"); return cacheResult; default: throw new Error(`Unexpected command: ${method}`); } }, } as unknown as CdpConnection; const limitations: string[] = []; 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/", limitations); expect(value.fingerprints_complete).toBe(expected); expect(value.cache_names).toEqual([]); expect(calls).toEqual([ "Storage.getUsageAndQuota", "DOMStorage.getDOMStorageItems", "DOMStorage.getDOMStorageItems", "IndexedDB.requestDatabaseNames", "CacheStorage.requestCacheNames", "Network.getCookies", ]); console.log(JSON.stringify({ label, cache_names: value.cache_names, fingerprints_complete: value.fingerprints_complete, limitations })); }); } }); ``` ```sh npm run test:local -- src/browser/cacheInventoryProjection.test.ts ``` The assertions deliberately describe the current result so that this observation test passes on the failing baseline. The missing-field case is the smallest tested cache response. The non-array case and valid empty array distinguish lost enumeration from an observed empty inventory; the method-not-found control exercises the already handled unavailability path. ## Expected behavior An inspection must preserve the distinction between an observed empty cache inventory and a response that never supplies an inventory. The browser guide states that incomplete values are reported, and that missing evidence is never equivalence in capture comparisons. For an admitted response without a valid cache array, the capture should report incomplete coverage or an explicit observation failure rather than assert complete fingerprints. A valid empty array should continue to count as complete when all other selected storage observations are complete. ## Actual behavior The executed source seam returned these projections: ```json {"label":"valid empty inventory","cache_names":[],"fingerprints_complete":true} {"label":"missing required field","cache_names":[],"fingerprints_complete":true} {"label":"non-array required field","cache_names":[],"fingerprints_complete":true} {"label":"unsupported method","cache_names":[],"fingerprints_complete":false} ``` The malformed cases had only the ordinary cookie URL-scope limitation. No limitation identified the missing cache enumeration. The focused run passed four baseline-observation tests in one source adapter file and exited 0. Static inspection establishes that both malformed fixtures are admitted by the public inspection path and that their resulting completeness flag can satisfy the storage-comparability gate. No complete inspect_web_page invocation or downstream comparison was executed, and occurrence in a real browser remains unknown. CDP declares caches as a required array, so these fixtures represent producer malformation, not conforming browser output. ## Evidence - Expected source: docs/browser-observation.md:158-162 documents per-value validation and reporting of incomplete capture values; line 22 promises that missing or truncated comparison evidence is never equivalence. The selected storage options are documented at line 179. - Failure source: The executed captureStorage fixture above returned complete fingerprints for both malformed cache-name results. src/browser/CdpCaptureStorage.ts:55-94 and src/browser/CdpCaptureValues.ts:15-21 show the missing/non-array field becoming an accepted empty inventory. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete Static applicability references: src/browser/CdpBrowserProvider.ts:123-131 and 334-365; src/browser/CdpPageCapture.ts:149-183 and 332; src/browser/CdpConnection.ts:240-260; src/browser/CdpOptionalCommand.ts:15-21; src/domain/browserObservationSchemas.ts:348. Consumer reference: src/domain/webCaptureDiff.ts:654-663. Related: #2554 concerns an omitted IndexedDB record value, and #2608 concerns comparison of complete key inventories without selected fingerprints. Both have distinct triggers and restoration checks. #377 and its parent #65 introduced storage comparison; their closure does not establish correction of this malformed-cache-inventory result. ## Restoration check With the same valid selected input and otherwise complete empty inventories, neither malformed cache response should produce a successful claim of complete storage fingerprints. An explicit failure or a retained result with incomplete coverage restores the distinction. Keep the valid empty-array control complete and the method-not-found control incomplete. If malformed evidence is retained, comparing two otherwise identical captures containing it must not classify their storage evidence as unchanged solely because both cache lists are empty. Verify that consumer consequence separately from the parser assertion. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The observed parser converts missing cache enumeration into a complete empty inventory, which can satisfy the storage comparison gate and conceal uncertainty. This is bounded to admitted malformed producer results; real-browser occurrence and frequency were not measured. - Workaround status: unverified - Workaround basis: No caller-side mitigation was tested. The normal caller does not supply the cache-name response, and disabling fingerprint capture would forgo the selected content-comparison evidence rather than recover it.
bsakaguchi 2026-10-15 18:29:56 +00:00
  • closed this issue
  • added the
    bug
    label
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#962
No description provided.