Skip to content

feat: CDP mode, screencast recorder, snapshot perf, multi-instance isolation, mode-aware anti-detection, and CLI/daemon hardening#21

Merged
NiceCode666 merged 75 commits into
devfrom
feature/cdp-url-support
May 18, 2026
Merged

feat: CDP mode, screencast recorder, snapshot perf, multi-instance isolation, mode-aware anti-detection, and CLI/daemon hardening#21
NiceCode666 merged 75 commits into
devfrom
feature/cdp-url-support

Conversation

@NiceCode666

@NiceCode666 NiceCode666 commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

New features

  • CDP connection modeBrowser(cdp=...) SDK arg and --cdp CLI flag on open/search. Accepts a port number (9222), ws:///wss:// URL, http://host:port discovery endpoint, or auto to scan local Chrome / Chromium / Brave / Edge / Chrome-Beta profiles (incl. Snap & Flatpak on Linux). Works around Playwright's _mainContext() hang on pre-existing tabs by driving title / navigation / element evaluation / page metrics through raw CDPSession calls.
  • CDP screencast video recorder — new VideoRecorder (session/_video_recorder.py) replaces Playwright's built-in record_video_dir. CDP screencast frames are piped into ffmpeg (VP8, 5 Mbps, CRF 4). Two-phase shutdown (prepare_stop while Chrome is alive → finalize after exit), single continuous output stream that hot-swaps the screencast source whenever bridgic's active tab moves (via switch_tab / new_tab / navigate_to / close_tab). Independent background tabs or unowned popups do not trigger a switch.
  • BRIDGIC_HOME multi-instance isolation — configurable root state directory via BRIDGIC_HOME env var. All derived paths (run info, socket, logs, tmp, config, user data) follow the override, enabling multiple independent CLI daemon instances in parallel.
  • wait --timeout — configurable appearance/disappearance timeout on the wait command.
  • Background snapshot pre-warming — dedicated prefetch SnapshotGenerator runs in the background after navigation so the next get_snapshot() is an instant cache hit. In-flight prefetch is canceled on navigate / page-switch / close to preserve cache integrity.
  • Headed/headless user_data_dir split — persistent profiles use mode-specific subdirs (<base>/headed vs <base>/headless) to prevent Chromium SingletonLock crashes when switching modes.
  • Tab privacy boundary (owned-pages tracking)Browser maintains an internal _owned_pages set so all public tab ops (get_tabs / switch_tab / close_tab) only show pages bridgic itself spawned or adopted via Page.opener(). In CDP borrowed mode the user's pre-existing tabs stay invisible, and any tabs the user later opens via Cmd+click / Ctrl+click / middle-click (Chromium's background-tab path strips openerId) / Cmd+T / address bar (no opener at all) are also unseen — a deliberate privacy boundary that prevents an LLM driving bridgic from switching to, reading, or closing the user's private work tabs. New Browser(auto_follow_popups=False) parameter keeps self._page fixed on the original tab when an adopted popup spawns. Decision rule, Chromium internals, and the full ✅/❌ truth table are documented in docs/INTERNALS.md (Adoption truth table + "Why Cmd-click strips openerId") and docs/CDP_MODE.md.
  • Silent downloads in CDP-borrowed mode + curl -O ergonomicsBrowser.setDownloadBehavior(allowAndName, downloadPath=..., eventsEnabled=true) is sent over a page-level CDP session attached to bridgic's tab (BrowserContext.new_cdp_session(self._page)); browser-level routing was empirically shown to NOT bypass Chrome's "Ask where to save each file" preference on Chrome 138+ (agent-browser uses the same Some(session_id) page-routing trick). New CdpDownloadRenamer (session/_cdp_download_renamer.py) subscribes to Browser.downloadWillBegin/downloadProgress on the same page session and renames <dir>/<guid><dir>/<real name> on completion (filename sanitization strips path separators / Windows-forbidden chars / control bytes / >255 byte; conflicts resolve to name (1).ext Chrome-style). Effective-path priority: explicit Browser(downloads_path=...) / bridgic-browser.json config > the CLI client's per-command CWD (every socket request now carries os.getcwd(); the daemon hot-swaps via update_cdp_downloads_path before dispatch) > ~/Downloads. The daemon's auto-default downloads_path=~/Downloads is suppressed in CDP mode so it can't shadow the CWD priority chain. Empirically-tried-and-rejected alternatives (in CLAUDE.mdPage.setDownloadBehavior(allow), Browser.setDownloadBehavior over a browser session with either allow or allowAndName, passing defaultBrowserContextId from Target.getBrowserContexts which Chrome rejects with "Failed to find browser context for id") are documented.

Anti-detection (mode-aware)

  • Industrial-grade JS+CDP stealth layer covering 24+ fingerprint vectors. Verified against the public CloakBrowser benchmark suite (tests/test_stealth.py) — 5/5 passing in headed mode, 4/5 in headless (the only headless miss is demo.fingerprint.com/web-scraping, a server-side TLS fingerprint check unreachable from JS).
  • R1: UA / Sec-CH-UA cleanup (headless) — context user_agent fallback + CDP Emulation.setUserAgentOverride (with userAgentMetadata.brands). Anchored CDP session keeps the override sticky across navigations; Network.* variant silently drops UAD metadata in Chromium 145+ so we deliberately use Emulation.
  • R2: native prototype identity for navigator.plugins / mimeTypes + Web IDL §3.2.4 uint32 truncation in item(i) (fixes incolumitas overflowTest where plugins.item(4294967296) === plugins[0]).
  • R3: race-proof Web/Service/Shared Worker injection via constructor wrap that prepends stealth code through importScripts before the worker's first instruction. Same-origin filter (blob: / data: / same-origin only) prevents CORS breakage on cross-origin worker URLs (e.g. reCAPTCHA's gstatic.com worker).
  • R4 + R6: delete Navigator.prototype.webdriver instead of an undefined-getter, achieving 'webdriver' in navigator === false (Web IDL §3.7.6 guarantees configurable: true). Headless via init script; headed via per-page page.evaluate + framenavigated re-apply on the main frame only.
  • R5-lite: console.{log,debug,info,warn,error,trace} Error pre-stringify — defeats CDP-attach detection that uses error.stack getter traps (deviceandbrowserinfo isAutomatedWithCDP*).
  • Iframe-safe by design. Every patch that could propagate into a cross-origin iframe is gated to headless-only (add_init_script runs in all frames, including Cloudflare Turnstile's challenge iframe). Headed mode uses targeted page.evaluate + same-origin filters to keep cross-origin iframes pristine. Documented as a hard rule in CLAUDE.md with a 5-point review checklist.

Performance

  • Snapshot three-phase evaluate (session/_snapshot.py) — extract _batch_get_elements_info JS to a module-level constant, split into (1) one-shot role-index build via querySelectorAll per unique role into window.__bridgicRoleIndex, (2) N×500-ref batch info reads that consume the index, (3) finally-cleanup. Per-snapshot state is keyed by a generation token so concurrent snapshots on the same page cannot clobber each other. ~10× speed-up on pages with 1000+ refs; fixes a regression where large pages (5549 refs) timed out at 95–123 s → 41–50 s.
  • asyncio.gather parallelization for page.title() in get_all_page_descs, is_visible() in _prefer_visible_locators, snapshot + page_info in get_full_page_info, and per-page close in close().
  • CDP evaluate_javascript parity with page.evaluate_wrap_js_for_cdp_eval mirrors Playwright's normalizeEvaluationExpression + utilityScript.evaluate so arbitrary JS strings (IIFEs with trailing ;, statement lists, class declarations, let/const blocks, template literals, async/await) round-trip byte-identically between CDP-borrowed and non-CDP modes. Replaces the narrower _maybe_wrap_arrow_fn. 28-case parity matrix verified in tests/integration/test_evaluate_cdp_parity.py.

Bug fixes (CR follow-ups)

  • _closing flag not reset on restart_start() now resets self._closing = False, fixing Browser instance reuse after close().
  • _is_browser_closed_error stack overflow — rewritten from recursion to iterative loop with seen set guard against circular exception chains.
  • evaluate_javascript() CDP path swallowed JS exceptions — now checks exceptionDetails and raises OperationError instead of returning the error description as a string.
  • go_back()/go_forward() silent failure — non-CDP path now checks Playwright return value; raises StateError(NO_HISTORY_ENTRY) when None (no history entry).
  • CDPSession leak in _apply_debugger_skip_pausestry/finally guarantees detach even on CancelledError.
  • start_video() race with concurrent close — uses page.context instead of self._context to avoid None during teardown.
  • wait_for_download double timeout — switched to deadline-based single timeout; actual wait no longer exceeds the user-specified duration. Note: wait_for_download is unsupported in CDP-borrowed mode (Playwright's per-context download event does not fire when downloads are routed away from artifactsDir); use non-CDP / CDP-owned mode for programmatic wait, or poll downloads_path after the click in CDP-borrowed mode.
  • _probe_cdp_alive timeout mismatch — default now uses _timeouts.CDP_PROBE_S instead of hardcoded 2.0s.

CLI freeze / hang fixes (from daemon.log forensics)

  • Click hang — cap click / dblclick / check / uncheck at 10 s and fall back to locator.dispatch_event() on PlaywrightTimeoutError instead of letting the 30 s actionability retry loop freeze the CLI on Vue / React SPAs.
  • Launch crash loop — wrap launch_persistent_context / launch in _retriable_launch with exponential backoff (0 → 1 → 2.5 s) for transient SingletonLock / target closed failures; fail fast on all other errors.
  • Observability — matched [CLI-CMD] / [CLI-RESP] log pairs with timing around every _dispatch; warn on commands > 60 s; log _snapshot_lock wait time when > 100 ms.

Daemon hardening (cli/_daemon.py, cli/_client.py)

  • Per-command socket timeout computed from timeout / seconds args so long-running wait commands don't race the 90 s default.
  • Cancel in-flight dispatch when the client disconnects mid-request (prevents orphaned tasks against the Browser singleton).
  • Reject new connections once shutdown has begun so back-to-back closeopen cannot crash mid-dispatch.
  • Force-reset internal handles in _cdp_reconnect so _start()'s early-return guard cannot silently skip reconnect after a partial close.
  • Factor out _resolve_cdp_url_from_env; short-circuit already-resolved ws:// URLs.
  • Auto-reconnect on CDP session drop (one-shot retry). Context-aware "browser closed" error messages for local vs. remote.
  • Downloads-path auto-default ~/Downloads is now scoped to non-CDP mode only. In CDP mode the daemon leaves downloads_path unset so the new CDP path-resolution chain (explicit > client CWD > ~/Downloads) drives behavior.
  • Per-command CWD propagation: every CLI socket request carries os.getcwd(); the daemon sets Browser._pending_client_cwd before dispatch and calls update_cdp_downloads_path() to re-target CDP setDownloadBehavior when the effective path changes.
  • Pre-close cancellation of in-flight snapshot prefetch to avoid stale-task errors during reconnect.

CDP detection hardening (session/_browser.py)

  • _probe_cdp_alive skips stale DevToolsActivePort files left by crashes / kill -9.
  • TCP probe of WebSocket targets with clearer error for stale CDP URLs.
  • Expanded Linux CDP scan directories (Chrome, Chromium, Brave, Edge, Chrome Beta, Snap, Flatpak).
  • Broader system-Chrome detection for macOS ~/Applications and Linux Chromium-based binaries.
  • Canonicalize host in port-mode URLs and always rewrite ws:// netloc so SSH-tunneled / port-forwarded targets work.
  • Robust handling of malformed DevToolsActivePort files.
  • Scroll-race detection in CDP evaluate() prevents executing JS on the wrong element.

VideoRecorder hardening (session/_video_recorder.py)

  • Drain ffmpeg stderr into a capped buffer to prevent a full pipe from deadlocking stdin.drain().
  • Hardened ffmpeg path discovery (X_OK check, Windows fallback, clearer error on missing binary).
  • Regression tests for closing-state handling during video recording.

Config hardening (_config.py)

  • Reject non-dict JSON values in user config, local config, and the BRIDGIC_BROWSER_JSON env var instead of silently failing later.

Refactoring

  • Re-introduced owned-pages tracking as a per-Page Set[Page] + parallel LRU List[Page] focus stack (replaces an earlier mid-branch refactor that removed all ownership bookkeeping). _mark_owned is idempotent and registers a per-page on("close") pruner; _maybe_adopt_page runs from a context.on("page") listener and adopts iff await page.opener() is already owned. Triple _closing guards prevent races during shutdown. _close_page four-tier fallback: closed_page.opener()_focus_stack top → get_pages()[0]None (latter triggers existing navigate_to auto-recreate branch, which now correctly marks the rebuilt page owned — fixed a real ghost-page bug found in manual testing).
  • In CDP borrowed mode close() remains a pure disconnect (no tabs killed), but start_video / snapshot / tab ops are now scoped to owned pages only — matching the privacy-boundary intent.
  • _switch_self_page_to(new_page) consolidates "move self._page" side effects (focus stack push, _invalidate_page_state, _switch_video_to_page). A legacy DownloadManager.detach_from_page(old) + attach_to_page(new) swap is retained in CDP-borrowed mode as a no-op safety net — DownloadManager is no longer the active CDP-borrowed download pipeline (CdpDownloadRenamer is, bound to bridgic's original self._page's CDP session). Downloads triggered from a followed popup fall back to Playwright's per-context override (artifactsDir) and rely on the L2 rescue net on close.

Docs & infra

  • New docs/CDP_MODE.md (remote-debugging setup, Chrome 144+ UI + launch flags, DevToolsActivePort paths per OS).
  • Expanded docs/INTERNALS.md (CDP bypass internals, snapshot generation token, video recorder pipeline, download pipeline notes).
  • New docs/KNOWN_LIMITATIONS.md — including the Chrome "Show in Folder" bug (Chromium #324282051) which is triggered by setDownloadBehavior(eventsEnabled: true) and therefore affects all three download pipelines.
  • CLAUDE.md — new ### Downloads section with 3-mode pipeline table, effective-path priority chain, empirically-tried-alternatives matrix, CdpDownloadRenamer design notes, and caveats.
  • README / README_zh — remote-debugging section + download path matrix (CLI/SDK × CDP/non-CDP × downloads_path set/unset → effective path).
  • CLI + SDK skill reference docs updated (--cdp, --timeout, BRIDGIC_HOME, multi-instance isolation, downloads_path per-mode behavior, command descriptions).
  • CI: Windows workflow switched to ubuntu-latest for compatibility; Playwright browser caching added.

Stats

93 files changed, +23,809 / −2,038 across 67 commits. (Stats are approximate — additional CDP eval-parity, silent-downloads, and lifecycle-test commits were appended on top; see the diff for authoritative numbers.)

Test plan

Unit & integration coverage added across the change surface:

  • make test-quick (unit) passes — 1247+ tests covering CDP URL resolution, reconnect, daemon env plumbing, client timeout, config, detect-chrome, snapshot chunking / role-index / cleanup, video recorder, CLI commands, BRIDGIC_HOME constants, click fallback, CDP evaluate_javascript parity, CDP-borrowed download behavior + filename rename.
  • New integration suites: test_cdp_borrowed_mode, test_cdp_cli_full, test_cdp_lifecycle, test_cli_close_race, test_cli_long_wait, test_click_fallback, test_snapshot_concurrency, test_evaluate_cdp_parity (28-case JS round-trip parity matrix against real Chrome).
  • New unit suites: test_constants (BRIDGIC_HOME), test_video_recorder_frame_ts, test_cdp_reconnect, test_daemon_cdp_env, test_detect_chrome, test_cli_client_timeout, test_cdp_download_renamer (17 cases: sanitize / lifecycle / rename / conflict / canceled / set_default_dir / robustness).
  • CloakBrowser public anti-bot benchmark (tests/test_stealth.py) — sannysoft (0/57 fail, both modes), incolumitas (0 fail, both modes — WEBDRIVER and overflowTest both fixed), browserscan (0 abnormal, both modes), demo.fingerprint.com web-scraping (PASS in headed), reCAPTCHA v3 (score 0.9, both modes).
  • Manual anti-detection verification — a Cloudflare-Turnstile-protected page issues a 1114-byte token; x.com manual login reaches /home with all logged-in selectors present; blog.aepkill.com devtools-detector reports status: close. All in headed mode.
  • Owned-pages tracking — new integration suite tests/integration/test_owned_pages.py (10 cases: user-tab invisibility, popup adoption via opener, popup-close fallback chain, download manager follows popup in borrowed CDP, non-CDP all-visible mode, opener API probe). New unit coverage (U1-U18 in test_browser_methods.py) for owned set membership, close-fallback four-tier order, popup adoption identity check, race guards under _closing, navigate-to recovery, and get_tabs hidden-tab hint. Pre-existing test_cdp_borrowed_mode.py / test_cdp_cli_full.py are module-level skipped (old "user tabs accessible" contract replaced by new-semantics coverage). Mode-matrix V5 (CDP attach × headless): FAIL=0.
  • Adoption rules manual 12-case fixture — confirmed: bridgic click <ref> adopts; user plain left-click on <a target="_blank"> / window.open() adopts (regardless of rel="noopener" / rel="noreferrer" / window.open(...,'noopener') — these only suppress JS-level window.opener, not CDP-level openerId); user Cmd+click / Ctrl+click / middle-click → NOT adopted; bridgic key-down Meta + click + key-up → also NOT adopted (Playwright keyboard state propagates into Input.dispatchMouseEvent.modifiers, role-agnostic). HTTPS source vs file:// source has no effect — only the click path matters.
  • Silent-download manual fixturecd <dir> && bridgic-browser open --cdp auto <github-release> && bridgic-browser click @<whl-ref> lands bridgic_browser-0.0.4-py3-none-any.whl (146441 B, real filename) directly in <dir> with no Save As dialog, no GUID residue, and a [CdpDownloadRenamer] <guid-prefix> → <real-name> daemon-log entry. Verified across all four mode/caller combinations: CLI+CDP (CWD), CLI+non-CDP (~/Downloads via DownloadManager), SDK+CDP (explicit downloads_path), SDK+non-CDP (explicit downloads_path via DownloadManager).
  • bridgic-browser open https://example.com --cdp 9222 against a locally-running Chrome.
  • bridgic-browser search "test" --cdp auto auto-discovers a running browser.
  • bridgic-browser video-start → browse → bridgic-browser video-stop ./out.webm produces a playable file that includes tab switches.
  • bridgic-browser wait --timeout 5 "Submit" respects the custom timeout.
  • Kill remote Chrome mid-session → daemon reconnects on the next command.
  • bridgic-browser close in CDP mode disconnects without killing the remote Chrome.
  • Large page (1000+ refs) snapshot completes within daemon timeout.
  • BRIDGIC_HOME=/tmp/b2 bridgic-browser open https://example.com launches independent daemon instance.

🤖 Generated with Claude Code

NiceCode666 and others added 25 commits April 8, 2026 17:22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…unctionality

- Updated descriptions for CLI commands to clarify usage and parameters, including `fill-form`, `press`, `mouse-down`, `mouse-up`, `wait`, `switch-tab`, and `close-tab`.
- Enhanced the `wait` command to support a custom timeout option, allowing users to specify a maximum wait duration.
- Adjusted the implementation of the `wait` command to include timeout handling in the underlying logic.
- Improved mock setups in tests to reflect changes in command behavior and ensure accurate testing of new functionalities.
- Added a new `--cdp` option to the `search` command, allowing users to connect to an existing browser session instead of launching a new one.
- Updated the command's implementation to handle the new `cdp` parameter and resolve the input correctly.
- Modified CLI command descriptions to reflect the addition of the `--cdp` option.
- Adjusted related tests to verify the correct behavior of the updated `search` command with the new option.
- Enhanced the `fake_read` function in the `TestFindCdpUrl` class to improve browser detection by checking for variations of "chrome" in a case-insensitive manner.
- Added conditions to exclude "canary", "unstable", and "beta" versions of Chrome to ensure accurate URL retrieval for the standard Chrome browser.
- Introduced a new `VideoRecorder` class to handle single-stream video recording on the active tab, allowing seamless switching between tabs while maintaining a continuous output file.
- Updated CLI commands to reflect the new video recording behavior, including enhanced descriptions for `video-start` and `video-stop`.
- Adjusted documentation to clarify that only the active tab is recorded, improving user understanding of the video recording process.
- Enhanced tests to validate the new video recording features and ensure proper functionality across different scenarios.
- Modified tests to use absolute paths for screenshot and video stop commands, ensuring consistent behavior across different environments.
- Added conditional skips for tests that rely on OS-specific functionality, improving compatibility with Windows.
- Refactored filename extraction in snapshot text tests to utilize `os.path.basename` for better cross-platform support.
- Added caching for Playwright browsers in the Windows CI workflow to speed up subsequent runs.
- Changed the CI runner from `windows-latest` to `ubuntu-latest` for better compatibility.
- Refactored path handling in the `_daemon.py` and `_video_recorder.py` files to improve directory access checks and ensure proper handling of local application data.
- Updated tests to use `as_uri()` for generating test URLs, enhancing cross-platform compatibility.
…setup

- Updated the README files to include detailed instructions for enabling remote debugging in Chrome 144+ using the in-browser UI and launch flags for earlier versions.
- Added platform-specific paths for the `DevToolsActivePort` file to improve user guidance.
- Clarified the connection process for `bridgic-browser` with examples and sources for further reading.
- Expanded the `CDP_MODE.md` documentation to cover the same remote debugging setup, ensuring consistency across documentation.
P0-1 snapshot perf: extract _batch_get_elements_info JS to a module-level
constant; pre-build a role index (one querySelectorAll per unique role
instead of per ref); memoize accessible-name / textContent in WeakMaps;
chunk the Python payload (500/batch) with asyncio.sleep(0) between chunks
so other daemon commands can interleave on large pages (~10x speedup for
1000+ refs).

P0-2 click hang: cap click/dblclick/check/uncheck at 10s and fall back to
locator.dispatch_event() on PlaywrightTimeoutError - stops the 30s
actionability retry loop that froze the CLI on Vue/React SPA pages.

P1-3 launch crash loop: wrap launch_persistent_context / launch in
_retriable_launch with exponential backoff (0s -> 1s -> 2.5s) for
transient "SingletonLock" / "target closed" failures; fail fast on other
errors.

P2-4 observability: emit matched [CLI-CMD]/[CLI-RESP] log pairs around
every _dispatch with timing; warn on commands >60s; log _snapshot_lock
wait time when >0.1s.

Adds 21 unit tests covering chunking behaviour, fallback semantics,
retriable token matching, and dispatch log shape.
…three-phase evaluate

Previous fix chunked page.evaluate() calls to yield the event loop between
chunks, but built the roleIndex (querySelectorAll per role) inside each
chunk's JS call. On a 5549-ref page with 12 chunks, the expensive
`div:not([role])` selector ran 12× instead of once, causing 95-123s
runtimes and DAEMON_RESPONSE_TIMEOUT failures.

Fix: split into three phases per snapshot call:
- Phase 1 (_BUILD_ROLE_INDEX_JS): one evaluate that runs QSA for all
  unique roles across the entire batch and stores Element arrays in
  window.__bridgicRoleIndex. Uses a generation token to prevent stale
  data from a previous snapshot bleeding into a new one.
- Phase 2 (_BATCH_INFO_JS, N chunks): reads window.__bridgicRoleIndex
  instead of re-scanning the DOM. Falls back to per-chunk QSA if Phase 1
  was skipped or failed (defensive).
- Phase 3 (_CLEANUP_ROLE_INDEX_JS): deletes window.__bridgic* variables
  in a finally block to free Element references and prevent cross-
  snapshot contamination.

Results on aiqicha company detail page (5549 refs, 12 chunks):
  Before: 95-123s → client DAEMON_RESPONSE_TIMEOUT
  After:  41-50s  → completes within 90s limit

Remaining ~40-50s is getBoundingClientRect × 5549 layout flushes, an
unavoidable DOM API cost.

Also updates TestBatchChunking tests to account for the build+cleanup
calls (N+2 evaluate calls instead of N), adds 4 new tests:
  - test_role_index_built_once_regardless_of_chunks
  - test_build_phase_receives_all_unique_roles
  - test_cleanup_is_last_evaluate_call
  - test_cleanup_runs_on_chunk_exception
…d performance

Added functionality to pre-warm snapshots in the background after navigation, allowing for instant cache hits on subsequent snapshot requests. Introduced a dedicated prefetch generator to avoid conflicts with the main snapshot generator. Implemented cancellation of in-flight prefetch tasks during navigation or page switches to maintain cache integrity. Updated tests to validate the new pre-filtering behavior for interactive and non-interactive elements.
# Conflicts:
#	pyproject.toml
#	uv.lock
…o recorder

- cli/_client: compute per-command socket timeout from `timeout`/`seconds`
  args so long-running `wait` commands do not race the 90s default.
- cli/_daemon: (1) cancel in-flight dispatch when the client disconnects
  mid-request to avoid orphaned tasks against the Browser singleton;
  (2) reject new connections once shutdown begins so back-to-back
  close→open does not crash mid-dispatch; (3) force-reset internal
  handles in `_cdp_reconnect` so `_start()`'s early-return guard cannot
  silently skip reconnect after a partial close; (4) factor out
  `_resolve_cdp_url_from_env` and short-circuit pre-resolved ws URLs.
- session/_browser: (1) add `_probe_cdp_alive` to skip stale
  DevToolsActivePort files (crash / kill -9 leftovers); (2) expand
  Linux CDP scan dirs to cover Snap, Flatpak, Edge, Chrome Beta;
  (3) broaden system-Chrome detection for macOS `~/Applications` and
  Linux Chromium-based binaries; (4) canonicalize host in port-mode URL
  and always rewrite ws URL netloc for SSH-tunnel / port-forward cases.
- session/_download: expose `detach_from_page` for page-scoped cleanup
  in CDP borrowed-context mode.
- session/_snapshot: key the per-snapshot role index / caches on a
  generation token so concurrent snapshot tasks on the same page do
  not clobber each other's window state.
- session/_video_recorder: (1) drain ffmpeg stderr into a capped buffer
  so a full pipe cannot deadlock `stdin.drain()`; (2) harden ffmpeg
  path discovery (X_OK check, Windows fallback, clearer error).
- tests: add unit + integration coverage for all of the above.
- Added a pre-close cancellation of in-flight snapshot prefetch to prevent errors from stale tasks during reconnect.
- Implemented a TCP probe to verify the reachability of WebSocket targets, providing clearer error messages for stale CDP URLs.
- Improved the handling of malformed DevToolsActivePort files to ensure robust URL generation.
- Enhanced scroll race detection in CDP evaluations to prevent executing JavaScript on incorrect elements.
- Added regression tests to ensure proper behavior during video recording and CDP interactions, including handling of closing states and error scenarios.
Add explicit Optional[str] annotation plus pyright ignore on the None
branch so basedpyright stops flagging the validation raise as dead code.
The helper _read_devtools_active_port can legitimately return None for
malformed files, so the check is intentional.
@NiceCode666 NiceCode666 changed the title feat: add CDP connection mode, screencast video recording, and CLI enhancements feat: CDP mode, screencast recorder, snapshot perf, and CLI/daemon hardening Apr 16, 2026
- Updated the SDK and CLI to use `cdp` instead of `cdp_url` for connecting to existing Chrome instances via CDP.
- Adjusted documentation and examples to reflect the new parameter name.
- Ensured backward compatibility by updating relevant methods and tests to handle the new naming convention.
- Added a skip condition for the test that checks for non-executable ffmpeg binaries, specifically for Windows environments, to avoid false failures due to OS limitations.
- snapshot: release _snapshot_lock before awaiting in-flight prefetch to
  avoid a self-deadlock against _pre_warm_snapshot's commit path; add
  regression test.
- snapshot: include STRUCTURAL_NOISE_ROLES in interactive pre-filter and
  run precise visibility checks on INTERACTIVE_ROLES leaf controls so
  off-viewport buttons/links no longer leak into viewport-only snapshots.
- snapshot: only consult interactive_map when the ref is actually in it;
  missing entries fall back to suffix heuristics instead of forcing a
  false negative.
- download: track in-flight per-page download tasks and cancel them on
  detach/close so files are not written after teardown; swallow
  CancelledError instead of reporting download.failure().
- video: remove context-level 'page' listener auto-subscribe; recording
  now hot-swaps only on bridgic-initiated tab switches. Reap ffmpeg via
  wait_for(wait()) on CDP setup failure to avoid zombies.
- cli: redact CDP URL path/query in the browser-closed hint so tokens
  are not echoed back to the user.
- docs: dedupe the active-tab bullet in CDP_MODE.md.
Previously _get_dropdown_option_locators tried locator.locator("option")
first, which matched the hidden native <select> Arco / Element Plus /
AntD host inside their trigger for a11y / form posting. Clicks on those
options are silent no-ops: select_dropdown_option_by_ref would return
"Selected option: X" while the UI stayed unchanged.

Reworked resolution:
  - gate the native path on tagName == "select"
  - for custom comboboxes, prefer aria-controls / aria-owns with a
    visibility filter at the option level (not container level, so
    virtualized portals with 0x0 shells still work)
  - ascend one level to cover AntD / Arco / Element Plus class patterns
    when the aria-controls target contains only hidden ghost options
  - filter [role='option'] descendants strictly visible so nested shadow
    <option> can't win
  - keep the single-visible-listbox fallback

Adds a Section 4 shadow-select fixture and two integration tests covering
the options listing and the actual selection-applied path.
NiceCode666 and others added 9 commits April 22, 2026 11:12
…(H02/H03/M01)

H02 — CDP reconnect across Chrome restart
  - Daemon clears _cdp_resolved + _closing before _start() so reconnect
    re-resolves the ws URL against the live Chrome (fresh browser UUID
    instead of the stale one that 404s).
  - CLI client no longer pre-resolves --cdp; the raw port / "auto" /
    http value is forwarded to the daemon so reconnect can re-probe.
  - CDP scan path also re-probes /json/version for UUID freshness.
  - _is_browser_closed_error walks __cause__ so TargetClosedError
    wrapped by asyncio.gather still classifies as BROWSER_CLOSED.

H03 — click fallback budget
  - Dispatch-event fallback gets an explicit FALLBACK_DISPATCH_TIMEOUT_MS
    (2s) so unstable / disabled elements fail fast instead of pegging
    the main click budget for 10s and pressing the wrong target.

M01 — evaluate_javascript arrow-fn CDP parity
  - _maybe_wrap_arrow_fn normalises bare arrow expressions into Runtime
    function calls, matching Playwright's Launch-mode semantics so the
    same user code works in CDP borrowed mode.
  - asyncio.gather exception context primed via raise-from so originals
    surface in trace instead of being lost.

Tests
  - Unit: browser / cdp_reconnect / cli / find_cdp_url coverage for the
    new paths.
  - Integration: test_click_fallback.py (H03 shake/stable fixtures);
    test_cdp_borrowed_mode arrow-fn parametrize (M01 parity).

Docs
  - docs/CDP_MODE.md Connection drops section reflects reconnect
    re-resolution and the input-form reconnect matrix.

QA tooling (scripts/qa/) committed so clean-clone CI can run the P0
suite and CLI coverage sweep; tmp-upload.txt / cli-full-coverage.html
stay ignored via the prior chore commit.

Version bumped to 0.0.5.dev6.
…ion (task.md P0 gaps)

Three task.md P0 scenarios the existing integration suite did not
exercise, consolidated into already-present test files (no new
per-scenario file proliferation):

- §5.2 TestActionTools::test_click_disabled_button_raises_without_firing_handler
  Clicking <button disabled> must raise; dispatch_event fallback must
  not sneak the onclick past (guards H03 / M01 regression).
- §8.1 TestNavigationTools::test_ref_invalidated_after_go_back
  go_back() drops the snapshot cache; reusing a pre-navigation ref
  must raise instead of silently landing on a same-role element on
  the restored page.
- §2.3 test_cdp_lifecycle::test_cdp_close_does_not_kill_remote_chrome
  bridgic "close" in CDP mode must be pure-disconnect: the borrowed
  Chrome process keeps running and a subsequent --cdp open reattaches
  to the same pid.

Plus an internal cleanup: Chrome lifecycle helpers (pick_free_port,
launch_chrome, wait_for_chrome, kill_chrome) sunk into
tests/integration/_chrome_utils.py so every CDP integration test
shares one implementation.
Covers 3 link modes (persistent / ephemeral / CDP attach) x 2 display modes
(headless / headed) plus a stealth=off smoke, all against real browsers.

- run-mode-matrix.sh: top-level orchestrator for V1..V7 with auto free-port
  selection for CDP variants and per-variant user_data_dir for V7 to avoid
  stealth flag-set collisions.
- run-cli-full-coverage.sh: accept BRIDGIC_QA_* env vars to inject --headed /
  --clear-user-data / --cdp per variant; V1 default preserved.
- run-sdk-coverage.py: cover 11 SDK methods not exposed through the CLI
  (get_snapshot, get_element_by_ref, scroll_to_text, switch_to_page, etc.)
  for V1 / V3 / V5.
- render-mode-matrix-report.py: aggregate per-variant TSVs to a single
  Markdown matrix report with summary and failure details.
- mode-matrix-scenarios.md: document per-variant semantics, prerequisites,
  expected N/A commands, and known limitations.
- CLAUDE.md: surface the mode-matrix entrypoint.
- cli-command-matrix.md: record the 7-variant execution baseline
  (584 data points across CLI + SDK, 0 FAIL, 5 expected N/A).
…etonLock crashes

Headed and headless Chromium can't safely share the same profile dir, so
launch_persistent_context now always lands in <base>/headed or <base>/headless
(applies to both the default ~/.bridgic/bridgic-browser/user_data and a custom
user_data_dir). The public Browser.user_data_dir property is unchanged.

Also fixes a pre-existing test bug: test_navigate_to_empty_url_raises_invalid_input
forgot to mock async_playwright, so navigate_to's _ensure_started() was silently
launching real Chromium and writing to the real profile dir on every unit run.
Verifies restore_storage_state writes localStorage and cookies under each
origin's own scope, not under the currently navigated page's origin.
Allow users to override the root state directory via BRIDGIC_HOME env
var so multiple independent CLI daemon instances can run in parallel,
each with its own socket, logs, user data, and config paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…wser_closed_error

- _browser.py: reset self._closing = False at top of _start() so a
  Browser instance can be reused after close() + restart via
  _ensure_started()
- _daemon.py: rewrite _is_browser_closed_error as iterative loop with
  seen-set guard to prevent stack overflow on circular exception chains

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…one, CDP session leak, video context, download timeout, probe timeout

1. evaluate_javascript() CDP path now checks exceptionDetails and raises
   OperationError instead of silently returning the error description
2. go_back()/go_forward() non-CDP path checks Playwright return value;
   raises StateError(NO_HISTORY_ENTRY) when None (no history)
3. _apply_debugger_skip_pauses uses try/finally to guarantee CDPSession
   detach even on CancelledError
4. start_video() uses page.context instead of self._context to avoid
   None race during concurrent close
5. wait_for_download uses deadline-based single timeout instead of
   double timeout that could wait 2x the expected duration
6. _probe_cdp_alive default timeout now uses _timeouts.CDP_PROBE_S
   instead of hardcoded 2.0s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@NiceCode666 NiceCode666 changed the title feat: CDP mode, screencast recorder, snapshot perf, and CLI/daemon hardening feat: CDP mode, screencast recorder, snapshot perf, multi-instance isolation, and CLI/daemon hardening May 9, 2026
NiceCode666 and others added 7 commits May 9, 2026 14:47
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update Features, Snapshot tool description, and Browser component
sections to use $BRIDGIC_HOME notation consistently.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ADMEs

Document the storage-save/storage-load workflow for sharing login sessions
across browser instances and across headed/headless modes.
page.go_back() returns None for both "no history" and same-document
navigations (e.g. anchor hash changes). Compare URL before/after to
distinguish the two cases.
Adds the CDP-mode skill reference (`skills/bridgic-browser/references/cdp-mode.md`) covering when to use CDP, how to enable it on Chrome, and the connection forms (CLI / SDK / env). Updates the existing CLI / SDK / env-vars references and SKILL.md index to point at the new section.

Adds an API probe test (`tests/integration/test_opener_api_probe.py`) that validates Playwright's opener / page-event / close-event guarantees — foundation for the upcoming owned-page tracking design.
…anup, webdriver removal, IDL truncation

Code (bridgic/browser/session/_stealth.py + _browser.py):

- R1: clean `HeadlessChrome` from UA — context `user_agent` fallback +
  CDP `Emulation.setUserAgentOverride` (anchored CDP session keeps the
  override sticky across navigations; `Network.*` variant drops UAD
  metadata silently in Chromium 145+, so we use Emulation). Headless-only.
- R2: `plugins`/`mimeTypes` get native `PluginArray`/`MimeTypeArray`
  prototype identity via `setPrototypeOf`; `item(i)` does `i >>> 0`
  uint32 truncation per Web IDL §3.2.4 (fixes incolumitas overflowTest
  where `plugins.item(4294967296)` must equal `plugins[0]`).
- R3: `Worker` / `SharedWorker` / `serviceWorker.register` constructor
  wrap injects worker-stealth code via `importScripts` before the
  worker's first instruction (race-proof). Gated to top-frame; further
  filtered to blob:/data:/same-origin URLs only — cross-origin URLs
  pass through unchanged so libraries like reCAPTCHA v3 (which uses
  cross-origin gstatic.com workers) don't hang on CORS-failed
  importScripts. Worker stealth body covers `webdriver`, `deviceMemory`,
  `hardwareConcurrency`, `languages`, `vendor`/`vendorSub`/`productSub`,
  WebGL UNMASKED_VENDOR/RENDERER, and console.* Error stringify.
- R4 + R6: `delete Navigator.prototype.webdriver` (Web IDL §3.7.6
  guarantees configurable: true). Replaces the previous undefined-getter
  approach which kept the descriptor present, leaving `'webdriver' in
  navigator` returning true (an internally inconsistent fingerprint).
  Headless via init script; headed via `_arm_r6_webdriver_delete` —
  `page.evaluate` on the main frame plus `framenavigated` re-apply,
  deliberately not `add_init_script` (would propagate to Cloudflare
  iframes).
- R5-lite: `console.{log,debug,info,warn,error,trace}` pre-stringify
  any Error argument before forwarding. Defeats CDP-attach detection
  via `error.stack` getter trap (deviceandbrowserinfo
  `isAutomatedWithCDP*` test class). In top-frame guard inside
  anti-devtools-detector script, also in worker stealth body.

Iframe-safety invariant (the red line throughout this work):

  "Any patch that can propagate into a cross-origin iframe MUST be
   gated to `self._headless`."

Mode-aware gating: R1 / R3 (post-spawn worker hook) / R5-lite are
headless-only via `_browser.py:_start()` `if self._headless` checks;
the in-script Worker constructor wrap has its own
`if (window === window.top)` guard; R6 in headed uses per-page
`page.evaluate` (per-frame execution context) instead of
`add_init_script` (which would fan out to all frames).

Verified end-to-end on the CloakBrowser-published benchmark
(tests/test_stealth.py):

  bot.sannysoft.com         | 0/57 fail (both modes)
  bot.incolumitas.com       | 0 fail (both modes — overflowTest +
                              WEBDRIVER both fixed; CloakBrowser still
                              shows 1 fail on WEBDRIVER)
  browserscan.net           | 0 abnormal / 19 normal (both modes)
  demo.fingerprint.com      | Pass (headed) — TLS-blocked in headless
  reCAPTCHA v3              | score 0.9 (both modes)

Plus manual verification: Cloudflare Turnstile (chat-auto-team.pages.dev)
issues a 1114-byte token, x.com manual login reaches /home with all
three logged-in selectors present, blog.aepkill.com devtools-detector
reports `status: close`.

Documentation:

- README.md / README_zh.md: new "Anti-Detection" / "反检测" section
  with benchmark table + 24-vector coverage list. Removed the
  CloakBrowser-specific reference sections per scope cleanup.
- docs/INTERNALS.md: extends "Stealth JS Init Script — Patched
  Properties" with new vectors; adds "Mode-aware Stealth Design"
  chapter with decision flow + per-mode patch matrix; documents
  CDP UA cleanup gotchas and worker constructor wrap (incl. the
  same-origin filter rationale).
- docs/KNOWN_LIMITATIONS.md: new entries for TLS fingerprint
  (headless), `isAutomatedWithCDPInWebWorker` (headed), and Worker
  constructor wrap (incl. same-origin filter).
- CLAUDE.md: adds "Iframe-safety rule (CRITICAL)" subsection with the
  per-mechanism matrix and a 5-point checklist for reviewing future
  stealth patches.
@NiceCode666 NiceCode666 changed the title feat: CDP mode, screencast recorder, snapshot perf, multi-instance isolation, and CLI/daemon hardening feat: CDP mode, screencast recorder, snapshot perf, multi-instance isolation, mode-aware anti-detection, and CLI/daemon hardening May 12, 2026
Browser now maintains an internal `_owned_pages: Set[Page]` so all public
tab ops (get_pages / get_tabs / switch_tab / close_tab / _close_page
fallback) only see pages bridgic itself spawned or adopted via opener
relationship. In CDP borrowed mode the user's pre-existing tabs and any
Cmd+click / Cmd+T / address-bar tabs they later open stay invisible to
bridgic — preventing an LLM driving bridgic from switching to, reading,
or closing private work tabs.

Mechanism:
- `context.on("page", _on_new_page)` listener schedules `_maybe_adopt_page`
  which queries `await page.opener()` and adopts iff opener is already
  owned. `_mark_owned` is idempotent and registers a per-page on-close
  pruner.
- Popup-follow: when an adopted popup's opener is `self._page`, the active
  page moves to the popup (mirrors Chrome foreground-promotion UX).
  Disable via `Browser(auto_follow_popups=False)`.
- `_close_page` four-tier fallback when closing the active page:
  opener → focus_stack LRU → owned-first by context order → None.
- CDP-borrowed `_switch_self_page_to` migrates DownloadManager attachment
  so popups don't leak downloads into the user's Chrome default path.
- Triple `_closing` guards in adoption listener prevent race during
  shutdown; navigate_to recovery path uses `_recover_page_in_existing_context`
  to ensure the rebuilt page is correctly marked owned (fix for a real
  ghost-page bug found during manual testing).

Adoption is governed by Chromium's CDP `Target.attachedToTarget.openerId`,
which is populated by foreground-tab navigations (programmatic click, user
plain left-click, `window.open()` with user gesture) and cleared by
background-tab navigations (Cmd/Ctrl+click, middle-click). Verified by
manual 12-case fixture + counter-experiment that:
- `rel="noopener"` / `rel="noreferrer"` / `window.open(...,'noopener')` do
  NOT prevent adoption — they only suppress JS-level `window.opener`, not
  the CDP-level relationship. bridgic reads CDP, so these are ineffective.
- Adoption is role-agnostic, behavior-driven: Playwright-dispatched click
  with `modifiers=['Meta']` is indistinguishable from a real Cmd+click,
  so `bridgic key-down Meta + click` produces an orphan tab bridgic
  cannot see (real but rarely-useful capability — not promoted as API).
- Cmd+T / address bar / history tabs have no opener at all and never
  enter the owned set.

Tests:
- Unit: 1374 passing. New coverage in test_browser_methods.py for owned
  membership (U1-U18), close-fallback chain, popup adoption, race
  guards under _closing, navigate-to recovery, get_tabs hint with
  hidden user tabs.
- New integration suite test_owned_pages.py (I1-I10): user-tab
  invisibility, popup adoption via opener, popup-close fallback,
  download manager follows popup in borrowed mode, non-CDP all-visible
  fallback chain, opener-API probe.
- Pre-existing test_cdp_borrowed_mode.py and test_cdp_cli_full.py are
  module-level skipped — old API contract (user tabs accessible) no
  longer holds; replaced by test_owned_pages.py with new-semantics
  coverage.
- Mode-matrix V5 (CDP attach × headless): FAIL=0.

Docs:
- INTERNALS.md: full Owned-page Tracking section with adoption truth
  table (2-condition rule: opener populated + opener owned), Why
  Cmd-click strips openerId (Chromium NEW_FOREGROUND_TAB vs
  NEW_BACKGROUND_TAB browser-process paths), and tradeoffs.
- CDP_MODE.md / skills/cdp-mode.md: user-facing ✅/❌ adoption table
  including why `rel=noopener` is ineffective at CDP layer.
- CLAUDE.md, README.md, README_zh.md, sdk-guide.md, SKILL.md: synced
  with Cmd+click vs plain left-click distinction.
Replace _maybe_wrap_arrow_fn — which only handled top-level arrow-literal
expressions — with _wrap_js_for_cdp_eval, mirroring Playwright's internal
normalizeEvaluationExpression + utilityScript.evaluate. Arbitrary JS now
round-trips byte-identically between CDP-borrowed Runtime.evaluate and
non-CDP page.evaluate(str):

- Indirect-eval via globalThis.eval(src) yields V8 REPL completion-value
  semantics — IIFEs with trailing `;`, statement lists, class declarations,
  let/const blocks, template literals, async/await all parse and return
  the same as page.evaluate.
- Auto-call when the eval result is a function (matches Playwright's
  isFunction === undefined branch).
- Pre-emptively rewrite Window / Document / Node / Error instances into
  JSON-safe substitutes so common host-object returns stay interchangeable.

Bumps version dev11 → dev13. New integration test
tests/integration/test_evaluate_cdp_parity.py runs a 28-row parity matrix
against a real Chrome Runtime.evaluate session.
…ting

Chrome 138+ honors the user's "Ask where to save each file" preference even
when CDP Browser.setDownloadBehavior(allowAndName, downloadPath=...) is
sent via a browser-level session — the Save As dialog still pops. The fix
is to send that exact command via a PAGE-level CDP session attached to
bridgic's own tab (BrowserContext.new_cdp_session(self._page)); Chrome
treats that as target-scoped and bypasses the user pref. agent-browser's
Some(session_id) argument is the same trick.

Because allowAndName writes files under GUID names, add CdpDownloadRenamer
that subscribes to Browser.downloadWillBegin / downloadProgress on the
same page session, snapshots the suggestedFilename per GUID, and renames
<dir>/<guid> → <dir>/<real name> on completion. Filename sanitization
strips path separators, Windows-forbidden chars, control bytes, and
truncates to 255 bytes. Conflicts resolve to "name (1).ext", "name (2).ext".

Download path resolution for CDP-borrowed mode now follows:
  explicit Browser(downloads_path=...) / config  >
  CLI client's per-command CWD (mirrors curl -O ergonomics)  >
  ~/Downloads
The CLI client puts os.getcwd() in every socket request; the daemon sets
Browser._pending_client_cwd before dispatch so the first lazy-start L1
sees it. The daemon skips its auto-default downloads_path=~/Downloads in
CDP mode — otherwise that default is indistinguishable from a user
configuration and silently wins over CWD priority.

Empirical alternatives tried & rejected (table in CLAUDE.md): Browser
session with/without allowAndName, Page.setDownloadBehavior(allow), and
Browser.setDownloadBehavior with defaultBrowserContextId from
Target.getBrowserContexts (Chrome rejects: "Failed to find browser
context for id").

Docs touched: CLAUDE.md (new Downloads section + 3-mode pipeline table +
empirically-tried-alternatives matrix), README.md / README_zh.md
(download path matrix), docs/API.md (Downloads pipeline overview +
CdpDownloadRenamer note), docs/INTERNALS.md (clarify
_switch_self_page_to DownloadManager swap is now a no-op safety net in
CDP-borrowed mode), docs/KNOWN_LIMITATIONS.md ("Show in Folder" applies
to all pipelines), skills env-vars (downloads_path per-mode behavior).

Tests: 17-case test_cdp_download_renamer.py (sanitize / lifecycle /
rename / conflict / canceled / set_default_dir / robustness); updated
TestCdpDownloadBehaviorOverride to assert allowAndName + eventsEnabled
+ page-session routing; new TestEffectiveCdpDownloadsPath and
TestUpdateCdpDownloadsPath.
Five new integration tests in test_owned_pages.py lock the SDK-exit
cleanup contract in CDP borrowed mode: user's pre-existing tabs survive
bridgic's disconnect, every bridgic-created tab (initial page, new_tab,
adopted popup) is reaped — no residue, no accumulation across sessions,
even when user code crashes mid-task.

Scenarios (each pairs an independent Playwright probe that diffs
Chrome's target multiset before vs after the `async with Browser(...)`):

  I11 clean exit reaps bridgic tabs
  I12 mid-task RuntimeError still triggers reap
  I13 three consecutive sessions — no per-session drift
  I14 popup adopted via `<a target=_blank>` reaped at SDK exit
  I15 user manually closes bridgic's tab → SDK exit still clean

New _chrome_snapshot helper: read-only Counter of every page URL in the
connected Chrome via a separate connect_over_cdp + close() (disconnect
only). The Counter diff is each scenario's primary assertion.

Why these tests didn't already exist: older mock-based tests asserted
`page.close.assert_not_called()` — implementation-bound, silently locked
the leak in. The real-business-code regression (skills.sh scraper
leaking a list tab on every SDK exit) surfaced the gap; the contract is
what should have been tested all along.

Fixture stability: chrome_with_user_tabs now passes
--password-store=basic + --use-mock-keychain so Playwright's bundled
Chromium doesn't silently hang on macOS's keychain prompt during
headless launch (no-op on Linux/Windows CI).

Tests: tests/integration/test_owned_pages.py — 15 passed in 20s
(I1-I10 unchanged + I11-I15 new).
`self._cdp_download_session` is a PAGE-scoped CDP session (attached via
`BrowserContext.new_cdp_session(self._page)`). The previous teardown
order closed every owned page first, then tried to send
`Browser.setDownloadBehavior(default)` on that now-dead session — every
borrowed-mode close logged:

  [CDP pre-close] Browser.setDownloadBehavior failed: CDPSession.send:
  Target page, context or browser has been closed.

Move L3 restore + renamer detach + page-session detach to before the
page.close() loop. L2 orphan rescue stays where it is (it only needs to
run before `browser.close()` destroys the artifactsDir, independent of
page lifecycle).

After the fix the same close path logs:

  [CDP pre-close] restored Chrome native download behavior

No behavior change for owned/launch/persistent modes.
`test_wrapper_parity_with_playwright` flaked on Windows GitHub Actions
runners with TimeoutExpired on the first parametrized case ("spread
args (1 arg)"). Symptoms:

- 28/29 cases passed, only the first (cold-start) case timed out.
- Hit exactly the 10s subprocess.run timeout.
- macOS/Linux runs reliably <500ms per case.

Root cause is Windows-specific cold-start latency: Defender real-time
scan + I/O contention on the runner can push the first `node.exe`
spawn to 5-15s. Subsequent invocations are warm and back to normal.

Bump the cap to 30s. Warm-path runtime is unchanged; the cap only
kicks in on cold runners.
…cklist

Replace the specific chat-auto-team.pages.dev redeem page with a
generic "Cloudflare-Turnstile-protected page" placeholder. The
methodology (3-site headed verification covering Cloudflare /
server-side detection / devtools probe) is unchanged; reviewers pick
their own Cloudflare-protected target.
Previous 10s → 30s bump (commit 7c9ec68) did not stabilize the suite.
Latest Windows CI run failed two parametrized cases ("spread args (1
arg)" + "plain expr") at exactly the 30s ceiling, including the
trivial "document ? 42 : 0" probe that completes in <100ms locally.
Hitting the budget on a near-empty JS payload rules out cold-start /
JIT and points at Windows Defender real-time scan + node.exe spawn
queueing on GitHub Actions runners — a per-spawn cost that simply
raising the timeout cannot solve (next failure would just creep up to
60s, 120s, …).

Skip the whole class on win32 instead. Rationale:
- `_wrap_js_for_cdp_eval` is pure Python string composition; its
  cross-platform behavior is identical, so macOS/Linux CI fully covers
  the regression risk.
- JS semantic parity against the real V8 engine is independently
  verified end-to-end in tests/integration/test_evaluate_cdp_parity.py
  against actual Chrome.
- The Windows-specific node-spawn flake provides no signal we don't
  already have elsewhere.

Also reverts the 30s timeout knob to the original 10s (now that
Windows skips, the cap only governs macOS/Linux behavior, where 10s
has always been comfortable).
@NiceCode666 NiceCode666 merged commit eaf2bf3 into dev May 18, 2026
16 checks passed
NiceCode666 added a commit to dreamluyao/bridgic-browser that referenced this pull request May 19, 2026
…ile-download-failure

Resolved 5 conflicts that came up after dev landed PR bitsky-tech#21 (CDP attach)
and PR bitsky-tech#25 (deps installer):

- bridgic/browser/session/_download.py: kept dev's _pending_waiters
  Future-list + per-page task tracking for wait_for_download(),
  while also keeping PR bitsky-tech#28's _completed_queue used by the new
  wait_for_next_download() — they cover different call sites and now
  coexist. Also drained _completed_queue inside clear_history() so a
  subsequent wait_for_next_download() can't yield a stale completion.
- README.md / README_zh.md: combined PR bitsky-tech#28's "always-on DownloadManager"
  description with dev's two-pipeline / path-matrix CDP docs; merged the
  two code samples into one block that demonstrates wait_for_next_download()
  in non-CDP mode and contrasts the CDP-borrowed flow.
- docs/API.md: kept PR bitsky-tech#28's new SDK methods and corrected the
  download_manager / downloaded_files rows for the merged behavior
  (manager is always created but not attached in CDP-borrowed mode).
- skills/bridgic-browser/references/cli-sdk-api-mapping.md: kept dev's
  updated video-stop semantics and appended PR bitsky-tech#28's download workflow row.

make test-quick: 1249 passed. Tool count = 69.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants