Skip to content

fix(browser): mark an undecodable worker inventory unavailable - #3513

Open
NIU-123370 wants to merge 2 commits into
morluto:mainfrom
NIU-123370:fix/worker-inventory-uncertainty
Open

NIU-123370 wants to merge 2 commits into
morluto:mainfrom
NIU-123370:fix/worker-inventory-uncertainty

Conversation

@NIU-123370

@NIU-123370 NIU-123370 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #3378.

inspect-web-page reported workers: [] with no omission marker whenever the
browser's Target.getTargets reply could not be read as a worker inventory. Only a
rejected command was treated as unobservable; a reply that omitted
targetInfos, delivered it as a scalar, or delivered a non-object entry was
silently turned into "this page has no workers", so a failed enumeration was
indistinguishable from a valid empty inventory.

Problem and expected behavior

  • Trigger: the Target.getTargets reply is {}, or
    { targetInfos: "worker-one" }, or { targetInfos: [null] }.
  • Violated invariant: an observation REA could not decode must stay visible as
    unavailable/uncertain instead of being reported as an empty result.
  • Expected now: an absent or non-list targetInfos marks the workers section
    unavailable; a malformed entry is recorded as an item exclusion; a delivered
    empty list remains a valid empty inventory and gains no marker.

This follows the pattern the repository already uses for the same protocol shape
in storage capture (src/browser/CdpCaptureStorage.ts:145 treats
raw === undefined || !Array.isArray(entries) as "cannot observe", distinguishing
provider_unavailable from invalid_protocol_value) rather than inventing a new
vocabulary.

Change and scope

  • src/browser/CdpPageCaptureWorkers.ts — the inventory is read once, and:
    • reply missing / non-list ⇒ completeness.unavailable("workers") and the
      enumeration stops;
    • an entry that is not an object ⇒ completeness.exclude("workers", "invalid_protocol_value"),
    • a delivered array (including an empty one) is enumerated exactly as before.
      The now-unused recordsValue import was dropped; the pre-filtering helper that
      caused the silent drop is no longer used on this path.
  • tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts — four new cases
    (absent key, non-list, malformed entry followed by a valid worker, valid empty
    control). The malformed-entry case and the existing it.each(["omitted","null"])
    case now share one local workerTarget(origin, attachment) builder, so the file
    keeps a single capture preamble and a single TargetInfo literal.

Intentionally not included:

  • The Electron worker enumeration has the same shape
    (src/browser/CdpElectronWorkers.ts:42 pre-filters
    recordValue(result)?.targetInfos with recordsValue). I left it untouched
    because its boundary lane needs a real .app fixture that I did not execute in
    this session, and I would rather ship a proven page-side fix than an
    unproven second copy. The same three cases apply there and can follow this PR.
  • No change to WebPageInspection["workers"] items, the completeness schema, or
    the comparison rules: the fix only stops an undecodable enumeration from being
    reported as an empty observation.

Contract and boundary impact

  • Semantic owner and earliest changed stage: CdpCaptureCompleteness remains the
    only owner of completeness state; the earliest changed stage is worker inventory
    decoding inside page capture.
  • Contribution scope fit and linked maintainer decision: ordinary fix inside an
    existing boundary — not applicable.
  • CLI and MCP/tool-catalog contract: no schema, flag, or description change. A
    malformed reply now yields unavailable_sections: ["workers"] (plus the
    corresponding provider_unavailable exclusion) instead of a clean-looking empty
    list.
  • Provider, bridge, target-format, or platform compatibility: CDP passive page
    inspection only; no provider or platform gate touched.
  • Evidence, artifact, provenance, or reconstruction contract: unsupported-empty
    claims disappear for this section; comparisons that previously treated two such
    captures as equal on workers now see the uncertainty. No retained field is
    renamed or removed.
  • Target selection, authority, side effects, and cleanup details: unchanged — no
    new CDP command is sent, and the same single Target.getTargets call is used.
  • Generated metadata (docs/public/product-catalog.json), package, or installation
    impact: none.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated: four cases in
    tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts, including the
    negative control that a delivered empty inventory stays unmarked, and the case
    that a malformed entry is excluded without ending the enumeration (arm M7).

  • Review revision 9126757b, taken after the Greptile [P2] on head f2cf93be:
    the malformed-entry case used to feed [null] alone, so it also passed when the
    per-entry continue became return items, i.e. when the rest of the inventory
    was abandoned. Measured, not inferred — that mutation gives 6 passed (6)
    against the previous file and 1 failed | 5 passed (6) against this one. Every
    arm below was re-run against the revised file.

  • Base reproduction: with the final test file and only the source change reverted,
    the same file reports 3 failed | 3 passed (6) (arm M1 below is exactly that
    base revision), i.e. the reported behavior was "no marker at all". Executed.

  • Why the non-list case asserts the exclusion entry rather than only
    unavailable_sections: while measuring this fix I found that a weaker guard
    (inventory === undefined) still leaves workers in unavailable_sections for
    that reply, so the section marker alone does not pin the distinction. The
    observable that does is which exclusion the section carries:
    { reason: "provider_unavailable", count: null } (section cannot be observed)
    versus { reason: "invalid_protocol_value", count: 10 } (one per character of
    the scalar, i.e. the reply decoded item-by-item). The assertion records the
    former. The same mechanism is why arm M3 is now killed by that one case alone:
    CdpCaptureCompleteness.exclude (src/browser/CdpCaptureCompleteness.ts:63-67)
    also puts a section into unavailable_sections for invalid_protocol_value, so
    an item-exclusion-only implementation still satisfies a bare
    unavailable_sections assertion and only the recorded reason/count tells
    the two apart.

  • Mutation arms, each actually run against
    tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts, tree restored and
    re-verified between arms (final test file re-run afterwards, same result):

    arm mutation observed killed by
    C1 none (control) 6 passed (6) —
    M1 base defect (pre-filtered inventory, marker only for an undefined reply) 3 failed | 3 passed (6) absent-key, non-list and malformed-entry cases
    M2 weaker guard (inventory === undefined instead of !Array.isArray) 1 failed | 5 passed (6) non-list case
    M3 swap the section marker for an item exclusion 1 failed | 5 passed (6) non-list case
    M4 over-broad: a delivered empty list is also unavailable 1 failed | 5 passed (6) empty-list control
    M5 drop the per-entry record guard (trust producer items) 1 failed | 5 passed (6) malformed-entry case
    M6 report the wrong exclusion reason 1 failed | 5 passed (6) malformed-entry case
    M7 a malformed entry abandons the rest of the list (continue → return items) 1 failed | 5 passed (6) malformed-entry case, and nothing else

    M4 is killed only by the empty-list control, which is why that case is in the
    file. M5, M6 and M7 are killed only by the malformed-entry case, and M7 is the
    arm that separates "excluded this entry" from "stopped enumerating": it can only
    fail if a valid entry follows the malformed one.

  • Remaining proof gaps: the malformed replies are delivered by the repository's
    fake CDP peer (commandResult knob), matching the reporter's own
    producer-simulation method; no live Chrome was made to send a malformed
    Target.getTargets. The Electron sibling path is uncovered here, stated above.

Validation performed

  • npx vitest run tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts
    with only the source change reverted — 3 failed | 3 passed (6); with the fix —
    Test Files 1 passed (1), Tests 6 passed (6).
  • npx vitest run tests/boundary/browser/ tests/boundary/mcp/browserMcp.test.ts
    — Test Files 50 passed | 1 skipped (51), Tests 554 passed | 6 skipped (560);
    re-run on the final base eab789aed, same counts.
  • npm run typecheck — exit 0 (also re-run on eab789aed).
  • npm run lint — exit 0 (Checked resolved module boundaries in 1885 source files.).
  • npm run verify:test-discovery — exit 0 (Discovered all 1279 test files exactly once.). npm run verify:architecture-guards — exit 0
    (Architecture guards passed across 1398 source files.).
  • ./node_modules/.bin/oxfmt --check on the two changed files — exit 0,
    All matched files use the correct format. (prettier was not run).
  • npm run jscpd on this head — Duplication: 2724 files, 2008 clones, 3.22%.
    Measured three times in one tree swapping only the two PR files: trunk
    eab789aed = 2008 clones / 21105 duplicated lines / 3.215794%, previous head
    f2cf93be = 2008 / 21105 / 3.215520%, this head = 2008 / 21105 / 3.215441%.
    The clone count and duplicated-line count are unchanged, so neither the patch
    nor the review revision adds a clone — the percentage moves only because the
    total line count does. The review revision replaces a second inline
    TargetInfo literal with the shared workerTarget builder instead of adding
    one.
  • Not run: npm run build/real-browser runtime for this path (the cases are
    protocol-level), check:ci, the Electron boundary lane, other platforms.

Compatibility, safety, and release

  • Breaking changes or migration steps: none in schema. Callers that relied on a
    silent empty workers array for a malformed reply now see the uncertainty,
    which is the reported defect.
  • Real Hopper/Ghidra, browser, or OS coverage: none required for this decoding
    fix; no live browser produced a malformed inventory in this session.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: no new command, no new
    origin scope, no new retained content; malformed protocol data is disclosed
    rather than interpreted.

Author readiness

Human owner: @NIU-123370

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

@NIU-123370
NIU-123370 marked this pull request as ready for review October 14, 2026 15:35
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the previous coverage gap is fixed and no new blocking issue was found.

Summary

Marks unreadable worker inventories as unavailable instead of reporting a clean empty result.

  • Page capture marks unreadable worker inventories as unavailable.

Reviews (2) · Last reviewed commit: "test(browser): pin that a malformed work..." · Reviewed by Greptile

Comment thread tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts Outdated
…meration

A malformed entry followed by a valid worker is the only shape that tells a
per-entry exclusion apart from abandoning the rest of the inventory.

Refs morluto#3378.

@baron baron left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head 9126757. The worker capture producer now distinguishes absent/non-array inventory from valid empty inventory and records non-object omissions before filtering, retaining later valid workers. The completeness owner already makes invalid_protocol_value unavailable; tests assert both survivor and omission and preserve the valid-empty control. No blocker found within this focused page-inventory fix. Isolated matching-lock snapshot with pinned Node 24.18.0: cdpWorkerAttachmentProtocol.test.ts passed 6/6. This is controlled CDP evidence, not a new real-browser verification claim. Current unchanged head is open, mergeable and all reported checks green (conditional skips only), including CI required.

@baron baron left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supersedes my earlier approval: a follow-up malformed-value probe confirms directly related producer-boundary cleanup is incomplete. The current six tests pass, but all four added probes for an absent/null/numeric/empty TargetInfo.type fail because worker completeness stays falsely complete. Holding this head until the remaining unknown-to-empty collapse is addressed. Reproduced in an isolated snapshot using the existing inspectWithWorkerInventory helper, controlled loopback CDP, matching locked dependencies and pinned Node 24.18.0 (6 existing passed; 4 probes failed); no live-browser claim.

completeness.exclude("workers", "invalid_protocol_value");
continue;
}
const type = cdpStringValue(target.type) ?? "";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve uncertainty when a record has an undecodable target type. The new loop rejects non-object entries, but cdpStringValue(target.type) ?? "" still turns a record-shaped malformed entry into a definite non-worker at if (!type.includes("worker")) continue. For example, return targetInfos: [{ targetId: "worker-1", url: origin + "/worker.js", openerId: "allowed-page", attached: true }] through the existing helper: the inspection returns workers: [] and no worker unavailable/exclusion marker, just like the false-complete inventory this PR fixes. Null, numeric and empty type values behave identically. A missing/undecodable required discriminator cannot establish that an entry is not a worker. Validate that discriminator at this owning boundary before the non-worker filter; record invalid_protocol_value for undecodable entries, retain later valid entries, and cover malformed-record plus valid-non-worker/empty controls. This is the same directly related unknown-to-empty representation issue, not a request to broaden validation to unrelated TargetInfo fields.

@morluto

morluto commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Compared this PR with main fc712b375 during the fixed/superseded backlog audit.

This uncertainty fix is still absent on main. The current worker inventory path converts missing/non-list targetInfos and filters malformed records in a way that can make an undecodable inventory look empty; command rejection is handled separately.

Please keep missing, malformed, explicitly empty and valid inventories distinct at the producing CDP parser, with partial facts and coverage passed to consumers. Coordinate with #3850/#3901 so worker/resource inventory admission has one owner rather than parallel repairs. #3378 remains actionable.

PR head reviewed: 9126757b5. This is a source comparison; no new PR-branch tests or provider runs were performed for this comment.

@CassieFox23

Copy link
Copy Markdown
Contributor

TL;DR: The PR's fix works for the cases it names. On current main, 3 of its 4 new tests fail, and with the PR all 6 tests in the file pass. Baron's four extra probes still fail on this head, because a bad worker type leaves the list marked complete.

Thanks, NIU-123370, for the fix, and thanks, baron, for the probes. I tested head 9126757 on top of main 62bf098. The PR's changes were applied as a patch.

Results

Check Main plus the PR's tests only Main plus the whole PR
Tests in the PR's file (6) passed 3 6
Tests in the PR's file failed 3 0
Baron's 4 probes passed 0 0
  • Main alone passes its own 2 tests in this file. The PR adds 4 more.
  • Three new tests fail on main. A reply with no target list, a target list that is not a list, and a bad entry before a good one.
  • The fourth new test passes on main too. It checks that a delivered empty list stays a valid empty result.
  • With the PR, all 6 pass. Node 24.18.0 only.

Baron's probes

Probe (TargetInfo.type is...) On main On this head
absent fails still fails
null fails still fails
numeric (42) fails still fails
empty string fails still fails
  • In all four cases the head returns no workers and an empty exclusion list. The report still calls the worker list complete.
How the probes work

Baron reported four probes where TargetInfo.type is absent, null, numeric or empty. His probe code is not posted. I wrote my own version, and it is a reading of his report. Each probe sends one worker target through the PR's helper with the broken type. It expects a note that the worker list is not complete.

The cause is in src/browser/CdpPageCaptureWorkers.ts. A missing or non-string type becomes an empty string. The code then skips the entry without a note. So baron's finding holds on this head.

My probe file is in the draft folder as logs/baronProbes.test.ts. It is not part of the PR.

Checks and scope

  • The tests use a fake CDP browser on 127.0.0.1. That is a real local network socket, and nothing leaves the machine.
  • Real CI ran on this head. It ran 18 checks and all 18 passed. Another 16 were skipped.
  • morluto's audit comment asks for four kinds of inventory to stay apart: missing, bad, empty and valid. The four probes fit that request.

Not run

Commands
git -C ~/code/rea-mirror/rea.git fetch origin
git -C ~/code/rea-mirror/rea.git archive 62bf09809ef7bb224028916424f8f07652a20f4f | tar -x -C /tmp/t17-1015/main
# npm ci, Node 24.18.0, exit 0
npm run test:focused -- tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts   # main alone: 2 passed
# Before: apply only the PR's test diff onto main
git apply --include='tests/*' 3513.patch
npm run test:focused -- tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts   # 3 failed, 3 passed
# Probes on main, then After: apply the code change
npm run test:focused -- tests/boundary/browser/baronProbes.test.ts                   # 4 failed
git apply --exclude='tests/*' 3513.patch
npm run test:focused -- tests/boundary/browser/cdpWorkerAttachmentProtocol.test.ts   # 6 passed
npm run test:focused -- tests/boundary/browser/baronProbes.test.ts                   # 4 failed

I grepped the PR's test file and the fake CDP browser fixture for doctor first. There were no hits. The patch is the diff from the merge base to the head.

Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0
  • Head: 9126757
  • Base: 62bf098
  • Other: the box was shared with other test agents.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Malformed worker inventories are reported as empty

4 participants