[GH-ISSUE #5421] [Bug] Popup startup misses declared storage seeds while capture reports complete #3288

Open
opened 2026-10-31 00:38:48 +00:00 by bsakaguchi · 2 comments
Owner

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

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

node scripts/rea.mjs capture-browser-scenario <scenario.json> --json accepts declared popup-origin storage seeds, but a popup opened through a target="_blank" link executes without them. The cross-origin popup reads both values as null; the same-origin popup reads the shared local seed but its session value is null. The root-page control receives both seeds. All captures exit 0 and report complete, equality-eligible Evidence, with no limitation disclosing unapplied popup seeds.

Reproduction

An independent verifier ran the following production CLI fixture and replayed the exact draft fixture: two executions per case on the primary browser, with the same results. These are executed cases, not proposed tests.

Self-contained loopback fixture and three-case driver

Use the prepared baseline checkout and an installed Chromium matching the primary runtime below. Set BROWSER to its executable and EVIDENCE_DIR to an empty writable directory outside the checkout. No external site is needed. Save the following as server.mjs in that evidence directory. It binds two ephemeral ports on 127.0.0.1 and prints their origins.

import http from 'node:http';
const servers=[];
for(let i=0;i<2;i++){
 const server=http.createServer((req,res)=>{
  res.setHeader('Content-Type','text/html');
  if(req.url.startsWith('/opener')){
   const childOrigin=new URL(req.url,'http://fixture').searchParams.get('child');
   res.end(`<!doctype html><title>Root</title><a id="open" target="_blank" href="${childOrigin}/child">Open</a><script>console.log('root-first',JSON.stringify({local:localStorage.getItem('probe'),session:sessionStorage.getItem('probe')}))</script>`);
  }else if(req.url==='/child'){
   res.end(`<!doctype html><title>Child</title><script>const read=()=>({local:localStorage.getItem('probe'),session:sessionStorage.getItem('probe')});console.log('child-first',JSON.stringify(read()));setTimeout(()=>console.log('child-later',JSON.stringify(read())),500)</script>`);
  }else{res.statusCode=404;res.end('missing');}
 });
 await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));servers.push(server);
}
console.log(JSON.stringify({pid:process.pid,origins:servers.map(s=>`http://127.0.0.1:${s.address().port}`)}));
process.on('SIGTERM',()=>{for(const s of servers)s.close();});

Save the following as reproduce.py beside the server. From the baseline checkout, run python3 "$EVIDENCE_DIR/reproduce.py". The driver invokes the production CLI three times with isolated home/config/data/cache/state and temporary-profile directories, writes each exact scenario and full capture, and stops only its server PID. The capture command has no database or configuration-file path argument.

import json, os, pathlib, shutil, subprocess

work = pathlib.Path(os.environ["EVIDENCE_DIR"]).resolve()
repo = pathlib.Path.cwd()
node = shutil.which("node")
browser = os.environ["BROWSER"]
env = {"PATH": os.environ["PATH"]}
for name, directory in {
    "HOME": "home", "XDG_CONFIG_HOME": "config", "XDG_DATA_HOME": "data",
    "XDG_CACHE_HOME": "cache", "XDG_STATE_HOME": "state", "TMPDIR": "temp"
}.items():
    path = work / directory
    path.mkdir(exist_ok=True)
    env[name] = str(path)
server = subprocess.Popen(
    [node, str(work / "server.mjs")], env=env,
    stdout=subprocess.PIPE, text=True
)
try:
    origins = json.loads(server.stdout.readline())["origins"]
    for case in ["cross-popup", "same-popup", "root-control"]:
        root, child = origins
        seeded = root if case == "same-popup" else child
        actions = [
            {"step_id": "open", "action": "click", "locator": {
                "kind": "css", "selector": "#open"}, "timeout_ms": 5000},
            {"step_id": "settle", "action": "wait_for_timeout", "duration_ms": 1200}
        ]
        if case == "root-control":
            actions = actions[1:]
        entry = {"name": "probe", "value": {"source": "literal"}}
        scenario = {
            "browser": {"mode": "launch", "executable_path": browser},
            "start_url": {"url": seeded + "/child" if case == "root-control"
                else root + "/opener?child=" + seeded},
            "actions": actions,
            "storage": {
                "local_storage": [{"origin": seeded, "entries": [{
                    **entry, "value": {**entry["value"], "value": "seed-local"}}]}],
                "session_storage": [{"origin": seeded, "entries": [{
                    **entry, "value": {**entry["value"], "value": "seed-session"}}]}]
            },
            "capture": {"at_end": ["url"], "events": ["console", "popups"]}
        }
        source = work / (case + ".json")
        source.write_text(json.dumps(scenario, indent=2))
        with (work / (case + "-capture.json")).open("w") as out, \
             (work / (case + ".stderr")).open("w") as err:
            result = subprocess.run(
                [node, str(repo / "scripts/rea.mjs"), "capture-browser-scenario",
                 str(source), "--json"], cwd=repo, env=env,
                stdout=out, stderr=err, timeout=45
            )
        capture = json.loads((work / (case + "-capture.json")).read_text())
        value = capture["normalized_result"]
        print(case, "exit", result.returncode, value["browser"])
        for event in value["events"]["items"]:
            if event["kind"] == "console":
                print(event["text"])
        print(value["completeness"])
finally:
    server.terminate()
    server.wait(timeout=10)

Expected behavior

docs/browser-scenario-contract.md:195-205 promises local seeds once per browser storage partition and session seeds once per origin in each top-level browsing context, before the first document scripts. Both popup cases should read {"local":"seed-local","session":"seed-session"} in their first inline script and after the timer; the successful root control should remain successful. Lines 252-267 include discovered popups; the early-event caveat concerns frame/worker/WebSocket/download events, not storage seeding or console capture.

Impact and scope

The tested popup startup runs under different storage state from the accepted request while REA labels its Evidence complete and equality-eligible. This can misrepresent state-dependent application startup. Both executions of each primary-browser case reproduced the behavior. Capturing the child as the root loses the popup context; the supported actions do not offer a pre-script popup session-storage write. Nested popups, other opener relationships, attach mode, and broader browser/platform coverage were not tested. No credential exposure, data loss, or security-boundary crossing was shown.

REA version

6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node v26.12.0; macOS arm64; playwright-core 1.58.2; primary installed Playwright Chromium 153.0.8010.12 (build 1243).

Target and artifact context

Two loopback HTTP origins, fresh launched scenario-owned profiles, console and popup event capture; no external service, credentials, database, or configuration-file input.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The fixture's exact production invocation is node scripts/rea.mjs capture-browser-scenario <case.json> --json. The verifier observed these console records:

cross-popup:
  child-first {"local":null,"session":null}
  child-later {"local":null,"session":null}
same-popup:
  root-first  {"local":"seed-local","session":"seed-session"}
  child-first {"local":"seed-local","session":null}
  child-later {"local":"seed-local","session":null}
root-control:
  child-first {"local":"seed-local","session":"seed-session"}
  child-later {"local":"seed-local","session":"seed-session"}

Every command exited 0. Every normalized_result.completeness was:

{"status":"complete","equality_eligible":true,"missing_sections":[],"truncated_sections":[]}

Every browser result reported cleanup: "terminated-owned-process". Limitations included the general CDP storage-instrumentation explanation but did not disclose unapplied popup seeds. The passing root control isolates the popup-specific failure on this browser.

Typed outcome or recovery expected

The popup's first script must actually receive both declared seeds, and the root control must keep receiving them. Merely marking the capture incomplete would not restore the promised successful initialization. Equality eligibility should reflect the actual initialized scenario.

Analysis or suspected cause

Source-derived reasoning: src/browser/PlaywrightScenarioSession.ts:285-299 initializes storage for the opened root page. src/browser/PlaywrightScenarioStorage.ts:159,207-225,245-269,289-315 creates that page's CDP session and installs target-event-based seed instrumentation. This is consistent with popup targets missing initialization. No direct CDP attachment trace was collected; the observed CLI failure and controls establish the report independently of that interpretation.

Additional context

Prepared with AI assistance and independently reproduced through the production CLI. An earlier Chromium 145.0.7632.6 run also missed root seeds and therefore did not isolate a popup-specific defect; the report relies on Chromium 153's passing root control. No last-known-good version is established.

Upstream check: fetched origin/main at 1417bf943bf49e49b414e8a508bf28a31304be83; no post-baseline commits changed the cited contract or storage implementation. Searches included open and closed issues and PRs. #1578/#1579 concern reseeding after navigation, #4010 concerns cancellation cleanup, and #4956 concerns document-principal binding; none reports these popup cases.

Related: https://github.com/morluto/rea/issues/5420 (also declared storage seeding, different trigger)

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5421 ### 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 `node scripts/rea.mjs capture-browser-scenario <scenario.json> --json` accepts declared popup-origin storage seeds, but a popup opened through a `target="_blank"` link executes without them. The cross-origin popup reads both values as `null`; the same-origin popup reads the shared local seed but its session value is `null`. The root-page control receives both seeds. All captures exit 0 and report complete, equality-eligible Evidence, with no limitation disclosing unapplied popup seeds. ### Reproduction An independent verifier ran the following production CLI fixture and replayed the exact draft fixture: two executions per case on the primary browser, with the same results. These are executed cases, not proposed tests. <details> <summary>Self-contained loopback fixture and three-case driver</summary> Use the prepared baseline checkout and an installed Chromium matching the primary runtime below. Set `BROWSER` to its executable and `EVIDENCE_DIR` to an empty writable directory outside the checkout. No external site is needed. Save the following as `server.mjs` in that evidence directory. It binds two ephemeral ports on 127.0.0.1 and prints their origins. ```javascript import http from 'node:http'; const servers=[]; for(let i=0;i<2;i++){ const server=http.createServer((req,res)=>{ res.setHeader('Content-Type','text/html'); if(req.url.startsWith('/opener')){ const childOrigin=new URL(req.url,'http://fixture').searchParams.get('child'); res.end(`<!doctype html><title>Root</title><a id="open" target="_blank" href="${childOrigin}/child">Open</a><script>console.log('root-first',JSON.stringify({local:localStorage.getItem('probe'),session:sessionStorage.getItem('probe')}))</script>`); }else if(req.url==='/child'){ res.end(`<!doctype html><title>Child</title><script>const read=()=>({local:localStorage.getItem('probe'),session:sessionStorage.getItem('probe')});console.log('child-first',JSON.stringify(read()));setTimeout(()=>console.log('child-later',JSON.stringify(read())),500)</script>`); }else{res.statusCode=404;res.end('missing');} }); await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));servers.push(server); } console.log(JSON.stringify({pid:process.pid,origins:servers.map(s=>`http://127.0.0.1:${s.address().port}`)})); process.on('SIGTERM',()=>{for(const s of servers)s.close();}); ``` Save the following as `reproduce.py` beside the server. From the baseline checkout, run `python3 "$EVIDENCE_DIR/reproduce.py"`. The driver invokes the production CLI three times with isolated home/config/data/cache/state and temporary-profile directories, writes each exact scenario and full capture, and stops only its server PID. The capture command has no database or configuration-file path argument. ```python import json, os, pathlib, shutil, subprocess work = pathlib.Path(os.environ["EVIDENCE_DIR"]).resolve() repo = pathlib.Path.cwd() node = shutil.which("node") browser = os.environ["BROWSER"] env = {"PATH": os.environ["PATH"]} for name, directory in { "HOME": "home", "XDG_CONFIG_HOME": "config", "XDG_DATA_HOME": "data", "XDG_CACHE_HOME": "cache", "XDG_STATE_HOME": "state", "TMPDIR": "temp" }.items(): path = work / directory path.mkdir(exist_ok=True) env[name] = str(path) server = subprocess.Popen( [node, str(work / "server.mjs")], env=env, stdout=subprocess.PIPE, text=True ) try: origins = json.loads(server.stdout.readline())["origins"] for case in ["cross-popup", "same-popup", "root-control"]: root, child = origins seeded = root if case == "same-popup" else child actions = [ {"step_id": "open", "action": "click", "locator": { "kind": "css", "selector": "#open"}, "timeout_ms": 5000}, {"step_id": "settle", "action": "wait_for_timeout", "duration_ms": 1200} ] if case == "root-control": actions = actions[1:] entry = {"name": "probe", "value": {"source": "literal"}} scenario = { "browser": {"mode": "launch", "executable_path": browser}, "start_url": {"url": seeded + "/child" if case == "root-control" else root + "/opener?child=" + seeded}, "actions": actions, "storage": { "local_storage": [{"origin": seeded, "entries": [{ **entry, "value": {**entry["value"], "value": "seed-local"}}]}], "session_storage": [{"origin": seeded, "entries": [{ **entry, "value": {**entry["value"], "value": "seed-session"}}]}] }, "capture": {"at_end": ["url"], "events": ["console", "popups"]} } source = work / (case + ".json") source.write_text(json.dumps(scenario, indent=2)) with (work / (case + "-capture.json")).open("w") as out, \ (work / (case + ".stderr")).open("w") as err: result = subprocess.run( [node, str(repo / "scripts/rea.mjs"), "capture-browser-scenario", str(source), "--json"], cwd=repo, env=env, stdout=out, stderr=err, timeout=45 ) capture = json.loads((work / (case + "-capture.json")).read_text()) value = capture["normalized_result"] print(case, "exit", result.returncode, value["browser"]) for event in value["events"]["items"]: if event["kind"] == "console": print(event["text"]) print(value["completeness"]) finally: server.terminate() server.wait(timeout=10) ``` </details> ### Expected behavior `docs/browser-scenario-contract.md:195-205` promises local seeds once per browser storage partition and session seeds once per origin in each top-level browsing context, before the first document scripts. Both popup cases should read `{"local":"seed-local","session":"seed-session"}` in their first inline script and after the timer; the successful root control should remain successful. Lines 252-267 include discovered popups; the early-event caveat concerns frame/worker/WebSocket/download events, not storage seeding or console capture. ### Impact and scope The tested popup startup runs under different storage state from the accepted request while REA labels its Evidence complete and equality-eligible. This can misrepresent state-dependent application startup. Both executions of each primary-browser case reproduced the behavior. Capturing the child as the root loses the popup context; the supported actions do not offer a pre-script popup session-storage write. Nested popups, other opener relationships, attach mode, and broader browser/platform coverage were not tested. No credential exposure, data loss, or security-boundary crossing was shown. ### REA version 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node v26.12.0; macOS arm64; playwright-core 1.58.2; primary installed Playwright Chromium 153.0.8010.12 (build 1243). ### Target and artifact context Two loopback HTTP origins, fresh launched scenario-owned profiles, console and popup event capture; no external service, credentials, database, or configuration-file input. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The fixture's exact production invocation is `node scripts/rea.mjs capture-browser-scenario <case.json> --json`. The verifier observed these console records: ```text cross-popup: child-first {"local":null,"session":null} child-later {"local":null,"session":null} same-popup: root-first {"local":"seed-local","session":"seed-session"} child-first {"local":"seed-local","session":null} child-later {"local":"seed-local","session":null} root-control: child-first {"local":"seed-local","session":"seed-session"} child-later {"local":"seed-local","session":"seed-session"} ``` Every command exited 0. Every `normalized_result.completeness` was: ```json {"status":"complete","equality_eligible":true,"missing_sections":[],"truncated_sections":[]} ``` Every browser result reported `cleanup: "terminated-owned-process"`. Limitations included the general CDP storage-instrumentation explanation but did not disclose unapplied popup seeds. The passing root control isolates the popup-specific failure on this browser. ### Typed outcome or recovery expected The popup's first script must actually receive both declared seeds, and the root control must keep receiving them. Merely marking the capture incomplete would not restore the promised successful initialization. Equality eligibility should reflect the actual initialized scenario. ### Analysis or suspected cause Source-derived reasoning: `src/browser/PlaywrightScenarioSession.ts:285-299` initializes storage for the opened root page. `src/browser/PlaywrightScenarioStorage.ts:159,207-225,245-269,289-315` creates that page's CDP session and installs target-event-based seed instrumentation. This is consistent with popup targets missing initialization. No direct CDP attachment trace was collected; the observed CLI failure and controls establish the report independently of that interpretation. ### Additional context Prepared with AI assistance and independently reproduced through the production CLI. An earlier Chromium 145.0.7632.6 run also missed root seeds and therefore did not isolate a popup-specific defect; the report relies on Chromium 153's passing root control. No last-known-good version is established. Upstream check: fetched `origin/main` at `1417bf943bf49e49b414e8a508bf28a31304be83`; no post-baseline commits changed the cited contract or storage implementation. Searches included open and closed issues and PRs. #1578/#1579 concern reseeding after navigation, #4010 concerns cancellation cleanup, and #4956 concerns document-principal binding; none reports these popup cases. Related: https://github.com/morluto/rea/issues/5420 (also declared storage seeding, different trigger)
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #5420 ([Bug] Browser scenarios silently skip storage seeds with an empty Chromium execution-context origin). Both address storage seed initialization, context origin matching, and silent observation drops during browser scenario startup. Cataloged under Cluster BL in consolidation tracker #4330.

<!-- gh-comment-id:6296444201 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #5420 (`[Bug] Browser scenarios silently skip storage seeds with an empty Chromium execution-context origin`). Both address storage seed initialization, context origin matching, and silent observation drops during browser scenario startup. Cataloged under Cluster BL in consolidation tracker #4330.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

The current source addresses the silent completeness claim: the storage owner records popup first-script coverage as unknown, and the provider regression expects missing storage, incomplete coverage and equality_eligible: false while retaining completed steps.

That is useful mitigation, but it does not establish that either popup receives the declared seeds before its first script. The storage regression emits simulated page/popup events and checks the limitation; it never executes a popup's first inline storage read. The current contract now explicitly says popup first-script initialization is not guaranteed, narrowing the earlier expectation recorded here.

This therefore remains an unsupported initialization requirement plus a contract decision, rather than evidence that the old complete/equality-eligible result still occurs. If restoring the original guarantee, validate the supplied cross-origin popup, same-origin popup and root control through the production CLI on a recorded Chromium version: both seeds must be present in the first inline script and later read, with the root control preserved. Popup coverage should become complete only when initialization is established. If the intended resolution is instead the documented limitation, closure should explicitly accept that changed support contract and verify the public warning/coverage behavior; it should not describe pre-script popup seeding as fixed.

This is source/test review at d87e73f; I have not rerun the browser fixture.

<!-- gh-comment-id:6380894771 --> @coygeek commented on GitHub (Oct 24, 2026): The current source addresses the silent completeness claim: the [storage owner](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/browser/PlaywrightScenarioStorage.ts#L302-L344) records popup first-script coverage as unknown, and the [provider regression](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/browser/PlaywrightBrowserScenarioProvider.test.ts#L326-L345) expects missing `storage`, incomplete coverage and `equality_eligible: false` while retaining completed steps. That is useful mitigation, but it does not establish that either popup receives the declared seeds before its first script. The [storage regression](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/browser/PlaywrightScenarioStorage.test.ts#L330-L368) emits simulated page/popup events and checks the limitation; it never executes a popup's first inline storage read. The [current contract](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/docs/browser-scenario-contract.md#L222-L244) now explicitly says popup first-script initialization is not guaranteed, narrowing the earlier expectation recorded here. This therefore remains an unsupported initialization requirement plus a contract decision, rather than evidence that the old complete/equality-eligible result still occurs. If restoring the original guarantee, validate the supplied cross-origin popup, same-origin popup and root control through the production CLI on a recorded Chromium version: both seeds must be present in the first inline script and later read, with the root control preserved. Popup coverage should become complete only when initialization is established. If the intended resolution is instead the documented limitation, closure should explicitly accept that changed support contract and verify the public warning/coverage behavior; it should not describe pre-script popup seeding as fixed. This is source/test review at `d87e73f`; I have not rerun the browser fixture.
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#3288
No description provided.