[GH-ISSUE #6239] [Bug] Attached block policy accepts a final capture that becomes service-worker-controlled after its check #3796

Closed
opened 2026-10-31 00:44:08 +00:00 by bsakaguchi · 1 comment
Owner

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

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

With browser.mode: "connect" and environment.service_workers: "block", capture_browser_scenario checks the selected page for a service-worker controller or registration at the start of each step capture, then takes that step's artifacts without checking again. A service worker registered from another same-origin tab can take control of the selected page while those artifacts are being taken. When that happens during the last step, the scenario succeeds: the final step is completed with completeness.status: "complete" and equality_eligible: true, and its accessibility, storage and (in most runs) dom artifacts contain a response the worker served to the selected page. The output contains no rejection or limitation that reports the worker. When the same thing happens during an earlier step, the next step's check rejects the scenario with service_worker_blocked.

Reproduction

These are the steps as run. They are not minimized. Both tested builds behaved the same way. This reproduced in every trigger=gap run (4 of 4).

  1. Serve the following four files with python3 -m http.server <port> --bind 127.0.0.1. Chrome allows service-worker registration on a loopback HTTP origin. /probe is a file containing network.

    sw.js:

    self.addEventListener("install", () => self.skipWaiting());
    self.addEventListener("activate", (event) => event.waitUntil(clients.claim()));
    self.addEventListener("fetch", (event) => {
      if (new URL(event.request.url).pathname === "/probe")
        event.respondWith(new Response("service-worker-intercepted"));
    });
    

    raceother.html (the second tab; it registers the worker when asked):

    <!doctype html><title>race-other</title>
    <script>new BroadcastChannel("race").onmessage = () => navigator.serviceWorker.register("/sw.js");</script>
    

    race.html (the selected page). REA's check calls getRegistrations() in the page's main world, so the page wraps it to choose when to ask the other tab to register. trigger=gap signals on the first call that follows a pause of at least 1500 ms, which is the final step's check after the 3000 ms wait. Each final-step capture in these runs took 90–358 ms, well under that gap. trigger=first signals on the first call, which is the check right after the start navigation. trigger=never never signals. When the page becomes controlled, it fetches /probe and records the result in the DOM and as a sessionStorage key name. The 4,000 rows only make the screenshot artifact slower.

    <!doctype html><title>race</title><p id="out">pending</p><div id="bulk"></div>
    <script>
    const bulk = document.getElementById("bulk");
    for (let i = 0; i < 4000; i++) { const d = document.createElement("div"); d.textContent = "row " + i; d.style.cssText = "box-shadow:0 0 8px #000;filter:blur(0.3px)"; bulk.append(d); }
    const channel = new BroadcastChannel("race");
    const trigger = new URLSearchParams(location.search).get("trigger"); // gap | first | never
    const original = ServiceWorkerContainer.prototype.getRegistrations;
    let previous;
    ServiceWorkerContainer.prototype.getRegistrations = function (...args) {
      const now = performance.now();
      const fire = trigger === "first" ? previous === undefined : trigger === "gap" ? previous !== undefined && now - previous >= 1500 : false;
      previous = now;
      if (fire && !sessionStorage.getItem("signalled")) { sessionStorage.setItem("signalled", "1"); sessionStorage.setItem("t-check-" + Math.round(now), "1"); channel.postMessage("register"); }
      return original.apply(this, args);
    };
    navigator.serviceWorker.addEventListener("controllerchange", async () => {
      const text = await (await fetch("/probe", { cache: "no-store" })).text();
      document.getElementById("out").textContent = text + " controller=" + (navigator.serviceWorker.controller !== null);
      sessionStorage.setItem("probe=" + text + ";controller=" + (navigator.serviceWorker.controller !== null) + ";t=" + Math.round(performance.now()), "1");
    });
    </script>
    
  2. Launch Chrome headless with a fresh profile, --remote-debugging-port=0, and about:blank. Read the endpoint from DevToolsActivePort, and take the about:blank page's target id from /json/list.

  3. Open the second tab with PUT /json/new?http://127.0.0.1:<port>/raceother.html.

  4. Run rea capture-browser-scenario scenario.json --format json with:

    {"browser":{"mode":"connect","cdp_endpoint":"http://127.0.0.1:<devtools-port>","target_id":"<about:blank target id>"},
     "start_url":{"url":"http://127.0.0.1:<port>/race.html?trigger=gap"},
     "environment":{"service_workers":"block"},
     "actions":[{"step_id":"wait","action":"wait_for_timeout","duration_ms":3000}],
     "capture":{"events":[],"after_each_step":["screenshot","dom","accessibility","url","history","storage"],"at_end":["url"]}}
    
  5. Check the exit status, then the wait step's dom artifact (<p id="out">), whether its accessibility artifact contains service-worker-intercepted, and the key names in its storage.value.session_storage. Repeat with trigger=first and trigger=never.

Timer variant, which does not observe REA's check: in race.html, replace the trigger/getRegistrations lines with setTimeout(() => { sessionStorage.setItem("t-signal-" + Math.round(performance.now()), "1"); channel.postMessage("register"); }, Number(new URLSearchParams(location.search).get("delay")));, save it as timer.html, and start at timer.html?delay=<ms> with duration_ms: 500.

Expected behavior

docs/browser-scenario-contract.md lines 18–22 at the tested commit say: "For attached browsers, block rejects captures when the selected page or its frames are controlled by a service worker or already have a registration, including after navigation. ... Navigation can run destination code before a newly discovered worker is rejected; that rejection does not roll back browser effects." Closed issue #2956, whose fix added the current per-capture check, stated that "a block request should remain meaningful at each capture boundary". The policy could be read as promising only a check at each capture boundary, which the current code performs. However, the documented rule is about the capture itself: it rejects captures taken while the selected page is controlled, and it accepts that destination code can run before the rejection but still promises one. In this case the selected page is controlled while its final step's artifacts are being taken, and they contain content the worker served. The scenario should fail with service_worker_blocked, as it does when the worker takes control during an earlier step. Alternatively, it should not return those artifacts as a completed, complete, comparison-eligible capture under block.

Impact and scope

Evidence integrity of one step: a successful attached-browser capture under the default block policy can return a final step whose accessibility, storage and usually DOM artifacts include a service-worker response, while the step reports complete, comparison-eligible completeness and nothing in the output reports the worker. The trigger requires a same-origin registration from outside the selected page (another tab in the attached browser) that takes control during the final step's artifact capture. With an ordinary page timer instead of the deterministic trigger, it occurred in 1 of 12 runs. Earlier steps are caught by the next step's check. Launch mode, other platforms and other browsers were not tested. Attaching to a browser with no other same-origin tabs, or using browser.mode: "launch", avoids the outside registration path used here, but neither workaround was tested.

REA version

rea-agents@6.7.0 from npm (rea --version: 6.7.0), and a source build of 292fd532c9e62d0654a5277889f2a25acc96d5f3 (also prints 6.7.0)

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; headless Google Chrome for Testing 153.0.8010.12 (reported by REA as Chromium 153.0.8010.12)

Target and artifact context

Local loopback HTTP fixture in an attached (connect-mode) browser with a fresh disposable profile; second same-origin tab registers a claiming service worker

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

trigger=gap: 3 of 3 source-build runs and 1 of 1 release run exited 0. In all 4 runs, the final wait step's accessibility artifact contains service-worker-intercepted, and its storage artifact contains a key such as probe=service-worker-intercepted;controller=true;t=5134 next to t-check-5069. In 3 of the 4 runs, its dom artifact contains <p id="out">service-worker-intercepted controller=true</p>; in the remaining source-build run the DOM was taken before the page updated and shows pending. The step reports "status":"completed", "error":null and "completeness":{"status":"complete","equality_eligible":true,"missing_sections":[],"truncated_sections":[]}. The capture-level completeness is incomplete only because connect mode always lists events as missing. In this fixture, the page saw the worker's response 38–65 ms after the signal. The exposure window is longer than that: it covers the whole artifact sequence after the check.

trigger=first: 2 of 2 source-build runs and 1 of 1 release run exited 1 with code: "target_unavailable" and reason: "service_worker_blocked". The worker took control during the scenario_start capture: that step in details.partial_observation contains the worker's response. The wait step's check then rejected the scenario.

trigger=never (second tab open, never asked to register): 1 of 1 source-build run exited 0 with pending in every artifact and no worker key.

Timer variant on the source build: 1 of 12 runs (delay 1300 ms) succeeded with no error envelope, and service-worker-intercepted controller=true appeared in the final DOM and storage artifacts. 8 runs (delays 700, 900, 1100, 1250, 1350, and 1300 three more times) failed with service_worker_blocked. 3 runs (1500, 1700, 1900) completed with no worker content in their artifacts. Only the last of the three repeat runs at 1300 ms kept its output file; the other two were observed in the console only.

Before the version above, trigger=gap used a 300 ms gap with a 500 ms wait; it gave the same results (gap 4 of 4 succeeded with worker content, first 3 of 3 rejected, never clean), but a capture can approach 300 ms, so the margin was widened. A still earlier version of race.html signalled on the third getRegistrations() call. It gave the same final-step result in 4 of 4 source-build runs, and in one release run with all six artifacts. A second release run requested only screenshot, dom and url; its DOM showed pending, and it requested no artifact that could show whether the worker later took control.

Final-step fields from one source-build trigger=gap run (exit 0). The fields are copied from the output; session_storage_names is not an REA field but lists the name values from the step's artifacts.storage.value.session_storage:

{"step_id":"wait","status":"completed","error":null,"completeness":{"status":"complete","equality_eligible":true,"missing_sections":[],"truncated_sections":[]},"session_storage_names":["t-check-5069","probe=service-worker-intercepted;controller=true;t=5134","signalled"]}

The same step's DOM artifact contains <p id="out">service-worker-intercepted controller=true</p>.

Typed outcome or recovery expected

The run should end with BrowserObservationError reason service_worker_blocked (code: "target_unavailable"), as the trigger=first control does, or otherwise not return worker-influenced artifacts as a completed, complete, comparison-eligible final step. Failing result: with trigger=gap, the CLI exits 0 and the final wait step is completed and complete, while its artifacts contain service-worker-intercepted controller=true. Restored result: the same run fails with service_worker_blocked. Alternatively, it completes, but only with artifacts that show pending and an uncontrolled page; it never returns a complete final step whose artifacts contain the worker's response. Confirm that the run targeted the final step: the t-check-<ms> key must appear only in the final step's storage artifact and not in scenario_start's; if a fix's extra checks make a capture last close to 1500 ms, raise the wait and the gap together. Controls: trigger=first must still fail with service_worker_blocked, and trigger=never must still exit 0 with pending artifacts, so a fix that rejects every browser with a second same-origin tab does not count as restoration.

Analysis or suspected cause

Source-derived reasoning, at the tested commit (not a proposed fix): PlaywrightScenarioSession.capture() (src/browser/PlaywrightScenarioSession.ts lines 446–473) awaits serviceWorkers.assertBlocked(), then storage.settle(), then capturePlaywrightStepArtifacts(), which takes screenshot, DOM, accessibility, URL, history and storage in sequence. Nothing checks the worker state after the artifacts. src/browser/PlaywrightBrowserScenarioProvider.ts calls session.capture() once per step, so for the final step no later check runs. The policy's register wrapper covers the selected page and its auto-attached iframes. Other same-origin tabs in the attached browser are not covered, and they can register a worker that claims the selected page.

Additional context

Related: #2956 (closed) reported that the policy checked only the pre-navigation document, not the captured destination; its fix introduced the per-capture check discussed here. Open PR #5933 changes how assertBlocked() classifies about:blank, blob: and sandboxed frames; it does not address timing after the check.

Originally created by @coygeek on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6239 ### 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 With `browser.mode: "connect"` and `environment.service_workers: "block"`, `capture_browser_scenario` checks the selected page for a service-worker controller or registration at the start of each step capture, then takes that step's artifacts without checking again. A service worker registered from another same-origin tab can take control of the selected page while those artifacts are being taken. When that happens during the last step, the scenario succeeds: the final step is `completed` with `completeness.status: "complete"` and `equality_eligible: true`, and its `accessibility`, `storage` and (in most runs) `dom` artifacts contain a response the worker served to the selected page. The output contains no rejection or limitation that reports the worker. When the same thing happens during an earlier step, the next step's check rejects the scenario with `service_worker_blocked`. ### Reproduction These are the steps as run. They are not minimized. Both tested builds behaved the same way. This reproduced in every `trigger=gap` run (4 of 4). 1. Serve the following four files with `python3 -m http.server <port> --bind 127.0.0.1`. Chrome allows service-worker registration on a loopback HTTP origin. `/probe` is a file containing `network`. `sw.js`: ```js self.addEventListener("install", () => self.skipWaiting()); self.addEventListener("activate", (event) => event.waitUntil(clients.claim())); self.addEventListener("fetch", (event) => { if (new URL(event.request.url).pathname === "/probe") event.respondWith(new Response("service-worker-intercepted")); }); ``` `raceother.html` (the second tab; it registers the worker when asked): ```html <!doctype html><title>race-other</title> <script>new BroadcastChannel("race").onmessage = () => navigator.serviceWorker.register("/sw.js");</script> ``` `race.html` (the selected page). REA's check calls `getRegistrations()` in the page's main world, so the page wraps it to choose when to ask the other tab to register. `trigger=gap` signals on the first call that follows a pause of at least 1500 ms, which is the final step's check after the 3000 ms wait. Each final-step capture in these runs took 90–358 ms, well under that gap. `trigger=first` signals on the first call, which is the check right after the start navigation. `trigger=never` never signals. When the page becomes controlled, it fetches `/probe` and records the result in the DOM and as a `sessionStorage` key name. The 4,000 rows only make the `screenshot` artifact slower. ```html <!doctype html><title>race</title><p id="out">pending</p><div id="bulk"></div> <script> const bulk = document.getElementById("bulk"); for (let i = 0; i < 4000; i++) { const d = document.createElement("div"); d.textContent = "row " + i; d.style.cssText = "box-shadow:0 0 8px #000;filter:blur(0.3px)"; bulk.append(d); } const channel = new BroadcastChannel("race"); const trigger = new URLSearchParams(location.search).get("trigger"); // gap | first | never const original = ServiceWorkerContainer.prototype.getRegistrations; let previous; ServiceWorkerContainer.prototype.getRegistrations = function (...args) { const now = performance.now(); const fire = trigger === "first" ? previous === undefined : trigger === "gap" ? previous !== undefined && now - previous >= 1500 : false; previous = now; if (fire && !sessionStorage.getItem("signalled")) { sessionStorage.setItem("signalled", "1"); sessionStorage.setItem("t-check-" + Math.round(now), "1"); channel.postMessage("register"); } return original.apply(this, args); }; navigator.serviceWorker.addEventListener("controllerchange", async () => { const text = await (await fetch("/probe", { cache: "no-store" })).text(); document.getElementById("out").textContent = text + " controller=" + (navigator.serviceWorker.controller !== null); sessionStorage.setItem("probe=" + text + ";controller=" + (navigator.serviceWorker.controller !== null) + ";t=" + Math.round(performance.now()), "1"); }); </script> ``` 2. Launch Chrome headless with a fresh profile, `--remote-debugging-port=0`, and `about:blank`. Read the endpoint from `DevToolsActivePort`, and take the `about:blank` page's target id from `/json/list`. 3. Open the second tab with `PUT /json/new?http://127.0.0.1:<port>/raceother.html`. 4. Run `rea capture-browser-scenario scenario.json --format json` with: ```json {"browser":{"mode":"connect","cdp_endpoint":"http://127.0.0.1:<devtools-port>","target_id":"<about:blank target id>"}, "start_url":{"url":"http://127.0.0.1:<port>/race.html?trigger=gap"}, "environment":{"service_workers":"block"}, "actions":[{"step_id":"wait","action":"wait_for_timeout","duration_ms":3000}], "capture":{"events":[],"after_each_step":["screenshot","dom","accessibility","url","history","storage"],"at_end":["url"]}} ``` 5. Check the exit status, then the `wait` step's `dom` artifact (`<p id="out">`), whether its `accessibility` artifact contains `service-worker-intercepted`, and the key names in its `storage.value.session_storage`. Repeat with `trigger=first` and `trigger=never`. Timer variant, which does not observe REA's check: in `race.html`, replace the `trigger`/`getRegistrations` lines with `setTimeout(() => { sessionStorage.setItem("t-signal-" + Math.round(performance.now()), "1"); channel.postMessage("register"); }, Number(new URLSearchParams(location.search).get("delay")));`, save it as `timer.html`, and start at `timer.html?delay=<ms>` with `duration_ms: 500`. ### Expected behavior `docs/browser-scenario-contract.md` lines 18–22 at the tested commit say: "For attached browsers, `block` rejects captures when the selected page or its frames are controlled by a service worker or already have a registration, including after navigation. ... Navigation can run destination code before a newly discovered worker is rejected; that rejection does not roll back browser effects." Closed issue #2956, whose fix added the current per-capture check, stated that "a block request should remain meaningful at each capture boundary". The policy could be read as promising only a check at each capture boundary, which the current code performs. However, the documented rule is about the capture itself: it rejects captures taken while the selected page is controlled, and it accepts that destination code can run before the rejection but still promises one. In this case the selected page is controlled while its final step's artifacts are being taken, and they contain content the worker served. The scenario should fail with `service_worker_blocked`, as it does when the worker takes control during an earlier step. Alternatively, it should not return those artifacts as a completed, complete, comparison-eligible capture under `block`. ### Impact and scope Evidence integrity of one step: a successful attached-browser capture under the default `block` policy can return a final step whose accessibility, storage and usually DOM artifacts include a service-worker response, while the step reports complete, comparison-eligible completeness and nothing in the output reports the worker. The trigger requires a same-origin registration from outside the selected page (another tab in the attached browser) that takes control during the final step's artifact capture. With an ordinary page timer instead of the deterministic trigger, it occurred in 1 of 12 runs. Earlier steps are caught by the next step's check. Launch mode, other platforms and other browsers were not tested. Attaching to a browser with no other same-origin tabs, or using `browser.mode: "launch"`, avoids the outside registration path used here, but neither workaround was tested. ### REA version `rea-agents@6.7.0` from npm (`rea --version`: `6.7.0`), and a source build of `292fd532c9e62d0654a5277889f2a25acc96d5f3` (also prints `6.7.0`) ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; headless Google Chrome for Testing 153.0.8010.12 (reported by REA as Chromium 153.0.8010.12) ### Target and artifact context Local loopback HTTP fixture in an attached (connect-mode) browser with a fresh disposable profile; second same-origin tab registers a claiming service worker ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence `trigger=gap`: 3 of 3 source-build runs and 1 of 1 release run exited 0. In all 4 runs, the final `wait` step's `accessibility` artifact contains `service-worker-intercepted`, and its `storage` artifact contains a key such as `probe=service-worker-intercepted;controller=true;t=5134` next to `t-check-5069`. In 3 of the 4 runs, its `dom` artifact contains `<p id="out">service-worker-intercepted controller=true</p>`; in the remaining source-build run the DOM was taken before the page updated and shows `pending`. The step reports `"status":"completed"`, `"error":null` and `"completeness":{"status":"complete","equality_eligible":true,"missing_sections":[],"truncated_sections":[]}`. The capture-level completeness is `incomplete` only because connect mode always lists `events` as missing. In this fixture, the page saw the worker's response 38–65 ms after the signal. The exposure window is longer than that: it covers the whole artifact sequence after the check. `trigger=first`: 2 of 2 source-build runs and 1 of 1 release run exited 1 with `code: "target_unavailable"` and `reason: "service_worker_blocked"`. The worker took control during the `scenario_start` capture: that step in `details.partial_observation` contains the worker's response. The `wait` step's check then rejected the scenario. `trigger=never` (second tab open, never asked to register): 1 of 1 source-build run exited 0 with `pending` in every artifact and no worker key. Timer variant on the source build: 1 of 12 runs (delay 1300 ms) succeeded with no error envelope, and `service-worker-intercepted controller=true` appeared in the final DOM and storage artifacts. 8 runs (delays 700, 900, 1100, 1250, 1350, and 1300 three more times) failed with `service_worker_blocked`. 3 runs (1500, 1700, 1900) completed with no worker content in their artifacts. Only the last of the three repeat runs at 1300 ms kept its output file; the other two were observed in the console only. Before the version above, `trigger=gap` used a 300 ms gap with a 500 ms wait; it gave the same results (gap 4 of 4 succeeded with worker content, first 3 of 3 rejected, never clean), but a capture can approach 300 ms, so the margin was widened. A still earlier version of `race.html` signalled on the third `getRegistrations()` call. It gave the same final-step result in 4 of 4 source-build runs, and in one release run with all six artifacts. A second release run requested only `screenshot`, `dom` and `url`; its DOM showed `pending`, and it requested no artifact that could show whether the worker later took control. Final-step fields from one source-build `trigger=gap` run (exit 0). The fields are copied from the output; `session_storage_names` is not an REA field but lists the `name` values from the step's `artifacts.storage.value.session_storage`: ```json {"step_id":"wait","status":"completed","error":null,"completeness":{"status":"complete","equality_eligible":true,"missing_sections":[],"truncated_sections":[]},"session_storage_names":["t-check-5069","probe=service-worker-intercepted;controller=true;t=5134","signalled"]} ``` The same step's DOM artifact contains `<p id="out">service-worker-intercepted controller=true</p>`. ### Typed outcome or recovery expected The run should end with `BrowserObservationError` reason `service_worker_blocked` (`code: "target_unavailable"`), as the `trigger=first` control does, or otherwise not return worker-influenced artifacts as a completed, complete, comparison-eligible final step. Failing result: with `trigger=gap`, the CLI exits 0 and the final `wait` step is `completed` and `complete`, while its artifacts contain `service-worker-intercepted controller=true`. Restored result: the same run fails with `service_worker_blocked`. Alternatively, it completes, but only with artifacts that show `pending` and an uncontrolled page; it never returns a complete final step whose artifacts contain the worker's response. Confirm that the run targeted the final step: the `t-check-<ms>` key must appear only in the final step's `storage` artifact and not in `scenario_start`'s; if a fix's extra checks make a capture last close to 1500 ms, raise the wait and the gap together. Controls: `trigger=first` must still fail with `service_worker_blocked`, and `trigger=never` must still exit 0 with `pending` artifacts, so a fix that rejects every browser with a second same-origin tab does not count as restoration. ### Analysis or suspected cause Source-derived reasoning, at the tested commit (not a proposed fix): `PlaywrightScenarioSession.capture()` (`src/browser/PlaywrightScenarioSession.ts` lines 446–473) awaits `serviceWorkers.assertBlocked()`, then `storage.settle()`, then `capturePlaywrightStepArtifacts()`, which takes screenshot, DOM, accessibility, URL, history and storage in sequence. Nothing checks the worker state after the artifacts. `src/browser/PlaywrightBrowserScenarioProvider.ts` calls `session.capture()` once per step, so for the final step no later check runs. The policy's `register` wrapper covers the selected page and its auto-attached iframes. Other same-origin tabs in the attached browser are not covered, and they can register a worker that claims the selected page. ### Additional context Related: #2956 (closed) reported that the policy checked only the pre-navigation document, not the captured destination; its fix introduced the per-capture check discussed here. Open PR #5933 changes how `assertBlocked()` classifies about:blank, blob: and sandboxed frames; it does not address timing after the check.
Author
Owner

@XinwangTao commented on GitHub (Oct 22, 2026):

确认这个竞态真实存在:assertBlocked() 之后到 artifact 采集完成之间存在窗口,attached 浏览器里外部同源 tab 注册的 claiming worker 可以在这个窗口接管页面,而最终 step 没有后续检查兜底,导致含 worker 内容的 step 被标记 complete。trigger=gap 4/4、timer 变体 1/12 的比例说明这不是纯理论问题。建议在 capturePlaywrightStepArtifacts() 之后补一次 worker 状态复查(复用 assertBlocked),命中则按 service_worker_blocked 处理;同时保留 trigger=first / trigger=never 两条控制用例防止过度拒绝。若复查放最后导致捕获耗时接近 gap,需同步放宽 wait 与 gap,这点报告里已经写清楚了。

<!-- gh-comment-id:6332702435 --> @XinwangTao commented on GitHub (Oct 22, 2026): 确认这个竞态真实存在:assertBlocked() 之后到 artifact 采集完成之间存在窗口,attached 浏览器里外部同源 tab 注册的 claiming worker 可以在这个窗口接管页面,而最终 step 没有后续检查兜底,导致含 worker 内容的 step 被标记 complete。trigger=gap 4/4、timer 变体 1/12 的比例说明这不是纯理论问题。建议在 capturePlaywrightStepArtifacts() 之后补一次 worker 状态复查(复用 assertBlocked),命中则按 service_worker_blocked 处理;同时保留 trigger=first / trigger=never 两条控制用例防止过度拒绝。若复查放最后导致捕获耗时接近 gap,需同步放宽 wait 与 gap,这点报告里已经写清楚了。
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#3796
No description provided.