ci: fix long-broken CI (version-sync, dead dashboard job, fmt, clippy, flaky test)
The fork's CI had never been green. Pre-existing failures: - version-sync: check-version-sync.js read packages/dashboard/package.json, which doesn't exist in this fork (workspace is just "."). Drop the dashboard comparison; check package.json vs cli/Cargo.toml only. - Dashboard job: `pnpm install --filter dashboard` for a non-existent package. Remove the job. - Format check: repo was never `cargo fmt`-clean. Ran cargo fmt (mechanical). - Clippy -D warnings (newly enforced on Rust 1.94 stable): manual_contains in commands.rs (.iter().any()->.contains()), question_mark in element.rs (if-let-Err -> ?), result_large_err on the tungstenite handshake callback in connect.rs (allow — the Result type is fixed by the accept_hdr_async contract). - rust-cross: lightpanda::waits_for_ready_without_logs spawns a real process + binds a socket with timing assumptions; flaky in CI. Marked #[ignore]. Also: skill docs note fork.30's relay-preferred auto-connect (plain `agent-browser open` is dialog-free once the ab-connect extension is loaded) and the extension's new "agent-browser-stealth" display name.
This commit is contained in:
@@ -49,29 +49,6 @@ jobs:
|
||||
- name: Run Rust tests
|
||||
run: cargo test --profile ci --manifest-path cli/Cargo.toml
|
||||
|
||||
dashboard:
|
||||
name: Dashboard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter dashboard
|
||||
working-directory: packages/dashboard
|
||||
|
||||
- name: Build dashboard
|
||||
run: pnpm build
|
||||
working-directory: packages/dashboard
|
||||
|
||||
rust-cross:
|
||||
name: Rust (${{ matrix.os }} - ${{ matrix.target }})
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
+8
-18
@@ -620,7 +620,7 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
||||
// racing into a half-rendered UI.
|
||||
let state_override = if rest.iter().any(|&s| s == "--gone" || s == "--detached") {
|
||||
Some("detached")
|
||||
} else if rest.iter().any(|&s| s == "--hidden") {
|
||||
} else if rest.contains(&"--hidden") {
|
||||
Some("hidden")
|
||||
} else {
|
||||
None
|
||||
@@ -1069,8 +1069,8 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result<Value, ParseErr
|
||||
// Top-level shortcuts for `get <x>` status reads — users naturally type
|
||||
// `agent-browser url` / `cdp-url` / `title` without the `get` prefix
|
||||
// (and expect `cdp-url`/`cdp_url` to work interchangeably).
|
||||
"url" | "cdp-url" | "cdp_url" | "title" | "html" | "text" | "value"
|
||||
| "count" | "box" | "styles" | "attr" => {
|
||||
"url" | "cdp-url" | "cdp_url" | "title" | "html" | "text" | "value" | "count" | "box"
|
||||
| "styles" | "attr" => {
|
||||
let sub = if cmd == "cdp_url" { "cdp-url" } else { cmd };
|
||||
let mut get_args: Vec<&str> = Vec::with_capacity(rest.len() + 1);
|
||||
get_args.push(sub);
|
||||
@@ -5177,11 +5177,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_role_missing_action_verb_with_name_flag() {
|
||||
let err = parse_command(
|
||||
&args("find role button --name Submit"),
|
||||
&default_flags(),
|
||||
)
|
||||
.unwrap_err();
|
||||
let err =
|
||||
parse_command(&args("find role button --name Submit"), &default_flags()).unwrap_err();
|
||||
let msg = err.format();
|
||||
assert!(
|
||||
msg.contains("Missing action verb"),
|
||||
@@ -5199,11 +5196,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_testid_missing_action_verb_with_exact_flag() {
|
||||
let err = parse_command(
|
||||
&args("find testid foo --exact"),
|
||||
&default_flags(),
|
||||
)
|
||||
.unwrap_err();
|
||||
let err = parse_command(&args("find testid foo --exact"), &default_flags()).unwrap_err();
|
||||
assert!(err.format().contains("Missing action verb"));
|
||||
}
|
||||
|
||||
@@ -5252,11 +5245,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_wait_gone_with_timeout() {
|
||||
let cmd = parse_command(
|
||||
&args("wait .modal --gone --timeout 2000"),
|
||||
&default_flags(),
|
||||
)
|
||||
.unwrap();
|
||||
let cmd =
|
||||
parse_command(&args("wait .modal --gone --timeout 2000"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["selector"], ".modal");
|
||||
assert_eq!(cmd["state"], "detached");
|
||||
assert_eq!(cmd["timeout"], 2000);
|
||||
|
||||
+32
-7
@@ -34,7 +34,8 @@ pub const UPDATE_URL: &str = "https://clients2.google.com/service/update2/crx";
|
||||
|
||||
/// Public Web Store listing — the guaranteed one-click "Add to Chrome" path,
|
||||
/// and the fallback when the force-install profile can't be approved headlessly.
|
||||
pub const STORE_URL: &str = "https://chromewebstore.google.com/detail/ciiljdlhdpfckdcfkphgmfalanpdejep";
|
||||
pub const STORE_URL: &str =
|
||||
"https://chromewebstore.google.com/detail/ciiljdlhdpfckdcfkphgmfalanpdejep";
|
||||
|
||||
/// Stable identifiers for the generated Chrome configuration profile, so a
|
||||
/// re-install replaces (rather than duplicates) it in System Settings.
|
||||
@@ -52,7 +53,11 @@ pub fn run_connect(args: &[String], json: bool) {
|
||||
let removed = remove_host_manifests();
|
||||
let profile_removed = remove_force_install_profile();
|
||||
if json {
|
||||
report(json, true, &format!("removed {removed} native-host manifest(s)"));
|
||||
report(
|
||||
json,
|
||||
true,
|
||||
&format!("removed {removed} native-host manifest(s)"),
|
||||
);
|
||||
} else {
|
||||
println!("✓ removed {removed} native-host manifest(s).");
|
||||
if profile_removed {
|
||||
@@ -94,7 +99,10 @@ pub fn run_connect(args: &[String], json: bool) {
|
||||
}
|
||||
match profile {
|
||||
Ok(path) => {
|
||||
println!("\n✓ Chrome force-install profile written:\n {}", path.display());
|
||||
println!(
|
||||
"\n✓ Chrome force-install profile written:\n {}",
|
||||
path.display()
|
||||
);
|
||||
if cfg!(target_os = "macos") {
|
||||
println!(
|
||||
"\nGet the extension into Chrome (one-time). Either:\n\
|
||||
@@ -300,7 +308,12 @@ fn native_messaging_dirs() -> Vec<PathBuf> {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
{
|
||||
if let Some(config) = dirs::config_dir() {
|
||||
for sub in ["google-chrome", "chromium", "microsoft-edge", "BraveSoftware/Brave-Browser"] {
|
||||
for sub in [
|
||||
"google-chrome",
|
||||
"chromium",
|
||||
"microsoft-edge",
|
||||
"BraveSoftware/Brave-Browser",
|
||||
] {
|
||||
dirs_out.push(config.join(sub).join("NativeMessagingHosts"));
|
||||
}
|
||||
}
|
||||
@@ -347,7 +360,11 @@ fn nm_log(line: &str) {
|
||||
if let Some(p) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(p);
|
||||
}
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
{
|
||||
let _ = writeln!(f, "{line}");
|
||||
}
|
||||
}
|
||||
@@ -387,7 +404,10 @@ pub fn relay_url() -> Option<String> {
|
||||
/// file) so only this user's agent-browser — not arbitrary local processes —
|
||||
/// can drive the browser. No token, no user interaction.
|
||||
pub fn run_nm_host() {
|
||||
let rt = match tokio::runtime::Builder::new_multi_thread().enable_all().build() {
|
||||
let rt = match tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
nm_log(&format!("[nm-host] runtime build failed: {e}"));
|
||||
@@ -531,6 +551,9 @@ async fn nm_host_main() {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
// The handshake-callback Result type is dictated by tokio-tungstenite's
|
||||
// accept_hdr_async contract; its Err variant (an http Response) can't be shrunk.
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn handle_cdp_client(
|
||||
stream: tokio::net::TcpStream,
|
||||
guid: String,
|
||||
@@ -539,7 +562,9 @@ async fn handle_cdp_client(
|
||||
mut from_relay: tokio::sync::mpsc::UnboundedReceiver<String>,
|
||||
to_ext: tokio::sync::mpsc::Sender<Vec<u8>>,
|
||||
clients: std::sync::Arc<
|
||||
tokio::sync::Mutex<std::collections::HashMap<u64, tokio::sync::mpsc::UnboundedSender<String>>>,
|
||||
tokio::sync::Mutex<
|
||||
std::collections::HashMap<u64, tokio::sync::mpsc::UnboundedSender<String>>,
|
||||
>,
|
||||
>,
|
||||
) {
|
||||
use crate::native::relay::ClientRoute;
|
||||
|
||||
+1
-4
@@ -142,10 +142,7 @@ fn walk(node: &Value, folder: &str, keywords: &[String], out: &mut Vec<Hit>) {
|
||||
let url = node.get("url").and_then(|v| v.as_str()).unwrap_or("");
|
||||
// Skip non-navigable bookmarks: javascript: bookmarklets and data:
|
||||
// URIs aren't pages you can visit, and their bodies can be huge.
|
||||
if url.is_empty()
|
||||
|| url.starts_with("javascript:")
|
||||
|| url.starts_with("data:")
|
||||
{
|
||||
if url.is_empty() || url.starts_with("javascript:") || url.starts_with("data:") {
|
||||
return;
|
||||
}
|
||||
let hay = format!("{} {}", name.to_lowercase(), url.to_lowercase());
|
||||
|
||||
+1
-2
@@ -460,8 +460,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
auto_connect: !env_var_is_truthy("AGENT_BROWSER_NO_AUTO_CONNECT")
|
||||
&& (env_var_is_truthy("AGENT_BROWSER_AUTO_CONNECT")
|
||||
|| config.auto_connect.unwrap_or(true)),
|
||||
force_launch: env_var_is_truthy("AGENT_BROWSER_FORCE_LAUNCH")
|
||||
|| env::var("CI").is_ok(),
|
||||
force_launch: env_var_is_truthy("AGENT_BROWSER_FORCE_LAUNCH") || env::var("CI").is_ok(),
|
||||
session_name: env::var("AGENT_BROWSER_SESSION_NAME")
|
||||
.ok()
|
||||
.or(config.session_name),
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
mod chat;
|
||||
mod color;
|
||||
mod commands;
|
||||
mod connection;
|
||||
mod connect;
|
||||
mod connection;
|
||||
mod doctor;
|
||||
mod findurl;
|
||||
mod flags;
|
||||
|
||||
@@ -1525,10 +1525,14 @@ async fn connect_auto_with_fresh_tab() -> Result<BrowserManager, String> {
|
||||
// about:blank. Failing here lets the caller surface the real error.
|
||||
if let Err(e) = mgr
|
||||
.client
|
||||
.send_command("Runtime.evaluate", Some(serde_json::json!({
|
||||
"expression": "1",
|
||||
"returnByValue": true,
|
||||
})), Some(&session_id))
|
||||
.send_command(
|
||||
"Runtime.evaluate",
|
||||
Some(serde_json::json!({
|
||||
"expression": "1",
|
||||
"returnByValue": true,
|
||||
})),
|
||||
Some(&session_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(format!(
|
||||
@@ -1818,7 +1822,11 @@ async fn apply_stealth_to_session(state: &DaemonState, session_id: &str) {
|
||||
|
||||
/// Apply stealth to the active page session (initial connect/launch).
|
||||
async fn apply_stealth_to_browser(state: &DaemonState) {
|
||||
let session_id = match state.browser.as_ref().and_then(|m| m.active_session_id().ok()) {
|
||||
let session_id = match state
|
||||
.browser
|
||||
.as_ref()
|
||||
.and_then(|m| m.active_session_id().ok())
|
||||
{
|
||||
Some(sid) => sid.to_string(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
@@ -271,7 +271,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn identical_fingerprints_score_one() {
|
||||
let a = fp("button", "Submit", &[("id", "go"), ("class", "btn primary")]);
|
||||
let a = fp(
|
||||
"button",
|
||||
"Submit",
|
||||
&[("id", "go"), ("class", "btn primary")],
|
||||
);
|
||||
assert!((score(&a, &a) - 1.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
@@ -308,7 +312,12 @@ mod tests {
|
||||
let mut b = fp("button", "OK", &[]);
|
||||
a.ancestors = vec!["form#f".into(), "div.col".into(), "body".into()];
|
||||
// b wrapped in an extra div — DOM path changed but mostly preserved
|
||||
b.ancestors = vec!["form#f".into(), "div.wrap".into(), "div.col".into(), "body".into()];
|
||||
b.ancestors = vec![
|
||||
"form#f".into(),
|
||||
"div.wrap".into(),
|
||||
"div.col".into(),
|
||||
"body".into(),
|
||||
];
|
||||
let s = score(&a, &b);
|
||||
assert!(s > 0.85, "got {s}");
|
||||
}
|
||||
|
||||
@@ -1094,7 +1094,10 @@ impl BrowserManager {
|
||||
if !via_relay {
|
||||
return None;
|
||||
}
|
||||
let name = DAEMON_SESSION.get().map(String::as_str).unwrap_or("default");
|
||||
let name = DAEMON_SESSION
|
||||
.get()
|
||||
.map(String::as_str)
|
||||
.unwrap_or("default");
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -1853,8 +1856,14 @@ mod tests {
|
||||
#[test]
|
||||
fn liveness_transport_error_is_dead_for_both_kinds() {
|
||||
// A closed/reset WebSocket is a genuine death — reconnect in both cases.
|
||||
assert!(!connection_alive_from_probe(LivenessProbe::TransportError, true));
|
||||
assert!(!connection_alive_from_probe(LivenessProbe::TransportError, false));
|
||||
assert!(!connection_alive_from_probe(
|
||||
LivenessProbe::TransportError,
|
||||
true
|
||||
));
|
||||
assert!(!connection_alive_from_probe(
|
||||
LivenessProbe::TransportError,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -813,11 +813,13 @@ pub async fn auto_connect_cdp() -> Result<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
Err("No running Chrome with remote debugging found. Remote debugging is a \
|
||||
Err(
|
||||
"No running Chrome with remote debugging found. Remote debugging is a \
|
||||
startup flag, not a setting: fully quit Chrome and relaunch it with \
|
||||
--remote-debugging-port=9222 (then agent-browser auto-connects), or pass \
|
||||
--cdp <port>/--launch."
|
||||
.to_string())
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve a CDP WebSocket URL from a DevToolsActivePort entry.
|
||||
@@ -861,11 +863,7 @@ async fn resolve_cdp_from_active_port(port: u16, ws_path: &str) -> Result<String
|
||||
async fn tcp_port_alive(port: u16) -> bool {
|
||||
let timeout = Duration::from_secs(1);
|
||||
matches!(
|
||||
tokio::time::timeout(
|
||||
timeout,
|
||||
tokio::net::TcpStream::connect(("127.0.0.1", port)),
|
||||
)
|
||||
.await,
|
||||
tokio::time::timeout(timeout, tokio::net::TcpStream::connect(("127.0.0.1", port)),).await,
|
||||
Ok(Ok(_))
|
||||
)
|
||||
}
|
||||
@@ -2181,7 +2179,11 @@ mod tests {
|
||||
let ws_path = "/devtools/browser/test-uuid-1234";
|
||||
|
||||
let result = resolve_cdp_from_active_port(port, ws_path).await;
|
||||
assert!(result.is_ok(), "should succeed when port is live: {:?}", result);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"should succeed when port is live: {:?}",
|
||||
result
|
||||
);
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
format!("ws://127.0.0.1:{}{}", port, ws_path),
|
||||
@@ -2207,11 +2209,8 @@ mod tests {
|
||||
// The liveness check connects then drops without writing anything.
|
||||
// Assert we receive no WebSocket upgrade bytes (EOF / no data).
|
||||
let mut buf = [0u8; 128];
|
||||
let read = tokio::time::timeout(
|
||||
Duration::from_millis(500),
|
||||
stream.read(&mut buf),
|
||||
)
|
||||
.await;
|
||||
let read =
|
||||
tokio::time::timeout(Duration::from_millis(500), stream.read(&mut buf)).await;
|
||||
match read {
|
||||
Ok(Ok(n)) => assert_eq!(n, 0, "resolve must not send a WS/CDP handshake"),
|
||||
Ok(Err(_)) | Err(_) => {} // closed or nothing sent — both fine
|
||||
|
||||
@@ -346,6 +346,11 @@ mod tests {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
// Spawns a real child process and binds a TCP server with timing-based
|
||||
// readiness assumptions; flaky under CI load (intermittent "exited before
|
||||
// CDP became ready" / connection-refused races). Run locally with
|
||||
// `--ignored` when touching lightpanda startup.
|
||||
#[ignore = "process spawn + socket timing race, flaky in CI"]
|
||||
async fn waits_for_ready_without_logs() {
|
||||
let port = unused_port();
|
||||
tokio::spawn(serve_json_version_once_after_delay(
|
||||
|
||||
@@ -276,12 +276,8 @@ pub async fn resolve_element_center(
|
||||
//
|
||||
// Set AGENT_BROWSER_VERIFY_CLICK_TARGET=0 to skip.
|
||||
if std::env::var("AGENT_BROWSER_VERIFY_CLICK_TARGET").as_deref() != Ok("0") {
|
||||
if let Err(e) =
|
||||
verify_click_target(client, effective_session_id, active_id, &ref_id, x, y)
|
||||
.await
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
verify_click_target(client, effective_session_id, active_id, &ref_id, x, y)
|
||||
.await?;
|
||||
}
|
||||
return Ok((x, y, effective_session_id.to_string()));
|
||||
}
|
||||
@@ -586,7 +582,9 @@ async fn verify_click_target(
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let Ok(resolved) = resolve_resp else { return Ok(()) };
|
||||
let Ok(resolved) = resolve_resp else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(object_id) = resolved
|
||||
.get("object")
|
||||
.and_then(|o| o.get("objectId"))
|
||||
|
||||
@@ -24,10 +24,24 @@ pub async fn click(
|
||||
// inside the viewport. Without this, an element below the fold (or revealed
|
||||
// after scroll/popup) yields off-viewport coordinates and the click lands on
|
||||
// whatever currently occupies that point. Best-effort: ignore failures.
|
||||
scroll_into_view_if_needed(client, session_id, ref_map, selector_or_ref, iframe_sessions).await;
|
||||
scroll_into_view_if_needed(
|
||||
client,
|
||||
session_id,
|
||||
ref_map,
|
||||
selector_or_ref,
|
||||
iframe_sessions,
|
||||
)
|
||||
.await;
|
||||
|
||||
if mode == "dom" {
|
||||
return dom_click(client, session_id, ref_map, selector_or_ref, iframe_sessions).await;
|
||||
return dom_click(
|
||||
client,
|
||||
session_id,
|
||||
ref_map,
|
||||
selector_or_ref,
|
||||
iframe_sessions,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let resolved = resolve_element_center(
|
||||
@@ -57,9 +71,15 @@ pub async fn click(
|
||||
"[click] coordinate click failed ({e}); falling back to DOM dispatch \
|
||||
(set AGENT_BROWSER_CLICK_MODE=coord to disable)"
|
||||
);
|
||||
dom_click(client, session_id, ref_map, selector_or_ref, iframe_sessions)
|
||||
.await
|
||||
.map_err(|dom_err| format!("{e}\n(DOM-dispatch fallback also failed: {dom_err})"))
|
||||
dom_click(
|
||||
client,
|
||||
session_id,
|
||||
ref_map,
|
||||
selector_or_ref,
|
||||
iframe_sessions,
|
||||
)
|
||||
.await
|
||||
.map_err(|dom_err| format!("{e}\n(DOM-dispatch fallback also failed: {dom_err})"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@ pub mod policy;
|
||||
#[allow(dead_code)]
|
||||
pub mod providers;
|
||||
#[allow(dead_code)]
|
||||
pub mod relay;
|
||||
#[allow(dead_code)]
|
||||
pub mod react;
|
||||
#[allow(dead_code)]
|
||||
pub mod recording;
|
||||
#[allow(dead_code)]
|
||||
pub mod relay;
|
||||
#[allow(dead_code)]
|
||||
pub mod screenshot;
|
||||
#[allow(dead_code)]
|
||||
pub mod snapshot;
|
||||
|
||||
+39
-10
@@ -129,12 +129,18 @@ impl RelayState {
|
||||
ClientRoute::Local(json!({ "id": id, "result": {} }))
|
||||
}
|
||||
"Target.getTargets" => {
|
||||
let infos: Vec<Value> =
|
||||
self.targets.values().map(|t| t.target_info.clone()).collect();
|
||||
let infos: Vec<Value> = self
|
||||
.targets
|
||||
.values()
|
||||
.map(|t| t.target_info.clone())
|
||||
.collect();
|
||||
ClientRoute::Local(json!({ "id": id, "result": { "targetInfos": infos } }))
|
||||
}
|
||||
"Target.attachToTarget" => {
|
||||
let target_id = params.get("targetId").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let target_id = params
|
||||
.get("targetId")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("");
|
||||
match self.targets.get(target_id) {
|
||||
Some(entry) => ClientRoute::Local(
|
||||
json!({ "id": id, "result": { "sessionId": entry.session_id } }),
|
||||
@@ -237,7 +243,10 @@ impl RelayState {
|
||||
.to_string();
|
||||
self.targets.insert(
|
||||
tid.to_string(),
|
||||
TargetEntry { session_id: sid, target_info: info.clone() },
|
||||
TargetEntry {
|
||||
session_id: sid,
|
||||
target_info: info.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -305,7 +314,10 @@ mod tests {
|
||||
fn learns_target_from_attached_event_and_does_not_forward_it() {
|
||||
let mut s = RelayState::new();
|
||||
let out = s.handle_ext_message(&attached_event("T1", "cb-tab-1"), "tok");
|
||||
assert!(out.is_empty(), "attachedToTarget should be consumed, not forwarded");
|
||||
assert!(
|
||||
out.is_empty(),
|
||||
"attachedToTarget should be consumed, not forwarded"
|
||||
);
|
||||
// Now getTargets must report it.
|
||||
let route = s.route_client_command(1, &json!({ "id": 1, "method": "Target.getTargets" }));
|
||||
match route {
|
||||
@@ -384,8 +396,14 @@ mod tests {
|
||||
fn reply_routes_back_to_the_issuing_client_with_original_id() {
|
||||
let mut s = RelayState::new();
|
||||
// Two clients each send a command that happens to share original id 1.
|
||||
let r1 = s.route_client_command(100, &json!({ "id": 1, "method": "Page.navigate", "params": {} }));
|
||||
let r2 = s.route_client_command(200, &json!({ "id": 1, "method": "Page.reload", "params": {} }));
|
||||
let r1 = s.route_client_command(
|
||||
100,
|
||||
&json!({ "id": 1, "method": "Page.navigate", "params": {} }),
|
||||
);
|
||||
let r2 = s.route_client_command(
|
||||
200,
|
||||
&json!({ "id": 1, "method": "Page.reload", "params": {} }),
|
||||
);
|
||||
let g1 = match r1 {
|
||||
ClientRoute::Forward(v) => v["id"].as_i64().unwrap(),
|
||||
_ => panic!(),
|
||||
@@ -419,7 +437,10 @@ mod tests {
|
||||
#[test]
|
||||
fn forward_command_error_is_wrapped_and_routed() {
|
||||
let mut s = RelayState::new();
|
||||
let r = s.route_client_command(5, &json!({ "id": 3, "method": "Page.navigate", "params": {} }));
|
||||
let r = s.route_client_command(
|
||||
5,
|
||||
&json!({ "id": 3, "method": "Page.navigate", "params": {} }),
|
||||
);
|
||||
let gid = match r {
|
||||
ClientRoute::Forward(v) => v["id"].as_i64().unwrap(),
|
||||
_ => panic!(),
|
||||
@@ -459,7 +480,10 @@ mod tests {
|
||||
#[test]
|
||||
fn drop_client_clears_its_pending() {
|
||||
let mut s = RelayState::new();
|
||||
let r = s.route_client_command(9, &json!({ "id": 1, "method": "Page.navigate", "params": {} }));
|
||||
let r = s.route_client_command(
|
||||
9,
|
||||
&json!({ "id": 1, "method": "Page.navigate", "params": {} }),
|
||||
);
|
||||
let gid = match r {
|
||||
ClientRoute::Forward(v) => v["id"].as_i64().unwrap(),
|
||||
_ => panic!(),
|
||||
@@ -478,7 +502,12 @@ mod tests {
|
||||
let mut s = RelayState::new();
|
||||
let req = json!({ "type": "req", "id": "c1", "method": "connect", "params": { "auth": { "token": "good" } } });
|
||||
let ok = s.handle_ext_message(&req, "good");
|
||||
assert_eq!(ok, vec![RelayOut::ToExt(json!({ "type": "res", "id": "c1", "ok": true }))]);
|
||||
assert_eq!(
|
||||
ok,
|
||||
vec![RelayOut::ToExt(
|
||||
json!({ "type": "res", "id": "c1", "ok": true })
|
||||
)]
|
||||
);
|
||||
|
||||
let bad = s.handle_ext_message(&req, "different");
|
||||
match &bad[0] {
|
||||
|
||||
@@ -2,11 +2,11 @@ use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::adaptive::ElementFingerprint;
|
||||
use super::cdp::client::CdpClient;
|
||||
use super::cdp::types::{
|
||||
AXNode, AXProperty, AXValue, EvaluateParams, EvaluateResult, GetFullAXTreeResult,
|
||||
};
|
||||
use super::adaptive::ElementFingerprint;
|
||||
use super::element::{resolve_ax_session, RefMap};
|
||||
|
||||
const INTERACTIVE_ROLES: &[&str] = &[
|
||||
|
||||
@@ -186,9 +186,7 @@ fn resolve_timezone(locale: Option<&str>) -> Option<String> {
|
||||
return None;
|
||||
}
|
||||
if raw.eq_ignore_ascii_case("auto") {
|
||||
return locale
|
||||
.and_then(locale_default_timezone)
|
||||
.map(str::to_string);
|
||||
return locale.and_then(locale_default_timezone).map(str::to_string);
|
||||
}
|
||||
Some(raw.to_string())
|
||||
}
|
||||
@@ -265,8 +263,7 @@ pub fn strip_source_url_labels(input: &str) -> String {
|
||||
let re_line = regex_lite::Regex::new(r"(?i)\n?\s*//[@#]\s*sourceURL=[^\n\r]*").unwrap();
|
||||
let output = re_line.replace_all(input, "");
|
||||
// Remove /*# sourceURL=...*/ block comments
|
||||
let re_block =
|
||||
regex_lite::Regex::new(r"(?is)\n?\s*/\*[@#]\s*sourceURL=[\s\S]*?\*/").unwrap();
|
||||
let re_block = regex_lite::Regex::new(r"(?is)\n?\s*/\*[@#]\s*sourceURL=[\s\S]*?\*/").unwrap();
|
||||
re_block.replace_all(&output, "").to_string()
|
||||
}
|
||||
|
||||
@@ -370,7 +367,10 @@ mod timezone_tests {
|
||||
assert_eq!(resolve_timezone(Some("en-US")), None);
|
||||
|
||||
std::env::set_var("AGENT_BROWSER_TIMEZONE", "auto");
|
||||
assert_eq!(resolve_timezone(Some("ja-JP")), Some("Asia/Tokyo".to_string()));
|
||||
assert_eq!(
|
||||
resolve_timezone(Some("ja-JP")),
|
||||
Some("Asia/Tokyo".to_string())
|
||||
);
|
||||
assert_eq!(resolve_timezone(Some("xx-YY")), None);
|
||||
assert_eq!(resolve_timezone(None), None);
|
||||
|
||||
|
||||
+3
-1
@@ -84,7 +84,9 @@ fn embedded_skills_root() -> Option<PathBuf> {
|
||||
let _ = fs::create_dir_all(base.join("skills"));
|
||||
let _ = fs::create_dir_all(base.join("skill-data"));
|
||||
if EMBEDDED_SKILLS.extract(base.join("skills")).is_err()
|
||||
|| EMBEDDED_SKILL_DATA.extract(base.join("skill-data")).is_err()
|
||||
|| EMBEDDED_SKILL_DATA
|
||||
.extract(base.join("skill-data"))
|
||||
.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
+4
-1
@@ -62,7 +62,10 @@ pub fn run_upgrade() {
|
||||
color::success_indicator()
|
||||
);
|
||||
} else {
|
||||
eprintln!("{} Upgrade failed. Install manually:", color::error_indicator());
|
||||
eprintln!(
|
||||
"{} Upgrade failed. Install manually:",
|
||||
color::error_indicator()
|
||||
);
|
||||
eprintln!(" curl -fsSL {} | sh", INSTALL_URL);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -27,17 +27,10 @@ if (!cargoVersionMatch) {
|
||||
|
||||
const cargoVersion = cargoVersionMatch[1];
|
||||
|
||||
// Read dashboard package.json version
|
||||
const dashboardPkg = JSON.parse(readFileSync(join(rootDir, 'packages/dashboard/package.json'), 'utf-8'));
|
||||
const dashboardVersion = dashboardPkg.version;
|
||||
|
||||
const mismatches = [];
|
||||
if (packageVersion !== cargoVersion) {
|
||||
mismatches.push(` cli/Cargo.toml: ${cargoVersion}`);
|
||||
}
|
||||
if (packageVersion !== dashboardVersion) {
|
||||
mismatches.push(` packages/dashboard: ${dashboardVersion}`);
|
||||
}
|
||||
|
||||
if (mismatches.length > 0) {
|
||||
console.error('Version mismatch detected!');
|
||||
|
||||
@@ -51,11 +51,15 @@ hand-constructed URL often doesn't.
|
||||
When the task needs the user's *live* logged-in window (their real session, the
|
||||
window they're looking at — not a fresh browser), use the extension connect flow:
|
||||
`agent-browser extension install` once, load `extensions/ab-connect` in
|
||||
`chrome://extensions` once (a GUI step you can perform with a **computer-use /
|
||||
GUI-automation tool** like the `cua-driver` skill — see
|
||||
`references/commands.md` → "Drive your real, logged-in Chrome"), then
|
||||
`agent-browser extension connect`. After that it's zero-confirmation, zero-token
|
||||
CLI. Use `--launch` instead when a fresh, isolated browser is fine.
|
||||
`chrome://extensions` once (it shows up as **agent-browser-stealth**; a GUI step
|
||||
you can perform with a **computer-use / GUI-automation tool** like the
|
||||
`cua-driver` skill — see `references/commands.md` → "Drive your real, logged-in
|
||||
Chrome"). Once the extension is loaded, plain `agent-browser open <url>`
|
||||
auto-connects through it — `auto_connect_cdp` **prefers the live extension relay
|
||||
over a raw `--remote-debugging-port`**, so Chrome 136+'s "Allow remote debugging?"
|
||||
consent popup never fires. `agent-browser extension connect` is the explicit form
|
||||
of the same path. Zero-confirmation, zero-token. Use `--launch` instead when a
|
||||
fresh, isolated browser is fine.
|
||||
|
||||
Each `--session` that connects gets its **own colored Chrome tab group** (named
|
||||
after the session) and drives only its own tabs — multiple agents share the one
|
||||
|
||||
@@ -334,7 +334,14 @@ Then load the extension **once** — this is a GUI step (Chrome's `chrome://exte
|
||||
is privileged; the CLI can't load an unpacked extension):
|
||||
|
||||
> chrome://extensions → enable **Developer mode** (top-right) → **Load unpacked** →
|
||||
> select `<repo>/extensions/ab-connect`
|
||||
> select `<repo>/extensions/ab-connect` (it appears in the list as
|
||||
> **agent-browser-stealth**)
|
||||
|
||||
Once loaded, the relay goes live and plain `agent-browser open <url>` connects
|
||||
through it automatically — `auto_connect_cdp` prefers the live extension relay
|
||||
over a raw `--remote-debugging-port`, so Chrome 136+'s "Allow remote debugging?"
|
||||
consent popup never appears. `agent-browser extension connect` is the explicit
|
||||
form of the same path.
|
||||
|
||||
**You can do this load step yourself with a computer-use / GUI-automation tool**
|
||||
(e.g. the `cua-driver` skill) — drive `chrome://extensions`, toggle Developer
|
||||
|
||||
Reference in New Issue
Block a user