feat(browser): replace abandoned playwright crate with chromiumoxide (CDP) + Rust CI - #488
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
5841157 to
51b1732
Compare
lacymorrow
left a comment
There was a problem hiding this comment.
CTO pre-merge review (LAC-3057)
Reviewed all 8 commits individually (the repo-wide rustfmt commit accounts for most of the 231-file diff; the substantive change is ~6 focused commits). Approving contingent on one fix: the Rust (fmt + clippy + test) job is red — do not merge until it's green.
The CI failure (must fix before merge)
cargo fmt --check fails on src-tauri/mcp-server-os-level/src/platforms/macos/attributes.rs — import ordering of __CFArray in the core_foundation::array use statement. Root cause is toolchain skew, not a bad commit: the branch was formatted with local rustfmt 1.8.0 (rustc 1.92, Dec 2025), while CI's dtolnay/rust-toolchain@stable resolves to current stable, whose rustfmt sorts leading-underscore identifiers first. Your local cargo fmt --check was genuinely clean — against the older rustfmt.
Fix: rustup update stable, re-run cargo fmt, commit the one-hunk reorder. Also add a rust-toolchain.toml pinning the channel (and point the CI action at it) so local and CI can never disagree on formatting again — without the pin this exact failure recurs on every future stable release that touches fmt rules.
What I verified (no changes requested)
- Cleanup semantics (
bd3d18ca): detach-not-close on CDP attach,owns_pagetab ownership, last-handle guard inDropviaArc::strong_count, crypto-provider install innew(). Logic is sound and fails in the safe direction (leak a tab rather than destroy the user's browser). Thestrong_count > 1early-return has a theoretical concurrent-drop race (two clones dropping simultaneously both see count 2 and neither cleans up), but the failure mode is a leaked detach — acceptable; not blocking. - LAC-3055 (
bde1e9e7):propertyvsattributedistinction is correct, mutual-exclusion error is right, and the live suite has a real regression test asserting both the live.valueread and the untouched static attribute (Rule 27 satisfied). - Deleted tests (
fe2f2784): spot-checked —assert!(true)and self-referential mocks. Correct deletion. - Constants parser fix (
51b17329):\s*around:/=is the right minimal fix; generated output confirmed to match committed file. - CI job (
f69b757d/bd3d18ca): macOS runner justified (Apple framework linking), cache keyed on Cargo.lock, cheap gates first, live suite correctly excluded. Good. - Rule 18 parity: public API and interact actions are 1:1 per your enumeration; live suite 7/7 against real Chrome exercises each surface.
- Scope (Rule 28): the rustfmt/clippy/CI-gating commits are prerequisites for gating, not scope creep — coherent and documented.
Once Rust CI is green: merge (keep the branch — LAC-1913), then verify main post-merge per the issue checklist.
…(CDP) playwright 0.0.20 is abandoned upstream and unbuildable: its build script downloads a hardcoded Playwright 1.11.0 driver (May 2021) from playwright.azureedge.net, which Microsoft decommissioned. Every URL variant now 404s, and Microsoft's GitHub releases carry no driver assets. Release builds were already impossible — the crate's /tmp driver cache is gated behind cfg!(debug_assertions), so `tauri build` always attempted the dead download and panicked on "file size is smaller than the driver". Only debug builds worked, and only on machines holding a stale cached driver. Replaces it with chromiumoxide 0.9 (maintained, pure Rust, ~3M downloads): - No bundled Node runtime and no 17.7MB driver embedded via include_bytes! into every Juno binary - Drops a 2021 Chromium/Firefox/WebKit stack out of the agent's browser path - Screenshots return bytes directly, removing the tempfile round-trip All three connection strategies are preserved (CDP attach to a running Chrome, persistent user profile, temp profile, fresh instance), as is the browser/profile detection and temp-profile cleanup logic. Page operations already went through JavaScript evaluation, so they map 1:1 onto chromiumoxide's evaluate_function. Notes: - chromiumoxide returns a Handler that must be polled for the connection to work; spawned via tauri::async_runtime::spawn per CLAUDE.md, and aborted on cleanup/Drop. - Browser::close() needs &mut, so Browser lives behind a mutex. The browser lock is never held while acquiring the page lock (deadlock rules). - AppState no longer needs a playwright_driver field or its lazy init, which removes one lock from the browser-controller init path. cargo check passes. Browser automation itself is untested at runtime and needs QA against a real Chrome. Co-Authored-By: Paperclip <noreply@paperclip.ing>
…e QA Runtime QA of the chromiumoxide port surfaced three defects that pure `cargo check` could not see. Adds the integration suite that found them. Destructive cleanup on an attached browser: `cleanup()` and `Drop` called `browser.close()` unconditionally. Over a CDP attach that sends `Browser.close`, terminating the user's entire Chrome — every window and tab — because an agent task finished. Both paths now detach instead when `connection_method` is an attach. Shared browser torn down by any clone: `BrowserController` derives `Clone` with all state behind `Arc` *and* implements `Drop`, so the first clone dropped tore down the browser every surviving clone was still using. `Drop` now returns early unless it holds the last handle. Page ownership: Fixing the above naively leaked a tab per session. Track `owns_page` — set when we call `new_page`, clear when we adopt `pages[0]` — and close only pages we opened. Verified: tab count is unchanged across a full session. Missing rustls crypto provider: chromiumoxide pulls reqwest 0.13 built with rustls but no bundled provider; such a client panics *on construction*, so every CDP attach would have panicked in the shipped app. Installs a default provider in `BrowserController::new()` (not just `run()` — the CLI, headless mode, and tests never call it). This is what the existing `test_browser_controller_lazy_initialization` was asserting all along. CI: Restores a Rust job, unblocked now that no build script fetches a dead artifact. Runs on macOS (src-tauri links Apple frameworks). Gates `cargo test` only; fmt and clippy stay ungated pending pre-existing debt (3,589 rustfmt hunks over 222 files; 46 clippy warnings, incl. 11 tests whose only assertion is `assert!(true, ...)`), documented inline.
Removes 18 tests that passed unconditionally or exercised only the standard
library, never Juno.
`test_fix_verification.rs` (whole module, 10 tests): every test built a local
mock and asserted against it — `MockVoiceSystem::new().is_ok()`,
`Err(x).is_err()`, that `Arc<Mutex<Vec>>` accepts a push, that three threads
incrementing a mutex reach three. `test_memory_safety_patterns` had no
assertion at all. None of it touched product code; the module was a wall of
`println!("✅ ...")`.
Eight more whose only assertion was `assert!(true, "...")`:
lib.rs::test_compilation_safety
window_management.rs::test_window_manager_safety
startup.rs::test_startup_sequence_creation
state_management.rs::test_state_management_module_compilation
state_management.rs::test_state_summary_structure
menu/tray_menu.rs::test_get_window_states_no_panic
commands/ui_token_selection.rs::test_initialize_ui_token_selection
commands/permissions.rs::test_no_admin_dependency_in_permission_checks
Kept two that do call real code — `test_environment_validation_safety` and
`test_quick_startup_safety` — with the tautological assert stripped and a
comment stating what reaching the end of the function actually proves.
309 -> 291 tests, all passing. The 18 that left contributed no coverage; they
only made the suite look larger than it was.
`cargo clippy --all-targets -- -D warnings` now exits clean (was 46 warnings).
26 were mechanical and applied with `cargo clippy --fix`: redundant `vec!`,
manual range checks, a redundant import, an immediately-dereferenced
reference, `== true`/`== false` comparisons.
The auto-fix introduced a build failure that needed a real fix, not a
rewrite: it turned `assert!(result == true || result == false)` into
`assert!(result || !result)`, which trips the deny-by-default
`overly_complex_bool_expr`. Both forms are tautologies. The test's own
comment says what it meant to check — that the call returns instead of
calling `std::process::exit()` and killing the test runner — so it now says
that and drops the assertion.
Six `_ => assert!(false, "Expected X")` match fallbacks in agent/structs.rs
became `other => panic!("Expected X, got {:?}", other)`, which is idiomatic
and reports what actually arrived. Two `Some(true) => assert!(true)` arms in
cloud/connector.rs became `Some(true) => {}` with the sibling arm widened to
print the unexpected value.
Also replaces a 19th test that asserted nothing. `test_config_validation`
assigned config fields under a comment reading "In a real test, we'd call
validate_tool_choice_config" — that function is 60 lines above it and
perfectly testable. It now covers both ends of the confidence-threshold
range and asserts an invalid mode is named in its own error message.
291 tests pass.
Mechanical only — no behaviour change. 291 tests pass before and after, and this is deliberately its own commit so `git blame` damage is contained to one revision and reviewers can skip it wholesale. 223 files, previously 3,589 unformatted hunks. `cargo fmt --check` now exits clean, which is what lets the next commit gate it in CI. Two spots needed hands. rustfmt aborts with an internal "left behind trailing whitespace" error rather than fixing it, so utils/resource_manager.rs:212 and integration.rs:125 kept the whole run from succeeding until the whitespace was stripped. Sorting the module list in lib.rs also orphaned trailing comments: the notes belonging to `integration` and `scheduler` were concatenated onto `window_management`, leaving one line carrying three unrelated comments. Each is back on the item it describes.
Adds `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` to
the Rust job. Both were verified locally with the exact commands CI runs,
in CI order, before being enabled — no gate lands here unwatched.
fmt runs first because it fails in seconds without compiling anything.
Turning the gate on immediately caught a manifest bug nothing else could see:
tauri-plugin-voice-transcription/Cargo.toml declared
[[example]]
name = "example"
path = "examples/example.rs"
for a file that does not exist — `examples/` contains only `basic-app.tsx`
and `integration.md`. Clippy never noticed because it resolves only the
src-tauri package's targets, while `cargo fmt --all` walks the workspace and
died on the missing path. Any workspace-wide command (`cargo package`,
`cargo fmt --all`) has been broken by this; it simply went unnoticed because
nobody ran one. Removed, with a note against re-adding it without a real file.
…3055) 'attribute' keeps reading static markup via getAttribute; the new 'property' option reads live DOM state (.value, .checked, ...) so an agent can see the result of its own type/toggle actions. Passing both is an error. Live test now reads the typed value straight off the input instead of the oninput mirror div. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rustfmt (now repo-wide on this branch) wraps long const definitions across two lines; the generator's rigid '= "' regexes silently dropped those constants from constants.generated.ts, breaking frontend typecheck. Use \s* around ':' and '=' in both parseEventConstants and parseSimpleConstants. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents local/CI rustfmt disagreements when stable advances. CI's dtolnay/rust-toolchain action honors this file; fmt output is now identical between local and CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rustfmt 1.97 sorts leading-underscore identifiers first within import groups. Apply the expected ordering to pass CI fmt check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ca88a97 to
6609b1f
Compare
Ensures cargo-fmt and cargo-clippy are installed for the pinned 1.97 toolchain in all environments including CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…arnings Rust 1.97 clippy enforces these pre-existing lints as errors: - sort_by with Reverse pattern → sort_by_key (self_awareness_tools, memory_manager) - for (_, v) in map.iter_mut() → map.values_mut() (mcp_integration) - nested if inside match arm → match guard (tool_provider, lib) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing one level of nesting changed line lengths; rustfmt collapses two split expressions to single lines. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Replaces the abandoned
playwrightcrate with chromiumoxide (pure-Rust CDP), which is what finally makes clean and release builds ofsrc-tauripossible again — then runs the browser automation against a real Chrome for the first time and fixes every defect that surfaced. Along the way, restores Rust CI and pays down the test/lint debt that was blocking it.Paperclip: LAC-3057 (landing task) · fixes LAC-3055
Eight commits, each independently reviewable (rebased on
main@9cc7244f):feat(browser)playwright0.0.20 →chromiumoxide0.9fix(browser)teststyle(clippy)style(rustfmt)cifeat(browser)browser_extract_contentgainspropertyoption — read live DOM state (.value,.checked) not just attributes (fixes LAC-3055)fix(scripts)Frontend (typecheck + build)CI failure)Why the dependency swap was mandatory
playwright0.0.20 is abandoned (newest published version). Its build script downloaded a hardcoded 2021 browser driver fromplaywright.azureedge.net, a CDN Microsoft has decommissioned — so its build script panics on any clean checkout. The driver cache was gated behindcfg!(debug_assertions), so release builds never used it and always attempted the dead download:bun run tauri buildhad been impossible for everyone. chromiumoxide speaks CDP directly with no bundled Node and no 17.7MB driver embedded in every binary.Bugs found by running it against a real Chrome
Pure
cargo checkcould not see any of these; they only appeared once something drove the code:cleanup()terminated the user's entire Chrome. Over a CDP attach,Browser.closekills the whole application — every window and tab — because an agent task finished. Now detaches when attached.BrowserControllerisClonewith all state behindArcand has aDropimpl; the first clone dropped killed the browser every other clone was using.Dropnow bails unless it holds the last handle.owns_pageand closes only pages it opened. Regression test asserts the tab count is unchanged across a session — failing in both directions.BrowserController::new().New
src-tauri/tests/browser_cdp_live.rs:#[ignore]d integration tests driving real Chrome on:9222against a fixture served from an ephemeral localhost port. 7/7 pass; verified the user's 22 open tabs survived untouched.Test & lint cleanup (unblocks CI)
test_fix_verification.rsmodule (10 tests exercising local mocks and std-library behavior, never Juno) plus 8 whose only assertion wasassert!(true, "..."). Replaced a 19th (test_config_validation, which set fields and asserted nothing) with a real test ofvalidate_tool_choice_config.deny-by-default build failure (overly_complex_bool_expr) that a blind--fix && commitwould have shipped.git blamedamage is contained.tauri-plugin-voice-transcription/Cargo.tomldeclared an[[example]]pointing at a file that doesn't exist, breaking every workspace-wide cargo command.CI
Restores a
Rust (fmt + clippy + test)job, impossible until the dead build-script download was removed. Runs on macos-latest (src-tauri links Apple frameworks; Linux fails at link time). All three gates verified locally with the exact commands, in CI order, before being enabled.Verification
Re-run in full after the rebase onto
main@9cc7244f(which took dep upgrades #487 and CI fixes #484/#486 — rebase was clean, no conflicts):Behavior parity (Rule 18): old and new
BrowserControllerexpose the identical public surface (navigate,get_current_url,extract_content,interact,screenshot,cleanup) and the identical interact actions (click,type,scroll,select); the live suite exercises each of them.Known caveats
TAURI_SIGNING_PRIVATE_KEY(a CI secret inrelease-tauri.yml); unrelated to code.propertyoption commit. LAC-3056 (rewriterace_condition_tests.rsto test real Juno types instead of std/tokio primitives) remains a follow-up.🤖 Generated with Claude Code