[GH-ISSUE #3379] [Bug] Electron inspection reports malformed CDP inventories as complete empty results #1247

Closed
opened 2026-10-16 21:03:19 +00:00 by bsakaguchi · 2 comments
Owner

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

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

inspect-electron-page converts missing or invalid CDP collection arrays into empty inventories while reporting complete_within_window. Malformed successful replies for DOM snapshots, resource inventories, and worker discovery each produce a successful result with no exclusion or unavailable section. A caller cannot distinguish these unreadable replies from evidence that the corresponding collection is empty.

All four public CLI calls exit with code 0. The valid control returns one DOM node, one resource, and one worker. Each malformed mode changes only the affected collection to an empty result.

Mode DOM nodes Resources Workers Completeness status
control 1 1 1 complete_within_window
malformed-dom 0 1 1 complete_within_window
malformed-resources 1 0 1 complete_within_window
malformed-workers 1 1 0 complete_within_window

Every run reports unavailable_sections: [], excluded: [], and zero dropped events. No real Electron instance emitting these malformed successful replies was established; the observed defect is at the public provider's protocol-admission boundary.

Reproduction

  1. Create a clean checkout at the reproduced baseline, install the locked dependencies, and build the runtime. These are setup instructions; the reported reproduction used dependencies and a runtime already available at this baseline.
git clone https://github.com/morluto/rea.git rea-repro
cd rea-repro
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. From the repository root, create a directory named sample-observation and save the following as sample-observation/sample-repro.mjs. It creates every required input file inline, starts a local HTTP/WebSocket CDP fixture, and isolates the CLI home directory. It requires no installed browser or Electron application.
mkdir -p sample-observation
cat > sample-observation/sample-repro.mjs <<'JS'
import {createServer} from 'node:http';
import {writeFile, mkdir} from 'node:fs/promises';
import {pathToFileURL,fileURLToPath} from 'node:url';
import {dirname,join} from 'node:path';
import {createRequire} from 'node:module';
import {spawn} from 'node:child_process';
const checkout=process.argv[2];
if(!checkout)throw new Error('Supply the absolute REA checkout path');
const root=dirname(fileURLToPath(import.meta.url));
const {WebSocketServer}=createRequire(join(checkout,'package.json'))('ws');
await mkdir(root+'/home',{recursive:true});
await writeFile(root+'/index.html','<script src="app.js"></script>');
await writeFile(root+'/app.js','globalThis.marker=1;');
const page=pathToFileURL(root+'/index.html').href;
const script=pathToFileURL(root+'/app.js').href;
let mode='control';
const http=createServer((req,res)=>{
 res.setHeader('content-type','application/json');
 const port=http.address().port;
 res.end(JSON.stringify(req.url==='/json/version'?{Browser:'FakeChrome/1.0','Protocol-Version':'1.3','User-Agent':'FakeChrome','V8-Version':'13','WebKit-Version':'fake',webSocketDebuggerUrl:`ws://127.0.0.1:${port}/devtools/browser/fake`}:[{id:'page',type:'page',url:page,title:'fixture',attached:false}]));
});
const wss=new WebSocketServer({server:http});
wss.on('connection',socket=>socket.on('message',raw=>{
 const c=JSON.parse(String(raw));
 const event=(method,params)=>socket.send(JSON.stringify({method,params,sessionId:c.sessionId}));
 let result={};
 if(c.method==='Target.attachToTarget')result={sessionId:'session'};
 if(c.method==='Page.getFrameTree')result={frameTree:{frame:{id:'main',url:page}}};
 if(c.method==='Page.getResourceTree')result={frameTree:{frame:{id:'main',url:page},resources:mode==='malformed-resources'?null:[{url:script,type:'Script',mimeType:'text/javascript',contentSize:20}]}};
 if(c.method==='DOMSnapshot.captureSnapshot')result=mode==='malformed-dom'?{}:{strings:[page,'#document',''],documents:[{documentURL:0,title:2,baseURL:0,contentLanguage:2,encodingName:2,publicId:2,systemId:2,frameId:'main',nodes:{nodeType:[9],nodeName:[1],nodeValue:[2],parentIndex:[-1],attributes:[[]],backendNodeId:[1]},layout:{nodeIndex:[],styles:[],bounds:[],text:[],stackingContexts:{index:[]}},textBoxes:{layoutIndex:[],bounds:[],start:[],length:[]}}]};
 if(c.method==='Target.getTargets')result=mode==='malformed-workers'?{targetInfos:null}:{targetInfos:[{targetId:'worker-1',type:'worker',title:'sample worker',url:script,attached:false,openerId:'page',parentFrameId:'main',canAccessOpener:false}]};
 if(c.method==='Debugger.getScriptSource')result={scriptSource:'globalThis.marker=1;'};
 if(c.method==='Runtime.enable')event('Runtime.executionContextCreated',{context:{id:1,origin:'file://',name:'',uniqueId:'context-1',auxData:{frameId:'main',isDefault:true,type:'default'}}});
 if(c.method==='Debugger.enable'){
  event('Debugger.scriptParsed',{scriptId:'script-1',url:script,startLine:0,startColumn:0,endLine:0,endColumn:20,hash:'sample-hash',length:20,isModule:false,executionContextId:1,scriptLanguage:'JavaScript'});
  if(mode==='destroyed-context')event('Runtime.executionContextDestroyed',{executionContextId:1,executionContextUniqueId:'context-1'});
 }
 socket.send(JSON.stringify({id:c.id,sessionId:c.sessionId,result}));
}));
await new Promise(r=>http.listen(0,'127.0.0.1',r));
const endpoint=`http://127.0.0.1:${http.address().port}`;
try {
 for(mode of ['control','malformed-dom','malformed-resources','malformed-workers']){
  const {code,stdout,stderr}=await new Promise(resolve=>{
   const p=spawn(process.execPath,['scripts/rea.mjs','inspect-electron-page',endpoint,'page','--observation-ms','100','--include-script-sources','--json'],{cwd:checkout,env:{...process.env,HOME:root+'/home'}});
   let stdout='',stderr='';p.stdout.on('data',x=>stdout+=x);p.stderr.on('data',x=>stderr+=x);p.on('exit',code=>resolve({code,stdout,stderr}));
  });
  await writeFile(root+'/'+mode+'.json',stdout);
  const output=JSON.parse(stdout);
  const x=output.normalized_result;
  console.log(JSON.stringify({mode,exit_code:code,frame_id:x?.scripts.items[0]?.frame_id,script_key:x?.scripts.items[0]?.script_key,dom_nodes:x?.dom.total_nodes,resources:x?.resources.length,workers:x?.workers.length,completeness:x?.completeness}));
 }
}finally{
 for(const client of wss.clients)client.terminate();
 await new Promise(r=>wss.close(r)); await new Promise(r=>http.close(r));
}
JS
  1. Run the fixture. It invokes the public CLI once with valid collection replies and once for each malformed reply, saves the results in the fixture directory, and closes its server and sockets.
node sample-observation/sample-repro.mjs "$PWD"

Each run calls node scripts/rea.mjs inspect-electron-page ENDPOINT page --observation-ms 100 --include-script-sources --json with a fixture-selected literal loopback endpoint and an isolated home directory. The malformed modes independently replace one successful CDP reply: the DOM snapshot becomes {}, the resource tree has resources: null, or worker discovery has targetInfos: null. The other collections and target-validation replies remain unchanged.

Expected behavior

An invalid or missing required collection must remain unknown or produce an actionable protocol failure. If REA retains a partial result, its completeness must identify the affected section. A valid empty array may represent an empty collection; null or absent required arrays must not provide that same evidence.

docs/tool-design.md:51-55 requires missing coverage to remain unknown rather than absence, and docs/tool-design.md:77-86 requires completeness derived from examined records and verification of malformed outcomes. docs/electron-observation.md:51-56 promises explicit completeness for the normalized Electron inventories.

Impact and scope

Impact level: P2. Assessment status: supported. Three unreadable producer replies were observed to become successful complete empty results, which can mislead analysis about the presence of DOM nodes, resources, or workers. The verified scope is malformed successful CDP replies, not normal conforming Electron responses.

Workaround status: unverified. A caller could inspect raw CDP replies separately, but no existing REA option was established that restores accurate completeness for the same malformed replies. Retrying without changing the replies does not restore the missing distinction.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1. Architecture was not recorded for this reproduction.

Target and artifact context

A local HTTP/WebSocket CDP fixture advertises a selected page backed by ordinary local HTML and JavaScript files. It returns one authorized frame and script plus a DOM node, resource, and worker in the valid control. It independently changes the successful DOM, resource, or worker collection reply for each malformed mode. This reproduction uses the public CLI and simulated producer replies; no real Electron producer was established.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The public CLI matrix above is the primary violation evidence. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete.

Mode                 Exit code  DOM nodes  Resources  Workers  Completeness status
control              0          1          1          1        complete_within_window
malformed-dom        0          0          1          1        complete_within_window
malformed-resources  0          1          0          1        complete_within_window
malformed-workers    0          1          1          0        complete_within_window
Every run: unavailable_sections: []; excluded: []; zero dropped events.

Typed outcome or recovery expected

Run the valid control and each malformed mode independently. The control should keep all three observed items. Each malformed mode must fail with a protocol diagnostic or identify its collection as unavailable or excluded with partial completeness. Also run valid empty-array controls to verify that genuinely empty inventories remain distinguishable from unreadable replies. Preserving unrelated observed sections is acceptable; claiming complete coverage for an unreadable section is the failing signal. The valid empty-array controls are restoration checks, not additional reported observations.

Analysis or suspected cause

Source-derived reasoning: src/browser/CdpElectronInspection.ts:285 silently normalizes invalid resource arrays; src/browser/CdpElectronInspection.ts:329-345 does the same for missing DOM arrays; src/browser/CdpElectronWorkers.ts:42 normalizes invalid worker target arrays. These paths explain why the independently malformed successful replies in the executed public CLI matrix become complete empty inventories. The source locators explain the observed boundary; the public CLI fixture result establishes the violation.

Additional context

Related completed issue #2556 concerns malformed child-frame IDs classified as scope exclusions; this report concerns malformed DOM, resource, and worker collection arrays returning complete empty inventories.

Related completed issue #2660 concerns malformed cache enumeration reported with complete storage fingerprints; this report concerns the separate Electron DOM/resource/worker collectors.

Related open issue #3172 concerns unavailable redirect-response fields treated as known evidence during web-capture comparison; this report concerns completeness at Electron collection admission.

Related open issue #3192 concerns false absence load states for ambiguous static script candidates during runtime reconciliation; this report concerns malformed CDP replies before reconciliation.

Related open issue #3316 concerns an alternate asset match hiding a mapped-file digest mismatch; this report concerns malformed collection replies reported as complete empties.

Related completed issue #1500 concerns script-to-frame attribution after execution-context mapping replacement; this report concerns collection completeness independent of script attribution.

Related open feature request #883 requests website worker source and lifecycle attribution; this report concerns inaccurate completeness when Electron worker enumeration supplies no valid collection.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3379 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior `inspect-electron-page` converts missing or invalid CDP collection arrays into empty inventories while reporting `complete_within_window`. Malformed successful replies for DOM snapshots, resource inventories, and worker discovery each produce a successful result with no exclusion or unavailable section. A caller cannot distinguish these unreadable replies from evidence that the corresponding collection is empty. All four public CLI calls exit with code `0`. The valid control returns one DOM node, one resource, and one worker. Each malformed mode changes only the affected collection to an empty result. | Mode | DOM nodes | Resources | Workers | Completeness status | | --- | --- | --- | --- | --- | | `control` | 1 | 1 | 1 | `complete_within_window` | | `malformed-dom` | 0 | 1 | 1 | `complete_within_window` | | `malformed-resources` | 1 | 0 | 1 | `complete_within_window` | | `malformed-workers` | 1 | 1 | 0 | `complete_within_window` | Every run reports `unavailable_sections: []`, `excluded: []`, and zero dropped events. No real Electron instance emitting these malformed successful replies was established; the observed defect is at the public provider's protocol-admission boundary. ### Reproduction 1. Create a clean checkout at the reproduced baseline, install the locked dependencies, and build the runtime. These are setup instructions; the reported reproduction used dependencies and a runtime already available at this baseline. ```sh git clone https://github.com/morluto/rea.git rea-repro cd rea-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. From the repository root, create a directory named `sample-observation` and save the following as `sample-observation/sample-repro.mjs`. It creates every required input file inline, starts a local HTTP/WebSocket CDP fixture, and isolates the CLI home directory. It requires no installed browser or Electron application. ```sh mkdir -p sample-observation cat > sample-observation/sample-repro.mjs <<'JS' import {createServer} from 'node:http'; import {writeFile, mkdir} from 'node:fs/promises'; import {pathToFileURL,fileURLToPath} from 'node:url'; import {dirname,join} from 'node:path'; import {createRequire} from 'node:module'; import {spawn} from 'node:child_process'; const checkout=process.argv[2]; if(!checkout)throw new Error('Supply the absolute REA checkout path'); const root=dirname(fileURLToPath(import.meta.url)); const {WebSocketServer}=createRequire(join(checkout,'package.json'))('ws'); await mkdir(root+'/home',{recursive:true}); await writeFile(root+'/index.html','<script src="app.js"></script>'); await writeFile(root+'/app.js','globalThis.marker=1;'); const page=pathToFileURL(root+'/index.html').href; const script=pathToFileURL(root+'/app.js').href; let mode='control'; const http=createServer((req,res)=>{ res.setHeader('content-type','application/json'); const port=http.address().port; res.end(JSON.stringify(req.url==='/json/version'?{Browser:'FakeChrome/1.0','Protocol-Version':'1.3','User-Agent':'FakeChrome','V8-Version':'13','WebKit-Version':'fake',webSocketDebuggerUrl:`ws://127.0.0.1:${port}/devtools/browser/fake`}:[{id:'page',type:'page',url:page,title:'fixture',attached:false}])); }); const wss=new WebSocketServer({server:http}); wss.on('connection',socket=>socket.on('message',raw=>{ const c=JSON.parse(String(raw)); const event=(method,params)=>socket.send(JSON.stringify({method,params,sessionId:c.sessionId})); let result={}; if(c.method==='Target.attachToTarget')result={sessionId:'session'}; if(c.method==='Page.getFrameTree')result={frameTree:{frame:{id:'main',url:page}}}; if(c.method==='Page.getResourceTree')result={frameTree:{frame:{id:'main',url:page},resources:mode==='malformed-resources'?null:[{url:script,type:'Script',mimeType:'text/javascript',contentSize:20}]}}; if(c.method==='DOMSnapshot.captureSnapshot')result=mode==='malformed-dom'?{}:{strings:[page,'#document',''],documents:[{documentURL:0,title:2,baseURL:0,contentLanguage:2,encodingName:2,publicId:2,systemId:2,frameId:'main',nodes:{nodeType:[9],nodeName:[1],nodeValue:[2],parentIndex:[-1],attributes:[[]],backendNodeId:[1]},layout:{nodeIndex:[],styles:[],bounds:[],text:[],stackingContexts:{index:[]}},textBoxes:{layoutIndex:[],bounds:[],start:[],length:[]}}]}; if(c.method==='Target.getTargets')result=mode==='malformed-workers'?{targetInfos:null}:{targetInfos:[{targetId:'worker-1',type:'worker',title:'sample worker',url:script,attached:false,openerId:'page',parentFrameId:'main',canAccessOpener:false}]}; if(c.method==='Debugger.getScriptSource')result={scriptSource:'globalThis.marker=1;'}; if(c.method==='Runtime.enable')event('Runtime.executionContextCreated',{context:{id:1,origin:'file://',name:'',uniqueId:'context-1',auxData:{frameId:'main',isDefault:true,type:'default'}}}); if(c.method==='Debugger.enable'){ event('Debugger.scriptParsed',{scriptId:'script-1',url:script,startLine:0,startColumn:0,endLine:0,endColumn:20,hash:'sample-hash',length:20,isModule:false,executionContextId:1,scriptLanguage:'JavaScript'}); if(mode==='destroyed-context')event('Runtime.executionContextDestroyed',{executionContextId:1,executionContextUniqueId:'context-1'}); } socket.send(JSON.stringify({id:c.id,sessionId:c.sessionId,result})); })); await new Promise(r=>http.listen(0,'127.0.0.1',r)); const endpoint=`http://127.0.0.1:${http.address().port}`; try { for(mode of ['control','malformed-dom','malformed-resources','malformed-workers']){ const {code,stdout,stderr}=await new Promise(resolve=>{ const p=spawn(process.execPath,['scripts/rea.mjs','inspect-electron-page',endpoint,'page','--observation-ms','100','--include-script-sources','--json'],{cwd:checkout,env:{...process.env,HOME:root+'/home'}}); let stdout='',stderr='';p.stdout.on('data',x=>stdout+=x);p.stderr.on('data',x=>stderr+=x);p.on('exit',code=>resolve({code,stdout,stderr})); }); await writeFile(root+'/'+mode+'.json',stdout); const output=JSON.parse(stdout); const x=output.normalized_result; console.log(JSON.stringify({mode,exit_code:code,frame_id:x?.scripts.items[0]?.frame_id,script_key:x?.scripts.items[0]?.script_key,dom_nodes:x?.dom.total_nodes,resources:x?.resources.length,workers:x?.workers.length,completeness:x?.completeness})); } }finally{ for(const client of wss.clients)client.terminate(); await new Promise(r=>wss.close(r)); await new Promise(r=>http.close(r)); } JS ``` 3. Run the fixture. It invokes the public CLI once with valid collection replies and once for each malformed reply, saves the results in the fixture directory, and closes its server and sockets. ```bash node sample-observation/sample-repro.mjs "$PWD" ``` Each run calls `node scripts/rea.mjs inspect-electron-page ENDPOINT page --observation-ms 100 --include-script-sources --json` with a fixture-selected literal loopback endpoint and an isolated home directory. The malformed modes independently replace one successful CDP reply: the DOM snapshot becomes `{}`, the resource tree has `resources: null`, or worker discovery has `targetInfos: null`. The other collections and target-validation replies remain unchanged. ### Expected behavior An invalid or missing required collection must remain unknown or produce an actionable protocol failure. If REA retains a partial result, its completeness must identify the affected section. A valid empty array may represent an empty collection; `null` or absent required arrays must not provide that same evidence. `docs/tool-design.md:51-55` requires missing coverage to remain unknown rather than absence, and `docs/tool-design.md:77-86` requires completeness derived from examined records and verification of malformed outcomes. `docs/electron-observation.md:51-56` promises explicit completeness for the normalized Electron inventories. ### Impact and scope Impact level: P2. Assessment status: supported. Three unreadable producer replies were observed to become successful complete empty results, which can mislead analysis about the presence of DOM nodes, resources, or workers. The verified scope is malformed successful CDP replies, not normal conforming Electron responses. Workaround status: unverified. A caller could inspect raw CDP replies separately, but no existing REA option was established that restores accurate completeness for the same malformed replies. Retrying without changing the replies does not restore the missing distinction. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1. Architecture was not recorded for this reproduction. ### Target and artifact context A local HTTP/WebSocket CDP fixture advertises a selected page backed by ordinary local HTML and JavaScript files. It returns one authorized frame and script plus a DOM node, resource, and worker in the valid control. It independently changes the successful DOM, resource, or worker collection reply for each malformed mode. This reproduction uses the public CLI and simulated producer replies; no real Electron producer was established. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The public CLI matrix above is the primary violation evidence. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete. ```text Mode Exit code DOM nodes Resources Workers Completeness status control 0 1 1 1 complete_within_window malformed-dom 0 0 1 1 complete_within_window malformed-resources 0 1 0 1 complete_within_window malformed-workers 0 1 1 0 complete_within_window Every run: unavailable_sections: []; excluded: []; zero dropped events. ``` ### Typed outcome or recovery expected Run the valid control and each malformed mode independently. The control should keep all three observed items. Each malformed mode must fail with a protocol diagnostic or identify its collection as unavailable or excluded with partial completeness. Also run valid empty-array controls to verify that genuinely empty inventories remain distinguishable from unreadable replies. Preserving unrelated observed sections is acceptable; claiming complete coverage for an unreadable section is the failing signal. The valid empty-array controls are restoration checks, not additional reported observations. ### Analysis or suspected cause Source-derived reasoning: `src/browser/CdpElectronInspection.ts:285` silently normalizes invalid resource arrays; `src/browser/CdpElectronInspection.ts:329-345` does the same for missing DOM arrays; `src/browser/CdpElectronWorkers.ts:42` normalizes invalid worker target arrays. These paths explain why the independently malformed successful replies in the executed public CLI matrix become complete empty inventories. The source locators explain the observed boundary; the public CLI fixture result establishes the violation. ### Additional context Related completed issue [#2556](https://github.com/morluto/rea/issues/2556) concerns malformed child-frame IDs classified as scope exclusions; this report concerns malformed DOM, resource, and worker collection arrays returning complete empty inventories. Related completed issue [#2660](https://github.com/morluto/rea/issues/2660) concerns malformed cache enumeration reported with complete storage fingerprints; this report concerns the separate Electron DOM/resource/worker collectors. Related open issue [#3172](https://github.com/morluto/rea/issues/3172) concerns unavailable redirect-response fields treated as known evidence during web-capture comparison; this report concerns completeness at Electron collection admission. Related open issue [#3192](https://github.com/morluto/rea/issues/3192) concerns false absence load states for ambiguous static script candidates during runtime reconciliation; this report concerns malformed CDP replies before reconciliation. Related open issue [#3316](https://github.com/morluto/rea/issues/3316) concerns an alternate asset match hiding a mapped-file digest mismatch; this report concerns malformed collection replies reported as complete empties. Related completed issue [#1500](https://github.com/morluto/rea/issues/1500) concerns script-to-frame attribution after execution-context mapping replacement; this report concerns collection completeness independent of script attribution. Related open feature request [#883](https://github.com/morluto/rea/issues/883) requests website worker source and lifecycle attribution; this report concerns inaccurate completeness when Electron worker enumeration supplies no valid collection.
Author
Owner

@YunzhouLi-hub commented on GitHub (Oct 15, 2026):

我来承接这个。它和 #3376 是同一个产生边界,我会合成一个 PR 一起修(CONTRIBUTING.md 的 "Choosing a contribution" 要求 keep related corrections together)。

根因

src/browser/CdpCaptureValues.ts:15-21 的 recordsValue 是一个全函数,把三种在 CDP 协议上完全不同的生产者状态塌缩成了同一个 []:

CDP 实际回复 recordsValue 返回 语义
字段是 [] [] 生产者说确实是空的(合法)
字段缺失或不是数组 [] 生产者省略了字段(未知)
字段是 [null] [] 生产者返回了畸形条目(未知,且内容已丢失)

Electron 侧受影响的四条路径,全在 src/browser/CdpElectronInspection.ts 和 src/browser/CdpElectronWorkers.ts:

  • DOM:CdpElectronInspection.ts:329-331 — Array.isArray(result.strings) ? ... : [];:334 — recordsValue(result.documents)
  • resources::285 — recordsValue(tree.resources);:310 — recordsValue(tree.childFrames)(子帧丢失还会连带丢掉整棵子树)
  • workers:CdpElectronWorkers.ts:42 — recordsValue(recordValue(result)?.targetInfos)

因为 CdpCaptureCompleteness 的记账簿从来没被通知,unavailable_sections、excluded、dropped_events 全是零,src/domain/browserCompleteness.ts:105-111 于是落到 complete_within_window。调用方无法把这些不可读的回复和「该集合确实为空」的证据区分开。

为什么这是产生边界而不是下游打补丁

仓库里已经有正确的惯用法,只是这几条路径没走到:CdpCaptureCompleteness.unavailable(section) / exclude(section, reason),以及 src/domain/browserCompleteness.ts:104 已经正确地把 unavailableSections 映射成 attach_limited。全仓库有 72 处调用这个惯用法,包括同族的 CdpPageCaptureWorkers.ts:33(if (result === undefined) completeness.unavailable("workers"))和 CdpElectronWorkers.ts:38。只有 CDP 命令成功返回、而字段本身畸形的路径在静默塌缩。

recordsValue 有 24 个调用点、跨 10 个文件(CdpElectronInspection、CdpCaptureStorage、CdpCaptureStorageFingerprints、CdpCaptureEventHandlers、CdpCaptureDocuments、CdpWebMcpDiscovery、CdpPageCaptureWorkers、CdpElectronWorkers、CdpCaptureEventHelpers),所以这是一个被广泛复用的语义漏洞,不是单点遗漏。

计划

结构性修法:让 recordsValue 把准入结果报出来,使调用方能区分「合法空数组」和「畸形/缺失」。具体是加一个 admitRecords(value),对缺失/非数组返回 undefined(未知)、对合法空数组返回 [],然后把确有记账需求的调用点路由到 completeness.unavailable(section)。我会逐个调用点确认 blast radius,其余保持现状。

验证

用 issue 里的 loopback HTTP/WebSocket CDP fixture(不需要真实 Electron)覆盖 control 和三种畸形模式:malformed-dom、malformed-resources、malformed-workers。断言每种畸形模式下对应 section 出现在 unavailable_sections 且 excluded 非空,status 不再是 complete_within_window,同时 control 仍是一个 DOM 节点、一个 resource、一个 worker。本地跑 npm run test:local、npm run check:fast、npm run verify:test-discovery,并在回复里报告实测结果和任何未覆盖的部分。

我基于 upstream/main 4135c911d 读的源码,上面的行号对应该 revision。开工前会先 rebase 到最新 main。

<!-- gh-comment-id:6184416207 --> @YunzhouLi-hub commented on GitHub (Oct 15, 2026): 我来承接这个。它和 #3376 是同一个产生边界,我会合成一个 PR 一起修(CONTRIBUTING.md 的 "Choosing a contribution" 要求 keep related corrections together)。 ## 根因 ``src/browser/CdpCaptureValues.ts:15-21`` 的 ``recordsValue`` 是一个全函数,把三种在 CDP 协议上完全不同的生产者状态塌缩成了同一个 ``[]``: | CDP 实际回复 | recordsValue 返回 | 语义 | | --- | --- | --- | | 字段是 ``[]`` | ``[]`` | 生产者说确实是空的(合法) | | 字段缺失或不是数组 | ``[]`` | 生产者省略了字段(未知) | | 字段是 ``[null]`` | ``[]`` | 生产者返回了畸形条目(未知,且内容已丢失) | Electron 侧受影响的四条路径,全在 ``src/browser/CdpElectronInspection.ts`` 和 ``src/browser/CdpElectronWorkers.ts``: - DOM:``CdpElectronInspection.ts:329-331`` — ``Array.isArray(result.strings) ? ... : []``;``:334`` — ``recordsValue(result.documents)`` - resources:``:285`` — ``recordsValue(tree.resources)``;``:310`` — ``recordsValue(tree.childFrames)``(子帧丢失还会连带丢掉整棵子树) - workers:``CdpElectronWorkers.ts:42`` — ``recordsValue(recordValue(result)?.targetInfos)`` 因为 ``CdpCaptureCompleteness`` 的记账簿从来没被通知,``unavailable_sections``、``excluded``、``dropped_events`` 全是零,``src/domain/browserCompleteness.ts:105-111`` 于是落到 ``complete_within_window``。调用方无法把这些不可读的回复和「该集合确实为空」的证据区分开。 ## 为什么这是产生边界而不是下游打补丁 仓库里已经有正确的惯用法,只是这几条路径没走到:``CdpCaptureCompleteness.unavailable(section)`` / ``exclude(section, reason)``,以及 ``src/domain/browserCompleteness.ts:104`` 已经正确地把 ``unavailableSections`` 映射成 ``attach_limited``。全仓库有 72 处调用这个惯用法,包括同族的 ``CdpPageCaptureWorkers.ts:33``(``if (result === undefined) completeness.unavailable("workers")``)和 ``CdpElectronWorkers.ts:38``。只有 CDP 命令成功返回、而字段本身畸形的路径在静默塌缩。 ``recordsValue`` 有 24 个调用点、跨 10 个文件(CdpElectronInspection、CdpCaptureStorage、CdpCaptureStorageFingerprints、CdpCaptureEventHandlers、CdpCaptureDocuments、CdpWebMcpDiscovery、CdpPageCaptureWorkers、CdpElectronWorkers、CdpCaptureEventHelpers),所以这是一个被广泛复用的语义漏洞,不是单点遗漏。 ## 计划 结构性修法:让 ``recordsValue`` 把准入结果报出来,使调用方能区分「合法空数组」和「畸形/缺失」。具体是加一个 ``admitRecords(value)``,对缺失/非数组返回 ``undefined``(未知)、对合法空数组返回 ``[]``,然后把确有记账需求的调用点路由到 ``completeness.unavailable(section)``。我会逐个调用点确认 blast radius,其余保持现状。 ## 验证 用 issue 里的 loopback HTTP/WebSocket CDP fixture(不需要真实 Electron)覆盖 control 和三种畸形模式:malformed-dom、malformed-resources、malformed-workers。断言每种畸形模式下对应 section 出现在 ``unavailable_sections`` 且 ``excluded`` 非空,``status`` 不再是 ``complete_within_window``,同时 control 仍是一个 DOM 节点、一个 resource、一个 worker。本地跑 ``npm run test:local``、``npm run check:fast``、``npm run verify:test-discovery``,并在回复里报告实测结果和任何未覆盖的部分。 我基于 upstream/main ``4135c911d`` 读的源码,上面的行号对应该 revision。开工前会先 rebase 到最新 main。
Author
Owner

@YunzhouLi-hub commented on GitHub (Oct 15, 2026):

PR 已提交:https://github.com/morluto/rea/pull/3850

基于 upstream/main 788864d52 重新读源码并 rebase 后实施。先修正我承接评论里的两个数字:recordsValue 实际是 19 个调用点、跨 9 个文件(我先前写成 24/10);同族惯用法最贴近的实例是 CdpCaptureDocuments.ts:105-114 的 web 侧 captureResources,它已经做了完全正确的事,我照着它做。

实际采用的修法

没有保留 recordsValue 作为别名,而是换成两个导出:

  • admitRecords(value) 对缺失/非数组返回 undefined(未知),只有 present array 携带生产者证据;
  • admittedRecords(value) 保留原来的逐条 record 过滤,给不需要记账的调用方用,文档字符串明说畸形条目被无声丢弃、需要记账的调用方应改用 admitRecords + 逐条 recordValue。

留着 recordsValue 就是留着这个陷阱默认值给下一个调用方(AGENTS.md 的 "Remove superseded representations, aliases, wrappers")。

这个 issue 里的四条路径

路径 处理
CdpElectronInspection.ts tree.resources admitRecords + 逐条 recordValue,缺失/非数组 → exclude("resources", "invalid_protocol_value", null)
CdpElectronInspection.ts tree.childFrames 删掉原来手写的 BFS while 循环,改用仓库已有的 walkFrameTrees,它本来就带畸形子帧记账回调
CdpElectronInspection.ts result.strings admitRecords,缺失 → exclude("dom", "invalid_protocol_value", null) 并直接返回空 DOM
CdpElectronInspection.ts result.documents 同上

strings 缺失时直接停掉是有意的:没有字符串表时 documentURL 索引无法解析,继续跑会把每篇文档误归因为 out_of_target_scope(policy 原因,进 policy_filtered_sections 而不是 unavailable_sections),是错误归因。子帧丢失连带丢整棵子树的问题,随 walkFrameTrees 的记账回调一起解决。

另外 CdpElectronWorkers.ts:42 的 targetInfos 和 web 侧 CdpPageCaptureWorkers.ts:34 是同一个 Target.getTargets 缺陷的跨适配器复制,只修一个会留下另一个陷阱,所以两个都修了(都进 workers section 的记账)。

剩余的、这个 PR 没做的

有 13 个调用点没有可达的记账簿,我没有改它们,只在 PR 正文里报告为 remaining cleanup:

  • CdpCaptureDocuments.ts accessibilityStates:掉一个畸形 state 会让 states 不完整,但 treeIncomplete 看不到。要报它需要新的 schema 字段,属于另一个 PR。
  • CdpCaptureStorageFingerprints.ts 8 处:该文件自己已有 Array.isArray 守卫 + cookies.length !== cookieValues.length 掉数检测。
  • CdpCaptureStorage.ts、CdpCaptureEventHelpers.ts、CdpCaptureEventHandlers.ts、CdpWebMcpDiscovery.ts。

实测

全部在 rebase 后的 base 788864d52 上跑:

命令 结果
tsc --noEmit clean
npm run check:fast 2/2 tasks successful
npm run test:local 4 个改动测试文件 47/47
npm run test:local src/browser 全部 34 个测试文件 452/452
npm run test:local -- tests/evaluation/moduleBoundaries.test.ts 113/113
npm run verify:test-discovery 1313 files, all once
npx oxfmt --check src/browser/ correct format
npm run docs:check 4/5;artifacts:managed-evidence 因 mkdtemp EPERM 失败,在无改动的干净树上同样失败,是环境问题

回归证明

把 admitRecords 原地改回旧的塌缩原语(缺失返回 [] 而不是 undefined),47 个测试里 12 个失败。其中覆盖这个 issue 的是:

CdpElectronInspection.test.ts > reports a frame whose resource inventory is missing rather than empty CdpElectronInspection.test.ts > reports a non-array resource inventory rather than empty CdpElectronInspection.test.ts > keeps a child frame's authorized resources after its parent answered CdpElectronInspection.test.ts > reports a snapshot without its string table rather than empty names CdpElectronInspection.test.ts > reports a snapshot whose documents field is not an array CdpCaptureWorkers.test.ts > Electron: reports a missing target inventory as unavailable rather than no workers

代表性断言差异(base revision):

`

  • "excluded": [{ "count": null, "reason": "invalid_protocol_value", "section": "resources" }],
  • "unavailable_sections": ["resources"],
  • "excluded": [],
  • "unavailable_sections": [],
    `

剩下 8 个新测试两种实现下都过,这是设计如此:它们走的是逐条 recordValue 路径,base revision 本来就处理了,是用来防止过度上报的对照组。

未覆盖的部分

那 13 个无记账簿可达的调用点保持现状,没有测试锁定它们的 ledger(没有 ledger 可锁定)。没有驱动真实 Chrome/Electron 会话——loopback fixture 覆盖的是同一处 CDP 应答解释分支,与仓库现有 browser 测试的边界相同。

<!-- gh-comment-id:6184837562 --> @YunzhouLi-hub commented on GitHub (Oct 15, 2026): PR 已提交:https://github.com/morluto/rea/pull/3850 基于 upstream/main `788864d52` 重新读源码并 rebase 后实施。先修正我承接评论里的两个数字:`recordsValue` 实际是 **19 个调用点、跨 9 个文件**(我先前写成 24/10);同族惯用法最贴近的实例是 `CdpCaptureDocuments.ts:105-114` 的 web 侧 `captureResources`,它已经做了完全正确的事,我照着它做。 ## 实际采用的修法 没有保留 `recordsValue` 作为别名,而是换成两个导出: - `admitRecords(value)` 对缺失/非数组返回 `undefined`(未知),只有 present array 携带生产者证据; - `admittedRecords(value)` 保留原来的逐条 record 过滤,给不需要记账的调用方用,文档字符串明说畸形条目被无声丢弃、需要记账的调用方应改用 `admitRecords` + 逐条 `recordValue`。 留着 `recordsValue` 就是留着这个陷阱默认值给下一个调用方(AGENTS.md 的 "Remove superseded representations, aliases, wrappers")。 ## 这个 issue 里的四条路径 | 路径 | 处理 | | --- | --- | | `CdpElectronInspection.ts` `tree.resources` | `admitRecords` + 逐条 `recordValue`,缺失/非数组 → `exclude("resources", "invalid_protocol_value", null)` | | `CdpElectronInspection.ts` `tree.childFrames` | 删掉原来手写的 BFS while 循环,改用仓库已有的 `walkFrameTrees`,它本来就带畸形子帧记账回调 | | `CdpElectronInspection.ts` `result.strings` | `admitRecords`,缺失 → `exclude("dom", "invalid_protocol_value", null)` 并直接返回空 DOM | | `CdpElectronInspection.ts` `result.documents` | 同上 | `strings` 缺失时直接停掉是有意的:没有字符串表时 `documentURL` 索引无法解析,继续跑会把每篇文档误归因为 `out_of_target_scope`(policy 原因,进 `policy_filtered_sections` 而不是 `unavailable_sections`),是错误归因。子帧丢失连带丢整棵子树的问题,随 `walkFrameTrees` 的记账回调一起解决。 另外 `CdpElectronWorkers.ts:42` 的 `targetInfos` 和 web 侧 `CdpPageCaptureWorkers.ts:34` 是同一个 `Target.getTargets` 缺陷的跨适配器复制,只修一个会留下另一个陷阱,所以两个都修了(都进 `workers` section 的记账)。 ## 剩余的、这个 PR 没做的 有 13 个调用点没有可达的记账簿,我**没有**改它们,只在 PR 正文里报告为 remaining cleanup: - `CdpCaptureDocuments.ts` `accessibilityStates`:掉一个畸形 state 会让 `states` 不完整,但 `treeIncomplete` 看不到。要报它需要新的 schema 字段,属于另一个 PR。 - `CdpCaptureStorageFingerprints.ts` 8 处:该文件自己已有 `Array.isArray` 守卫 + `cookies.length !== cookieValues.length` 掉数检测。 - `CdpCaptureStorage.ts`、`CdpCaptureEventHelpers.ts`、`CdpCaptureEventHandlers.ts`、`CdpWebMcpDiscovery.ts`。 ## 实测 全部在 rebase 后的 base `788864d52` 上跑: | 命令 | 结果 | | --- | --- | | `tsc --noEmit` | clean | | `npm run check:fast` | 2/2 tasks successful | | `npm run test:local` 4 个改动测试文件 | 47/47 | | `npm run test:local` `src/browser` 全部 34 个测试文件 | 452/452 | | `npm run test:local -- tests/evaluation/moduleBoundaries.test.ts` | 113/113 | | `npm run verify:test-discovery` | 1313 files, all once | | `npx oxfmt --check src/browser/` | correct format | | `npm run docs:check` | 4/5;`artifacts:managed-evidence` 因 `mkdtemp` EPERM 失败,在无改动的干净树上同样失败,是环境问题 | ## 回归证明 把 `admitRecords` 原地改回旧的塌缩原语(缺失返回 `[]` 而不是 `undefined`),47 个测试里 **12 个失败**。其中覆盖这个 issue 的是: ` CdpElectronInspection.test.ts > reports a frame whose resource inventory is missing rather than empty CdpElectronInspection.test.ts > reports a non-array resource inventory rather than empty CdpElectronInspection.test.ts > keeps a child frame's authorized resources after its parent answered CdpElectronInspection.test.ts > reports a snapshot without its string table rather than empty names CdpElectronInspection.test.ts > reports a snapshot whose documents field is not an array CdpCaptureWorkers.test.ts > Electron: reports a missing target inventory as unavailable rather than no workers ` 代表性断言差异(base revision): ` - "excluded": [{ "count": null, "reason": "invalid_protocol_value", "section": "resources" }], - "unavailable_sections": ["resources"], + "excluded": [], + "unavailable_sections": [], ` 剩下 8 个新测试两种实现下都过,这是设计如此:它们走的是逐条 `recordValue` 路径,base revision 本来就处理了,是用来防止过度上报的对照组。 ## 未覆盖的部分 那 13 个无记账簿可达的调用点保持现状,没有测试锁定它们的 ledger(没有 ledger 可锁定)。没有驱动真实 Chrome/Electron 会话——loopback fixture 覆盖的是同一处 CDP 应答解释分支,与仓库现有 browser 测试的边界相同。
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#1247
No description provided.