Skip to main content

victauri_browser/
mcp_handler.rs

1use std::sync::Arc;
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use crate::bridge_dispatch::BridgeDispatch;
5use crate::tab_state::TabManager;
6
7/// Strip CSS `/* ... */` comments so a remote-ref scan cannot be evaded by hiding
8/// `@import`/`url(` inside a comment the browser's CSS parser ignores.
9fn strip_css_comments(css: &str) -> String {
10    let bytes = css.as_bytes();
11    let mut out = String::with_capacity(css.len());
12    let mut i = 0;
13    while i < bytes.len() {
14        if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
15            i += 2;
16            while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
17                i += 1;
18            }
19            i = (i + 2).min(bytes.len());
20        } else {
21            out.push(bytes[i] as char);
22            i += 1;
23        }
24    }
25    out
26}
27
28/// Reject injected CSS that fetches a remote origin (`@import` or remote `url(...)`),
29/// unless `allow_remote` is set. The browser extension runs against arbitrary, possibly
30/// hostile websites, so a remote-fetching injection is a data-exfiltration / `SSRF` vector
31/// (especially when chained with page-sourced prompt injection). Relative refs, `data:`
32/// URIs, and `#fragment` refs are allowed.
33fn check_injected_css(css: &str, allow_remote: bool) -> Result<(), String> {
34    const MAX_CSS_LEN: usize = 256 * 1024;
35    if css.len() > MAX_CSS_LEN {
36        return Err(format!(
37            "injected CSS too large ({} bytes, limit {MAX_CSS_LEN})",
38            css.len()
39        ));
40    }
41    if allow_remote {
42        return Ok(());
43    }
44    let scan = strip_css_comments(css).to_ascii_lowercase();
45    if scan.contains("@import") {
46        return Err(
47            "`@import` is blocked in injected CSS (it fetches a remote stylesheet — a \
48                    data-exfiltration vector). Inline the rules, or pass `allow_remote: true`."
49                .to_string(),
50        );
51    }
52    let mut from = 0;
53    while let Some(rel) = scan[from..].find("url(") {
54        let start = from + rel + 4;
55        let end = scan[start..].find(')').map_or(scan.len(), |e| start + e);
56        let arg = scan[start..end].trim().trim_matches(['\'', '"']).trim();
57        if arg.starts_with("//") || arg.contains("://") {
58            return Err(
59                "remote `url(...)` is blocked in injected CSS (it would fetch a remote origin — \
60                 a data-exfiltration vector). Use a relative or data: URL, or pass \
61                 `allow_remote: true`."
62                    .to_string(),
63            );
64        }
65        from = end;
66    }
67    Ok(())
68}
69
70#[derive(Clone)]
71pub struct VictauriBrowserHandler {
72    tab_manager: Arc<TabManager>,
73    dispatch: Arc<BridgeDispatch>,
74    tool_invocations: Arc<AtomicU64>,
75}
76
77impl VictauriBrowserHandler {
78    #[must_use]
79    pub fn new(tab_manager: Arc<TabManager>, dispatch: Arc<BridgeDispatch>) -> Self {
80        Self {
81            tab_manager,
82            dispatch,
83            tool_invocations: Arc::new(AtomicU64::new(0)),
84        }
85    }
86
87    pub async fn tab_count(&self) -> usize {
88        self.tab_manager.tab_count().await
89    }
90
91    #[must_use]
92    pub fn list_tools(&self) -> Vec<ToolInfo> {
93        vec![
94            ToolInfo::new("eval_js", "Execute JavaScript in the active page"),
95            ToolInfo::new("dom_snapshot", "Get accessible DOM tree with ref handles"),
96            ToolInfo::new(
97                "find_elements",
98                "Search DOM elements by text, role, selector, or attribute",
99            ),
100            ToolInfo::new(
101                "interact",
102                "Click, hover, focus, scroll, or select elements",
103            ),
104            ToolInfo::new("input", "Fill, type text, or press keys"),
105            ToolInfo::new(
106                "inspect",
107                "CSS inspection, visual debug, accessibility audit, performance",
108            ),
109            ToolInfo::new("css", "Inject or remove custom CSS"),
110            ToolInfo::new(
111                "logs",
112                "Console, network, navigation, dialog, and event logs",
113            ),
114            ToolInfo::new("storage", "localStorage, sessionStorage, and cookie access"),
115            ToolInfo::new("navigate", "Navigate, go back, manage dialogs"),
116            ToolInfo::new("wait_for", "Wait for DOM conditions, text, or URL changes"),
117            ToolInfo::new(
118                "assert_semantic",
119                "Evaluate expression and assert condition",
120            ),
121            ToolInfo::new("recording", "Record interactions, checkpoint, replay"),
122            ToolInfo::new("screenshot", "Take page screenshot (PNG)"),
123            ToolInfo::new("tabs", "Manage browser tabs and windows"),
124            ToolInfo::new("page_info", "Get page metadata, headers, and resources"),
125            ToolInfo::new("cookies", "Cross-origin cookie management"),
126            ToolInfo::new("get_diagnostics", "Browser and extension diagnostics"),
127            ToolInfo::new("get_plugin_info", "Extension and host version info"),
128            ToolInfo::new("get_memory_stats", "JS heap memory statistics"),
129        ]
130    }
131
132    /// Execute a tool by name with JSON arguments.
133    ///
134    /// Tools that need the browser extension are dispatched via native messaging.
135    /// Tools that are host-only (`plugin_info`, tabs list) are handled locally.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error string if the tool is unknown or execution fails.
140    pub async fn execute_tool(
141        &self,
142        name: &str,
143        args: serde_json::Value,
144    ) -> Result<serde_json::Value, String> {
145        self.tool_invocations.fetch_add(1, Ordering::Relaxed);
146
147        let tab_id = args
148            .get("tab_id")
149            .and_then(serde_json::Value::as_u64)
150            .map(|v| v as u32);
151
152        match name {
153            "get_plugin_info" => Ok(serde_json::json!({
154                "name": "victauri-browser",
155                "version": env!("CARGO_PKG_VERSION"),
156                "mode": "browser",
157                "tool_count": self.list_tools().len(),
158                "tab_count": self.tab_manager.tab_count().await,
159                "invocations": self.tool_invocations.load(Ordering::Relaxed),
160            })),
161
162            "tabs" => {
163                let action = args
164                    .get("action")
165                    .and_then(serde_json::Value::as_str)
166                    .unwrap_or("list");
167                match action {
168                    "list" => {
169                        let tabs = self.tab_manager.list_tabs().await;
170                        Ok(serde_json::to_value(tabs).unwrap_or_default())
171                    }
172                    _ => Err(format!("unknown tabs action: {action}")),
173                }
174            }
175
176            "eval_js" => {
177                let code = args
178                    .get("code")
179                    .and_then(serde_json::Value::as_str)
180                    .ok_or("missing 'code' parameter")?;
181                self.dispatch
182                    .dispatch(tab_id, "eval", serde_json::json!({"code": code}))
183                    .await
184            }
185
186            "dom_snapshot" => {
187                let format = args.get("format").and_then(serde_json::Value::as_str);
188                self.dispatch
189                    .dispatch(tab_id, "snapshot", serde_json::json!({"format": format}))
190                    .await
191            }
192
193            "find_elements" => {
194                let mut query = if args.get("query").is_some() {
195                    args["query"].clone()
196                } else {
197                    args.clone()
198                };
199                if query.get("css").is_none()
200                    && let Some(sel) = query.get("selector").cloned()
201                    && let Some(obj) = query.as_object_mut()
202                {
203                    obj.insert("css".to_string(), sel);
204                }
205                self.dispatch.dispatch(tab_id, "findElements", query).await
206            }
207
208            "interact" => {
209                let action = args
210                    .get("action")
211                    .and_then(serde_json::Value::as_str)
212                    .ok_or("missing 'action' parameter")?;
213                let ref_id = args.get("ref_id").and_then(serde_json::Value::as_str);
214                let timeout_ms = args.get("timeout_ms").and_then(serde_json::Value::as_u64);
215
216                let method = match action {
217                    "click" => "click",
218                    "double_click" => "doubleClick",
219                    "hover" => "hover",
220                    "focus" => "focusElement",
221                    "scroll" | "scroll_into_view" => "scrollTo",
222                    "select" => "selectOption",
223                    _ => return Err(format!("unknown interact action: {action}")),
224                };
225
226                let mut bridge_args = serde_json::json!({});
227                if let Some(r) = ref_id {
228                    bridge_args["ref_id"] = serde_json::Value::String(r.to_string());
229                }
230                if let Some(t) = timeout_ms {
231                    bridge_args["timeout_ms"] = serde_json::Value::Number(t.into());
232                }
233                if let Some(v) = args.get("values") {
234                    bridge_args["values"] = v.clone();
235                }
236                if let Some(x) = args.get("x") {
237                    bridge_args["x"] = x.clone();
238                }
239                if let Some(y) = args.get("y") {
240                    bridge_args["y"] = y.clone();
241                }
242
243                self.dispatch.dispatch(tab_id, method, bridge_args).await
244            }
245
246            "input" => {
247                let action = args
248                    .get("action")
249                    .and_then(serde_json::Value::as_str)
250                    .ok_or("missing 'action' parameter")?;
251
252                match action {
253                    "fill" => {
254                        let ref_id = args
255                            .get("ref_id")
256                            .and_then(serde_json::Value::as_str)
257                            .ok_or("missing 'ref_id'")?;
258                        let value = args
259                            .get("value")
260                            .and_then(serde_json::Value::as_str)
261                            .ok_or("missing 'value'")?;
262                        self.dispatch
263                            .dispatch(
264                                tab_id,
265                                "fill",
266                                serde_json::json!({
267                                    "ref_id": ref_id,
268                                    "value": value,
269                                    "timeout_ms": args.get("timeout_ms"),
270                                }),
271                            )
272                            .await
273                    }
274                    "type" => {
275                        let ref_id = args
276                            .get("ref_id")
277                            .and_then(serde_json::Value::as_str)
278                            .ok_or("missing 'ref_id'")?;
279                        let text = args
280                            .get("text")
281                            .and_then(serde_json::Value::as_str)
282                            .ok_or("missing 'text'")?;
283                        self.dispatch
284                            .dispatch(
285                                tab_id,
286                                "type",
287                                serde_json::json!({
288                                    "ref_id": ref_id,
289                                    "text": text,
290                                    "timeout_ms": args.get("timeout_ms"),
291                                }),
292                            )
293                            .await
294                    }
295                    "press_key" => {
296                        let key = args
297                            .get("key")
298                            .and_then(serde_json::Value::as_str)
299                            .ok_or("missing 'key'")?;
300                        self.dispatch
301                            .dispatch(tab_id, "pressKey", serde_json::json!({"key": key}))
302                            .await
303                    }
304                    "clear" => {
305                        let ref_id = args
306                            .get("ref_id")
307                            .and_then(serde_json::Value::as_str)
308                            .ok_or("missing 'ref_id'")?;
309                        self.dispatch
310                            .dispatch(
311                                tab_id,
312                                "fill",
313                                serde_json::json!({"ref_id": ref_id, "value": ""}),
314                            )
315                            .await
316                    }
317                    _ => Err(format!("unknown input action: {action}")),
318                }
319            }
320
321            "inspect" => {
322                let action = args
323                    .get("action")
324                    .and_then(serde_json::Value::as_str)
325                    .ok_or("missing 'action' parameter")?;
326
327                match action {
328                    "styles" => {
329                        self.dispatch
330                            .dispatch(
331                                tab_id,
332                                "getStyles",
333                                serde_json::json!({
334                                    "ref_id": args.get("ref_id"),
335                                    "properties": args.get("properties"),
336                                }),
337                            )
338                            .await
339                    }
340                    "bounds" => {
341                        self.dispatch
342                            .dispatch(
343                                tab_id,
344                                "getBoundingBoxes",
345                                serde_json::json!({"ref_ids": args.get("ref_ids")}),
346                            )
347                            .await
348                    }
349                    "highlight" => {
350                        self.dispatch
351                            .dispatch(
352                                tab_id,
353                                "highlightElement",
354                                serde_json::json!({
355                                    "ref_id": args.get("ref_id"),
356                                    "color": args.get("color"),
357                                    "label": args.get("label"),
358                                }),
359                            )
360                            .await
361                    }
362                    "clear_highlights" => {
363                        self.dispatch
364                            .dispatch(tab_id, "clearHighlights", serde_json::json!({}))
365                            .await
366                    }
367                    "accessibility" => {
368                        self.dispatch
369                            .dispatch(tab_id, "auditAccessibility", serde_json::json!({}))
370                            .await
371                    }
372                    "performance" => {
373                        self.dispatch
374                            .dispatch(tab_id, "getPerformanceMetrics", serde_json::json!({}))
375                            .await
376                    }
377                    _ => Err(format!("unknown inspect action: {action}")),
378                }
379            }
380
381            "css" => {
382                let action = args
383                    .get("action")
384                    .and_then(serde_json::Value::as_str)
385                    .ok_or("missing 'action' parameter")?;
386
387                match action {
388                    "inject" => {
389                        let css = args
390                            .get("css")
391                            .and_then(serde_json::Value::as_str)
392                            .ok_or("missing 'css'")?;
393                        let allow_remote = args
394                            .get("allow_remote")
395                            .and_then(serde_json::Value::as_bool)
396                            .unwrap_or(false);
397                        check_injected_css(css, allow_remote)?;
398                        self.dispatch
399                            .dispatch(tab_id, "injectCss", serde_json::json!({"css": css}))
400                            .await
401                    }
402                    "remove" => {
403                        self.dispatch
404                            .dispatch(tab_id, "removeInjectedCss", serde_json::json!({}))
405                            .await
406                    }
407                    _ => Err(format!("unknown css action: {action}")),
408                }
409            }
410
411            "logs" => {
412                let action = args
413                    .get("action")
414                    .and_then(serde_json::Value::as_str)
415                    .ok_or("missing 'action' parameter")?;
416
417                match action {
418                    "console" => {
419                        self.dispatch
420                            .dispatch(
421                                tab_id,
422                                "getConsoleLogs",
423                                serde_json::json!({"since": args.get("since")}),
424                            )
425                            .await
426                    }
427                    "network" => {
428                        self.dispatch
429                            .dispatch(
430                                tab_id,
431                                "getNetworkLog",
432                                serde_json::json!({
433                                    "filter": args.get("filter"),
434                                    "limit": args.get("limit"),
435                                }),
436                            )
437                            .await
438                    }
439                    "navigation" => {
440                        self.dispatch
441                            .dispatch(tab_id, "getNavigationLog", serde_json::json!({}))
442                            .await
443                    }
444                    "dialogs" => {
445                        self.dispatch
446                            .dispatch(tab_id, "getDialogLog", serde_json::json!({}))
447                            .await
448                    }
449                    "events" => {
450                        self.dispatch
451                            .dispatch(
452                                tab_id,
453                                "getEventStream",
454                                serde_json::json!({"since": args.get("since")}),
455                            )
456                            .await
457                    }
458                    _ => Err(format!("unknown logs action: {action}")),
459                }
460            }
461
462            "storage" => {
463                let action = args
464                    .get("action")
465                    .and_then(serde_json::Value::as_str)
466                    .ok_or("missing 'action' parameter")?;
467
468                match action {
469                    "get" => {
470                        let store = args
471                            .get("store")
472                            .and_then(serde_json::Value::as_str)
473                            .unwrap_or("local");
474                        let method = if store == "session" {
475                            "getSessionStorage"
476                        } else {
477                            "getLocalStorage"
478                        };
479                        self.dispatch
480                            .dispatch(tab_id, method, serde_json::json!({"key": args.get("key")}))
481                            .await
482                    }
483                    "set" => {
484                        let store = args
485                            .get("store")
486                            .and_then(serde_json::Value::as_str)
487                            .unwrap_or("local");
488                        let method = if store == "session" {
489                            "setSessionStorage"
490                        } else {
491                            "setLocalStorage"
492                        };
493                        self.dispatch
494                            .dispatch(
495                                tab_id,
496                                method,
497                                serde_json::json!({
498                                    "key": args.get("key"),
499                                    "value": args.get("value"),
500                                }),
501                            )
502                            .await
503                    }
504                    "delete" => {
505                        let store = args
506                            .get("store")
507                            .and_then(serde_json::Value::as_str)
508                            .unwrap_or("local");
509                        let method = if store == "session" {
510                            "deleteSessionStorage"
511                        } else {
512                            "deleteLocalStorage"
513                        };
514                        self.dispatch
515                            .dispatch(tab_id, method, serde_json::json!({"key": args.get("key")}))
516                            .await
517                    }
518                    "cookies" => {
519                        self.dispatch
520                            .dispatch(tab_id, "getCookies", serde_json::json!({}))
521                            .await
522                    }
523                    _ => Err(format!("unknown storage action: {action}")),
524                }
525            }
526
527            "navigate" => {
528                let action = args
529                    .get("action")
530                    .and_then(serde_json::Value::as_str)
531                    .ok_or("missing 'action' parameter")?;
532
533                match action {
534                    "go_to" => {
535                        let url = args
536                            .get("url")
537                            .and_then(serde_json::Value::as_str)
538                            .ok_or("missing 'url'")?;
539                        // Only allow http(s) navigation (audit #13): block file:/data:/
540                        // javascript:/chrome: schemes that could read local files or run script.
541                        let scheme_ok = {
542                            let l = url.trim_start().to_ascii_lowercase();
543                            l.starts_with("http://") || l.starts_with("https://")
544                        };
545                        if !scheme_ok {
546                            return Err(
547                                "navigate: only http:// and https:// URLs are allowed".to_string()
548                            );
549                        }
550                        self.dispatch
551                            .dispatch(tab_id, "navigate", serde_json::json!({"url": url}))
552                            .await
553                    }
554                    "back" => {
555                        self.dispatch
556                            .dispatch(tab_id, "navigateBack", serde_json::json!({}))
557                            .await
558                    }
559                    "history" => {
560                        self.dispatch
561                            .dispatch(tab_id, "getNavigationLog", serde_json::json!({}))
562                            .await
563                    }
564                    "dialogs" => {
565                        self.dispatch
566                            .dispatch(tab_id, "getDialogLog", serde_json::json!({}))
567                            .await
568                    }
569                    _ => Err(format!("unknown navigate action: {action}")),
570                }
571            }
572
573            "wait_for" => self.dispatch.dispatch(tab_id, "waitFor", args).await,
574
575            "assert_semantic" => {
576                let expression = args
577                    .get("expression")
578                    .and_then(serde_json::Value::as_str)
579                    .ok_or("missing 'expression'")?;
580                let condition = args
581                    .get("condition")
582                    .and_then(serde_json::Value::as_str)
583                    .ok_or("missing 'condition'")?;
584
585                let eval_result = self
586                    .dispatch
587                    .dispatch(tab_id, "eval", serde_json::json!({"code": expression}))
588                    .await?;
589
590                let actual_str = eval_result
591                    .as_str()
592                    .unwrap_or(&eval_result.to_string())
593                    .to_string();
594
595                let expected = args.get("expected").and_then(serde_json::Value::as_str);
596
597                let passed = match condition {
598                    "equals" => expected.is_some_and(|e| actual_str == e),
599                    "not_equals" => expected.is_some_and(|e| actual_str != e),
600                    "contains" => expected.is_some_and(|e| actual_str.contains(e)),
601                    "truthy" => {
602                        actual_str != "false"
603                            && actual_str != "0"
604                            && actual_str != "null"
605                            && actual_str != "undefined"
606                            && actual_str != "\"\""
607                            && !actual_str.is_empty()
608                    }
609                    "greater_than" => {
610                        if let (Ok(a), Some(Ok(e))) =
611                            (actual_str.parse::<f64>(), expected.map(str::parse::<f64>))
612                        {
613                            a > e
614                        } else {
615                            false
616                        }
617                    }
618                    "less_than" => {
619                        if let (Ok(a), Some(Ok(e))) =
620                            (actual_str.parse::<f64>(), expected.map(str::parse::<f64>))
621                        {
622                            a < e
623                        } else {
624                            false
625                        }
626                    }
627                    _ => return Err(format!("unknown condition: {condition}")),
628                };
629
630                Ok(serde_json::json!({
631                    "passed": passed,
632                    "actual": actual_str,
633                    "expected": expected,
634                    "condition": condition,
635                }))
636            }
637
638            "recording" => {
639                let action = args
640                    .get("action")
641                    .and_then(serde_json::Value::as_str)
642                    .ok_or("missing 'action' parameter")?;
643
644                match action {
645                    "start" | "stop" | "checkpoint" | "get_events" | "list_checkpoints"
646                    | "export" => {
647                        self.dispatch
648                            .dispatch(tab_id, &format!("recording_{action}"), args)
649                            .await
650                    }
651                    _ => Err(format!("unknown recording action: {action}")),
652                }
653            }
654
655            "screenshot" => {
656                self.dispatch
657                    .dispatch(
658                        tab_id,
659                        "screenshot",
660                        serde_json::json!({
661                            "fullPage": args.get("full_page"),
662                        }),
663                    )
664                    .await
665            }
666
667            "page_info" => {
668                self.dispatch
669                    .dispatch(tab_id, "getDiagnostics", serde_json::json!({}))
670                    .await
671            }
672
673            "cookies" => {
674                self.dispatch
675                    .dispatch(tab_id, "getCookies", serde_json::json!({}))
676                    .await
677            }
678
679            "get_diagnostics" => {
680                self.dispatch
681                    .dispatch(tab_id, "getDiagnostics", serde_json::json!({}))
682                    .await
683            }
684
685            "get_memory_stats" => self
686                .dispatch
687                .dispatch(tab_id, "getPerformanceMetrics", serde_json::json!({}))
688                .await
689                .map(|v| {
690                    v.get("js_heap")
691                        .cloned()
692                        .unwrap_or(serde_json::json!({"note": "JS heap stats not available"}))
693                }),
694
695            _ => Err(format!("unknown tool: {name}")),
696        }
697    }
698}
699
700#[derive(Clone, serde::Serialize)]
701pub struct ToolInfo {
702    pub name: String,
703    pub description: String,
704}
705
706impl ToolInfo {
707    fn new(name: &str, description: &str) -> Self {
708        Self {
709            name: name.to_string(),
710            description: description.to_string(),
711        }
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    fn make_handler() -> VictauriBrowserHandler {
720        let tab_mgr = Arc::new(TabManager::new());
721        let dispatch = Arc::new(BridgeDispatch::new(tokio::io::stdout()));
722        VictauriBrowserHandler::new(tab_mgr, dispatch)
723    }
724
725    #[test]
726    fn tool_list_has_20_tools() {
727        let handler = make_handler();
728        assert_eq!(handler.list_tools().len(), 20);
729    }
730
731    #[test]
732    fn injected_css_blocks_remote_refs() {
733        assert!(check_injected_css("@import url(https://evil.com/x.css);", false).is_err());
734        assert!(check_injected_css("/* c */@import 'https://evil.com';", false).is_err());
735        assert!(check_injected_css("body{background:url(https://evil.com/x?d=1)}", false).is_err());
736        assert!(check_injected_css("a{cursor:url(//evil.com/c)}", false).is_err());
737    }
738
739    #[test]
740    fn injected_css_allows_local_and_opt_in() {
741        assert!(check_injected_css("body{color:red}", false).is_ok());
742        assert!(check_injected_css("body{background:url('/a.png')}", false).is_ok());
743        assert!(
744            check_injected_css("body{background:url(data:image/png;base64,AA)}", false).is_ok()
745        );
746        // Opt-in re-enables remote refs.
747        assert!(check_injected_css("@import url(https://cdn.example/x.css);", true).is_ok());
748    }
749
750    #[tokio::test]
751    async fn plugin_info_returns_metadata() {
752        let handler = make_handler();
753        let result = handler
754            .execute_tool("get_plugin_info", serde_json::json!({}))
755            .await
756            .unwrap();
757        assert_eq!(result["name"], "victauri-browser");
758        assert_eq!(result["mode"], "browser");
759        assert_eq!(result["tool_count"], 20);
760    }
761
762    #[tokio::test]
763    async fn unknown_tool_returns_error() {
764        let handler = make_handler();
765        let result = handler
766            .execute_tool("nonexistent", serde_json::json!({}))
767            .await;
768        assert!(result.is_err());
769        assert!(result.unwrap_err().contains("unknown tool"));
770    }
771
772    #[tokio::test]
773    async fn tabs_list_empty() {
774        let handler = make_handler();
775        let result = handler
776            .execute_tool("tabs", serde_json::json!({"action": "list"}))
777            .await
778            .unwrap();
779        assert!(result.as_array().unwrap().is_empty());
780    }
781
782    #[tokio::test]
783    async fn eval_js_requires_code() {
784        let handler = make_handler();
785        let result = handler.execute_tool("eval_js", serde_json::json!({})).await;
786        assert!(result.is_err());
787        assert!(result.unwrap_err().contains("code"));
788    }
789
790    #[tokio::test]
791    async fn interact_requires_action() {
792        let handler = make_handler();
793        let result = handler
794            .execute_tool("interact", serde_json::json!({"ref_id": "e0"}))
795            .await;
796        assert!(result.is_err());
797        assert!(result.unwrap_err().contains("action"));
798    }
799
800    #[tokio::test]
801    async fn plugin_info_increments_invocations() {
802        let handler = make_handler();
803        let r1 = handler
804            .execute_tool("get_plugin_info", serde_json::json!({}))
805            .await
806            .unwrap();
807        assert_eq!(r1["invocations"], 1);
808
809        let r2 = handler
810            .execute_tool("get_plugin_info", serde_json::json!({}))
811            .await
812            .unwrap();
813        assert_eq!(r2["invocations"], 2);
814    }
815
816    #[tokio::test]
817    async fn tabs_unknown_action_errors() {
818        let handler = make_handler();
819        let result = handler
820            .execute_tool("tabs", serde_json::json!({"action": "close"}))
821            .await;
822        assert!(result.is_err());
823        assert!(result.unwrap_err().contains("unknown tabs action"));
824    }
825
826    #[tokio::test]
827    async fn tabs_default_action_is_list() {
828        let handler = make_handler();
829        let result = handler
830            .execute_tool("tabs", serde_json::json!({}))
831            .await
832            .unwrap();
833        assert!(result.as_array().unwrap().is_empty());
834    }
835
836    #[tokio::test]
837    async fn interact_unknown_action_errors() {
838        let handler = make_handler();
839        let result = handler
840            .execute_tool("interact", serde_json::json!({"action": "destroy"}))
841            .await;
842        assert!(result.is_err());
843        assert!(result.unwrap_err().contains("unknown interact action"));
844    }
845
846    #[tokio::test]
847    async fn input_requires_action() {
848        let handler = make_handler();
849        let result = handler
850            .execute_tool("input", serde_json::json!({"ref_id": "e0"}))
851            .await;
852        assert!(result.is_err());
853        assert!(result.unwrap_err().contains("action"));
854    }
855
856    #[tokio::test]
857    async fn input_fill_requires_ref_id() {
858        let handler = make_handler();
859        let result = handler
860            .execute_tool("input", serde_json::json!({"action": "fill", "value": "x"}))
861            .await;
862        assert!(result.is_err());
863        assert!(result.unwrap_err().contains("ref_id"));
864    }
865
866    #[tokio::test]
867    async fn input_fill_requires_value() {
868        let handler = make_handler();
869        let result = handler
870            .execute_tool(
871                "input",
872                serde_json::json!({"action": "fill", "ref_id": "e0"}),
873            )
874            .await;
875        assert!(result.is_err());
876        assert!(result.unwrap_err().contains("value"));
877    }
878
879    #[tokio::test]
880    async fn input_type_requires_ref_id() {
881        let handler = make_handler();
882        let result = handler
883            .execute_tool("input", serde_json::json!({"action": "type", "text": "hi"}))
884            .await;
885        assert!(result.is_err());
886        assert!(result.unwrap_err().contains("ref_id"));
887    }
888
889    #[tokio::test]
890    async fn input_type_requires_text() {
891        let handler = make_handler();
892        let result = handler
893            .execute_tool(
894                "input",
895                serde_json::json!({"action": "type", "ref_id": "e0"}),
896            )
897            .await;
898        assert!(result.is_err());
899        assert!(result.unwrap_err().contains("text"));
900    }
901
902    #[tokio::test]
903    async fn input_press_key_requires_key() {
904        let handler = make_handler();
905        let result = handler
906            .execute_tool("input", serde_json::json!({"action": "press_key"}))
907            .await;
908        assert!(result.is_err());
909        assert!(result.unwrap_err().contains("key"));
910    }
911
912    #[tokio::test]
913    async fn input_clear_requires_ref_id() {
914        let handler = make_handler();
915        let result = handler
916            .execute_tool("input", serde_json::json!({"action": "clear"}))
917            .await;
918        assert!(result.is_err());
919        assert!(result.unwrap_err().contains("ref_id"));
920    }
921
922    #[tokio::test]
923    async fn input_unknown_action_errors() {
924        let handler = make_handler();
925        let result = handler
926            .execute_tool("input", serde_json::json!({"action": "destroy"}))
927            .await;
928        assert!(result.is_err());
929        assert!(result.unwrap_err().contains("unknown input action"));
930    }
931
932    #[tokio::test]
933    async fn inspect_requires_action() {
934        let handler = make_handler();
935        let result = handler
936            .execute_tool("inspect", serde_json::json!({"ref_id": "e0"}))
937            .await;
938        assert!(result.is_err());
939        assert!(result.unwrap_err().contains("action"));
940    }
941
942    #[tokio::test]
943    async fn inspect_unknown_action_errors() {
944        let handler = make_handler();
945        let result = handler
946            .execute_tool("inspect", serde_json::json!({"action": "destroy"}))
947            .await;
948        assert!(result.is_err());
949        assert!(result.unwrap_err().contains("unknown inspect action"));
950    }
951
952    #[tokio::test]
953    async fn css_requires_action() {
954        let handler = make_handler();
955        let result = handler
956            .execute_tool("css", serde_json::json!({"css": "body{}"}))
957            .await;
958        assert!(result.is_err());
959        assert!(result.unwrap_err().contains("action"));
960    }
961
962    #[tokio::test]
963    async fn css_inject_requires_css() {
964        let handler = make_handler();
965        let result = handler
966            .execute_tool("css", serde_json::json!({"action": "inject"}))
967            .await;
968        assert!(result.is_err());
969        assert!(result.unwrap_err().contains("css"));
970    }
971
972    #[tokio::test]
973    async fn css_unknown_action_errors() {
974        let handler = make_handler();
975        let result = handler
976            .execute_tool("css", serde_json::json!({"action": "compile"}))
977            .await;
978        assert!(result.is_err());
979        assert!(result.unwrap_err().contains("unknown css action"));
980    }
981
982    #[tokio::test]
983    async fn logs_requires_action() {
984        let handler = make_handler();
985        let result = handler.execute_tool("logs", serde_json::json!({})).await;
986        assert!(result.is_err());
987        assert!(result.unwrap_err().contains("action"));
988    }
989
990    #[tokio::test]
991    async fn logs_unknown_action_errors() {
992        let handler = make_handler();
993        let result = handler
994            .execute_tool("logs", serde_json::json!({"action": "delete"}))
995            .await;
996        assert!(result.is_err());
997        assert!(result.unwrap_err().contains("unknown logs action"));
998    }
999
1000    #[tokio::test]
1001    async fn storage_requires_action() {
1002        let handler = make_handler();
1003        let result = handler
1004            .execute_tool("storage", serde_json::json!({"key": "x"}))
1005            .await;
1006        assert!(result.is_err());
1007        assert!(result.unwrap_err().contains("action"));
1008    }
1009
1010    #[tokio::test]
1011    async fn storage_unknown_action_errors() {
1012        let handler = make_handler();
1013        let result = handler
1014            .execute_tool("storage", serde_json::json!({"action": "drop"}))
1015            .await;
1016        assert!(result.is_err());
1017        assert!(result.unwrap_err().contains("unknown storage action"));
1018    }
1019
1020    #[tokio::test]
1021    async fn navigate_requires_action() {
1022        let handler = make_handler();
1023        let result = handler
1024            .execute_tool("navigate", serde_json::json!({"url": "https://x.com"}))
1025            .await;
1026        assert!(result.is_err());
1027        assert!(result.unwrap_err().contains("action"));
1028    }
1029
1030    #[tokio::test]
1031    async fn navigate_go_to_requires_url() {
1032        let handler = make_handler();
1033        let result = handler
1034            .execute_tool("navigate", serde_json::json!({"action": "go_to"}))
1035            .await;
1036        assert!(result.is_err());
1037        assert!(result.unwrap_err().contains("url"));
1038    }
1039
1040    #[tokio::test]
1041    async fn navigate_unknown_action_errors() {
1042        let handler = make_handler();
1043        let result = handler
1044            .execute_tool("navigate", serde_json::json!({"action": "refresh"}))
1045            .await;
1046        assert!(result.is_err());
1047        assert!(result.unwrap_err().contains("unknown navigate action"));
1048    }
1049
1050    #[tokio::test]
1051    async fn recording_requires_action() {
1052        let handler = make_handler();
1053        let result = handler
1054            .execute_tool("recording", serde_json::json!({"label": "test"}))
1055            .await;
1056        assert!(result.is_err());
1057        assert!(result.unwrap_err().contains("action"));
1058    }
1059
1060    #[tokio::test]
1061    async fn recording_unknown_action_errors() {
1062        let handler = make_handler();
1063        let result = handler
1064            .execute_tool("recording", serde_json::json!({"action": "rewind"}))
1065            .await;
1066        assert!(result.is_err());
1067        assert!(result.unwrap_err().contains("unknown recording action"));
1068    }
1069
1070    #[tokio::test]
1071    async fn assert_semantic_requires_expression() {
1072        let handler = make_handler();
1073        let result = handler
1074            .execute_tool(
1075                "assert_semantic",
1076                serde_json::json!({"condition": "equals", "expected": "x"}),
1077            )
1078            .await;
1079        assert!(result.is_err());
1080        assert!(result.unwrap_err().contains("expression"));
1081    }
1082
1083    #[tokio::test]
1084    async fn assert_semantic_requires_condition() {
1085        let handler = make_handler();
1086        let result = handler
1087            .execute_tool(
1088                "assert_semantic",
1089                serde_json::json!({"expression": "1+1", "expected": "2"}),
1090            )
1091            .await;
1092        assert!(result.is_err());
1093        assert!(result.unwrap_err().contains("condition"));
1094    }
1095
1096    #[tokio::test]
1097    async fn tool_info_fields() {
1098        let handler = make_handler();
1099        let tools = handler.list_tools();
1100        for tool in &tools {
1101            assert!(!tool.name.is_empty());
1102            assert!(!tool.description.is_empty());
1103        }
1104        let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1105        assert!(names.contains(&"eval_js"));
1106        assert!(names.contains(&"screenshot"));
1107        assert!(names.contains(&"assert_semantic"));
1108    }
1109
1110    // --- Adversarial stress tests ---
1111
1112    /// Helper: creates a handler with a dispatch that we can manually resolve.
1113    /// Spawns `assert_semantic`, intercepts the pending dispatch via `on_response`.
1114    async fn run_assert_semantic(
1115        handler: &VictauriBrowserHandler,
1116        dispatch: &std::sync::Arc<BridgeDispatch>,
1117        eval_return: serde_json::Value,
1118        condition: &str,
1119        expected: Option<&str>,
1120    ) -> Result<serde_json::Value, String> {
1121        let d = dispatch.clone();
1122        let cond = condition.to_string();
1123        let exp = expected.map(str::to_string);
1124
1125        let eval_result = eval_return.clone();
1126        let handler = handler.clone();
1127        let handle = tokio::spawn(async move {
1128            let mut args = serde_json::json!({
1129                "expression": "test_expr",
1130                "condition": cond,
1131            });
1132            if let Some(e) = exp {
1133                args["expected"] = serde_json::Value::String(e);
1134            }
1135            handler.execute_tool("assert_semantic", args).await
1136        });
1137
1138        // Wait briefly for the dispatch to register the pending command
1139        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1140
1141        // Find and resolve the pending command
1142        let ids = d.pending_ids().await;
1143        if let Some(id) = ids.first() {
1144            d.on_response(id, Some(eval_result), None).await;
1145        }
1146
1147        handle.await.unwrap()
1148    }
1149
1150    fn make_handler_with_dispatch() -> (VictauriBrowserHandler, std::sync::Arc<BridgeDispatch>) {
1151        let tab_mgr = std::sync::Arc::new(TabManager::new());
1152        let dispatch = std::sync::Arc::new(BridgeDispatch::new(tokio::io::stdout()));
1153        let handler = VictauriBrowserHandler::new(tab_mgr, dispatch.clone());
1154        (handler, dispatch)
1155    }
1156
1157    #[tokio::test]
1158    async fn assert_semantic_equals_pass() {
1159        let (h, d) = make_handler_with_dispatch();
1160        let result =
1161            run_assert_semantic(&h, &d, serde_json::json!("hello"), "equals", Some("hello"))
1162                .await
1163                .unwrap();
1164        assert_eq!(result["passed"], true);
1165        assert_eq!(result["actual"], "hello");
1166    }
1167
1168    #[tokio::test]
1169    async fn assert_semantic_equals_fail() {
1170        let (h, d) = make_handler_with_dispatch();
1171        let result =
1172            run_assert_semantic(&h, &d, serde_json::json!("hello"), "equals", Some("world"))
1173                .await
1174                .unwrap();
1175        assert_eq!(result["passed"], false);
1176    }
1177
1178    #[tokio::test]
1179    async fn assert_semantic_not_equals_pass() {
1180        let (h, d) = make_handler_with_dispatch();
1181        let result = run_assert_semantic(
1182            &h,
1183            &d,
1184            serde_json::json!("hello"),
1185            "not_equals",
1186            Some("world"),
1187        )
1188        .await
1189        .unwrap();
1190        assert_eq!(result["passed"], true);
1191    }
1192
1193    #[tokio::test]
1194    async fn assert_semantic_not_equals_fail() {
1195        let (h, d) = make_handler_with_dispatch();
1196        let result = run_assert_semantic(
1197            &h,
1198            &d,
1199            serde_json::json!("same"),
1200            "not_equals",
1201            Some("same"),
1202        )
1203        .await
1204        .unwrap();
1205        assert_eq!(result["passed"], false);
1206    }
1207
1208    #[tokio::test]
1209    async fn assert_semantic_contains_pass() {
1210        let (h, d) = make_handler_with_dispatch();
1211        let result = run_assert_semantic(
1212            &h,
1213            &d,
1214            serde_json::json!("hello world"),
1215            "contains",
1216            Some("world"),
1217        )
1218        .await
1219        .unwrap();
1220        assert_eq!(result["passed"], true);
1221    }
1222
1223    #[tokio::test]
1224    async fn assert_semantic_contains_fail() {
1225        let (h, d) = make_handler_with_dispatch();
1226        let result =
1227            run_assert_semantic(&h, &d, serde_json::json!("hello"), "contains", Some("xyz"))
1228                .await
1229                .unwrap();
1230        assert_eq!(result["passed"], false);
1231    }
1232
1233    #[tokio::test]
1234    async fn assert_semantic_truthy_values() {
1235        let (h, d) = make_handler_with_dispatch();
1236
1237        for (val, expected_pass) in [
1238            (serde_json::json!("hello"), true),
1239            (serde_json::json!("1"), true),
1240            (serde_json::json!(42), true),
1241            (serde_json::json!("false"), false),
1242            (serde_json::json!("0"), false),
1243            (serde_json::json!("null"), false),
1244            (serde_json::json!("undefined"), false),
1245        ] {
1246            let result = run_assert_semantic(&h, &d, val.clone(), "truthy", None)
1247                .await
1248                .unwrap();
1249            assert_eq!(
1250                result["passed"], expected_pass,
1251                "truthy check failed for {val:?}, expected passed={expected_pass}",
1252            );
1253        }
1254    }
1255
1256    #[tokio::test]
1257    async fn assert_semantic_greater_than_pass() {
1258        let (h, d) = make_handler_with_dispatch();
1259        let result =
1260            run_assert_semantic(&h, &d, serde_json::json!("42"), "greater_than", Some("10"))
1261                .await
1262                .unwrap();
1263        assert_eq!(result["passed"], true);
1264    }
1265
1266    #[tokio::test]
1267    async fn assert_semantic_greater_than_fail() {
1268        let (h, d) = make_handler_with_dispatch();
1269        let result =
1270            run_assert_semantic(&h, &d, serde_json::json!("5"), "greater_than", Some("10"))
1271                .await
1272                .unwrap();
1273        assert_eq!(result["passed"], false);
1274    }
1275
1276    #[tokio::test]
1277    async fn assert_semantic_greater_than_equal_is_false() {
1278        let (h, d) = make_handler_with_dispatch();
1279        let result =
1280            run_assert_semantic(&h, &d, serde_json::json!("10"), "greater_than", Some("10"))
1281                .await
1282                .unwrap();
1283        assert_eq!(result["passed"], false);
1284    }
1285
1286    #[tokio::test]
1287    async fn assert_semantic_less_than_pass() {
1288        let (h, d) = make_handler_with_dispatch();
1289        let result = run_assert_semantic(&h, &d, serde_json::json!("3"), "less_than", Some("10"))
1290            .await
1291            .unwrap();
1292        assert_eq!(result["passed"], true);
1293    }
1294
1295    #[tokio::test]
1296    async fn assert_semantic_less_than_with_floats() {
1297        let (h, d) = make_handler_with_dispatch();
1298        let result =
1299            run_assert_semantic(&h, &d, serde_json::json!("3.14"), "less_than", Some("3.15"))
1300                .await
1301                .unwrap();
1302        assert_eq!(result["passed"], true);
1303    }
1304
1305    #[tokio::test]
1306    async fn assert_semantic_greater_than_non_numeric_fails() {
1307        let (h, d) = make_handler_with_dispatch();
1308        let result = run_assert_semantic(
1309            &h,
1310            &d,
1311            serde_json::json!("not_a_number"),
1312            "greater_than",
1313            Some("10"),
1314        )
1315        .await
1316        .unwrap();
1317        assert_eq!(result["passed"], false);
1318    }
1319
1320    #[tokio::test]
1321    async fn assert_semantic_unknown_condition() {
1322        let dispatch = std::sync::Arc::new(BridgeDispatch::new(tokio::io::stdout()));
1323        let h =
1324            VictauriBrowserHandler::new(std::sync::Arc::new(TabManager::new()), dispatch.clone());
1325
1326        let handle = tokio::spawn({
1327            let h = h.clone();
1328            async move {
1329                h.execute_tool(
1330                    "assert_semantic",
1331                    serde_json::json!({
1332                        "expression": "1",
1333                        "condition": "banana",
1334                    }),
1335                )
1336                .await
1337            }
1338        });
1339
1340        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1341        let ids = dispatch.pending_ids().await;
1342        if let Some(id) = ids.first() {
1343            dispatch
1344                .on_response(id, Some(serde_json::json!("1")), None)
1345                .await;
1346        }
1347
1348        let result = handle.await.unwrap();
1349        assert!(result.is_err());
1350        assert!(result.unwrap_err().contains("unknown condition"));
1351    }
1352
1353    #[tokio::test]
1354    async fn assert_semantic_equals_without_expected() {
1355        let (h, d) = make_handler_with_dispatch();
1356        let result = run_assert_semantic(&h, &d, serde_json::json!("hello"), "equals", None)
1357            .await
1358            .unwrap();
1359        // equals with no expected should fail (is_some_and returns false)
1360        assert_eq!(result["passed"], false);
1361    }
1362
1363    #[tokio::test]
1364    async fn concurrent_invocation_counter_correctness() {
1365        let tab_mgr = std::sync::Arc::new(TabManager::new());
1366        let dispatch = std::sync::Arc::new(BridgeDispatch::new(tokio::io::stdout()));
1367        let handler = VictauriBrowserHandler::new(tab_mgr, dispatch);
1368
1369        let mut handles = vec![];
1370        for _ in 0..100 {
1371            let h = handler.clone();
1372            handles.push(tokio::spawn(async move {
1373                h.execute_tool("get_plugin_info", serde_json::json!({}))
1374                    .await
1375                    .unwrap()
1376            }));
1377        }
1378
1379        for h in handles {
1380            h.await.unwrap();
1381        }
1382
1383        let final_info = handler
1384            .execute_tool("get_plugin_info", serde_json::json!({}))
1385            .await
1386            .unwrap();
1387        assert_eq!(final_info["invocations"], 101);
1388    }
1389
1390    #[tokio::test]
1391    async fn tabs_list_with_populated_manager() {
1392        let tab_mgr = std::sync::Arc::new(TabManager::new());
1393        tab_mgr.on_tab_created(1, "https://one.com", "One").await;
1394        tab_mgr.on_tab_created(2, "https://two.com", "Two").await;
1395        tab_mgr.on_tab_activated(2).await;
1396
1397        let dispatch = std::sync::Arc::new(BridgeDispatch::new(tokio::io::stdout()));
1398        let handler = VictauriBrowserHandler::new(tab_mgr, dispatch);
1399
1400        let result = handler
1401            .execute_tool("tabs", serde_json::json!({"action": "list"}))
1402            .await
1403            .unwrap();
1404        let tabs = result.as_array().unwrap();
1405        assert_eq!(tabs.len(), 2);
1406
1407        let active: Vec<_> = tabs.iter().filter(|t| t["active"] == true).collect();
1408        assert_eq!(active.len(), 1);
1409        assert_eq!(active[0]["tab_id"], 2);
1410    }
1411
1412    #[tokio::test]
1413    async fn get_memory_stats_extracts_js_heap() {
1414        let (h, d) = make_handler_with_dispatch();
1415
1416        let handle = tokio::spawn({
1417            let h = h.clone();
1418            async move {
1419                h.execute_tool("get_memory_stats", serde_json::json!({}))
1420                    .await
1421            }
1422        });
1423
1424        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1425        let ids = d.pending_ids().await;
1426        let id = ids.first().cloned();
1427
1428        if let Some(id) = id {
1429            d.on_response(
1430                &id,
1431                Some(serde_json::json!({
1432                    "js_heap": {"used_mb": 15.2, "total_mb": 32.0},
1433                    "dom_stats": {"elements": 500},
1434                })),
1435                None,
1436            )
1437            .await;
1438        }
1439
1440        let result = handle.await.unwrap().unwrap();
1441        assert_eq!(result["used_mb"], 15.2);
1442        assert!(result.get("dom_stats").is_none());
1443    }
1444
1445    #[tokio::test]
1446    async fn get_memory_stats_without_js_heap_key() {
1447        let (h, d) = make_handler_with_dispatch();
1448
1449        let handle = tokio::spawn({
1450            let h = h.clone();
1451            async move {
1452                h.execute_tool("get_memory_stats", serde_json::json!({}))
1453                    .await
1454            }
1455        });
1456
1457        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1458        let ids = d.pending_ids().await;
1459        let id = ids.first().cloned();
1460
1461        if let Some(id) = id {
1462            d.on_response(
1463                &id,
1464                Some(serde_json::json!({"dom_stats": {"elements": 100}})),
1465                None,
1466            )
1467            .await;
1468        }
1469
1470        let result = handle.await.unwrap().unwrap();
1471        assert!(result["note"].as_str().unwrap().contains("not available"));
1472    }
1473
1474    #[test]
1475    fn interact_action_routing_coverage() {
1476        let valid = [
1477            "click",
1478            "double_click",
1479            "hover",
1480            "focus",
1481            "scroll",
1482            "scroll_into_view",
1483            "select",
1484        ];
1485        let invalid = ["destroy", "swipe", "pinch", ""];
1486        for action in valid {
1487            let method = match action {
1488                "click" => "click",
1489                "double_click" => "doubleClick",
1490                "hover" => "hover",
1491                "focus" => "focusElement",
1492                "scroll" | "scroll_into_view" => "scrollTo",
1493                "select" => "selectOption",
1494                _ => panic!("unhandled action"),
1495            };
1496            assert!(
1497                !method.is_empty(),
1498                "valid action {action} should map to a method"
1499            );
1500        }
1501        for action in invalid {
1502            assert!(
1503                ![
1504                    "click",
1505                    "double_click",
1506                    "hover",
1507                    "focus",
1508                    "scroll",
1509                    "scroll_into_view",
1510                    "select"
1511                ]
1512                .contains(&action),
1513                "{action} should not be in valid set"
1514            );
1515        }
1516    }
1517
1518    #[test]
1519    fn inspect_action_routing_coverage() {
1520        let valid = [
1521            "styles",
1522            "bounds",
1523            "highlight",
1524            "clear_highlights",
1525            "accessibility",
1526            "performance",
1527        ];
1528        for action in valid {
1529            let is_known = matches!(
1530                action,
1531                "styles"
1532                    | "bounds"
1533                    | "highlight"
1534                    | "clear_highlights"
1535                    | "accessibility"
1536                    | "performance"
1537            );
1538            assert!(is_known, "action {action} not recognized");
1539        }
1540    }
1541
1542    #[test]
1543    fn logs_action_routing_coverage() {
1544        let valid = ["console", "network", "navigation", "dialogs", "events"];
1545        for action in valid {
1546            let is_known = matches!(
1547                action,
1548                "console" | "network" | "navigation" | "dialogs" | "events"
1549            );
1550            assert!(is_known, "action {action} not recognized");
1551        }
1552    }
1553
1554    #[tokio::test]
1555    async fn storage_session_store_routes_correctly() {
1556        let (h, d) = make_handler_with_dispatch();
1557
1558        for action in ["get", "set", "delete"] {
1559            let handle = tokio::spawn({
1560                let h = h.clone();
1561                let action = action.to_string();
1562                async move {
1563                    let mut args = serde_json::json!({"action": action, "store": "session"});
1564                    if action == "set" {
1565                        args["key"] = serde_json::json!("k");
1566                        args["value"] = serde_json::json!("v");
1567                    }
1568                    h.execute_tool("storage", args).await
1569                }
1570            });
1571
1572            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1573            let ids = d.pending_ids().await;
1574
1575            if let Some(id) = ids.first() {
1576                d.on_response(id, Some(serde_json::json!({"ok": true})), None)
1577                    .await;
1578            }
1579
1580            let result = handle.await.unwrap().unwrap();
1581            assert_eq!(result["ok"], true);
1582        }
1583    }
1584
1585    #[test]
1586    fn recording_action_routing_coverage() {
1587        let valid = [
1588            "start",
1589            "stop",
1590            "checkpoint",
1591            "get_events",
1592            "list_checkpoints",
1593            "export",
1594        ];
1595        for action in valid {
1596            let is_known = matches!(
1597                action,
1598                "start" | "stop" | "checkpoint" | "get_events" | "list_checkpoints" | "export"
1599            );
1600            assert!(is_known, "action {action} not recognized");
1601        }
1602    }
1603
1604    #[test]
1605    fn navigate_action_routing_coverage() {
1606        let valid = ["go_to", "back", "history", "dialogs"];
1607        for action in valid {
1608            let is_known = matches!(action, "go_to" | "back" | "history" | "dialogs");
1609            assert!(is_known, "action {action} not recognized");
1610        }
1611    }
1612
1613    // --- Deep assert_semantic edge cases ---
1614
1615    #[tokio::test]
1616    async fn assert_semantic_numeric_from_non_string_value() {
1617        // When eval returns a number (not wrapped in quotes), as_str() returns None
1618        // and we fall through to eval_result.to_string() — this is the numeric path
1619        let (h, d) = make_handler_with_dispatch();
1620        let result = run_assert_semantic(&h, &d, serde_json::json!(42), "greater_than", Some("10"))
1621            .await
1622            .unwrap();
1623        assert_eq!(result["passed"], true);
1624        assert_eq!(result["actual"], "42");
1625    }
1626
1627    #[tokio::test]
1628    async fn assert_semantic_truthy_empty_string_quoted() {
1629        // The literal string "" (two quotes) should be falsy
1630        let (h, d) = make_handler_with_dispatch();
1631        let result = run_assert_semantic(&h, &d, serde_json::json!("\"\""), "truthy", None)
1632            .await
1633            .unwrap();
1634        assert_eq!(result["passed"], false);
1635    }
1636
1637    #[tokio::test]
1638    async fn assert_semantic_truthy_whitespace_is_truthy() {
1639        let (h, d) = make_handler_with_dispatch();
1640        let result = run_assert_semantic(&h, &d, serde_json::json!(" "), "truthy", None)
1641            .await
1642            .unwrap();
1643        assert_eq!(result["passed"], true);
1644    }
1645
1646    #[tokio::test]
1647    async fn assert_semantic_contains_empty_expected() {
1648        // Every string contains ""
1649        let (h, d) = make_handler_with_dispatch();
1650        let result =
1651            run_assert_semantic(&h, &d, serde_json::json!("anything"), "contains", Some(""))
1652                .await
1653                .unwrap();
1654        assert_eq!(result["passed"], true);
1655    }
1656
1657    #[tokio::test]
1658    async fn assert_semantic_greater_than_negative_numbers() {
1659        let (h, d) = make_handler_with_dispatch();
1660        let result =
1661            run_assert_semantic(&h, &d, serde_json::json!("-5"), "greater_than", Some("-10"))
1662                .await
1663                .unwrap();
1664        assert_eq!(result["passed"], true);
1665
1666        let result2 = run_assert_semantic(
1667            &h,
1668            &d,
1669            serde_json::json!("-20"),
1670            "greater_than",
1671            Some("-10"),
1672        )
1673        .await
1674        .unwrap();
1675        assert_eq!(result2["passed"], false);
1676    }
1677
1678    #[tokio::test]
1679    async fn assert_semantic_less_than_zero() {
1680        let (h, d) = make_handler_with_dispatch();
1681        let result = run_assert_semantic(&h, &d, serde_json::json!("-1"), "less_than", Some("0"))
1682            .await
1683            .unwrap();
1684        assert_eq!(result["passed"], true);
1685    }
1686
1687    #[tokio::test]
1688    async fn assert_semantic_equals_with_json_object() {
1689        // When eval returns an object, as_str() is None, so actual_str = to_string()
1690        let (h, d) = make_handler_with_dispatch();
1691        let result = run_assert_semantic(
1692            &h,
1693            &d,
1694            serde_json::json!({"key": "val"}),
1695            "contains",
1696            Some("key"),
1697        )
1698        .await
1699        .unwrap();
1700        assert_eq!(result["passed"], true);
1701    }
1702
1703    #[tokio::test]
1704    async fn assert_semantic_greater_than_infinity() {
1705        let (h, d) = make_handler_with_dispatch();
1706        // "inf" parses as f64::INFINITY in Rust, so inf > 999999 is true
1707        let result = run_assert_semantic(
1708            &h,
1709            &d,
1710            serde_json::json!("inf"),
1711            "greater_than",
1712            Some("999999"),
1713        )
1714        .await
1715        .unwrap();
1716        assert_eq!(result["passed"], true);
1717
1718        // "infinity" also parses
1719        let result2 = run_assert_semantic(
1720            &h,
1721            &d,
1722            serde_json::json!("infinity"),
1723            "greater_than",
1724            Some("999999"),
1725        )
1726        .await
1727        .unwrap();
1728        assert_eq!(result2["passed"], true);
1729
1730        // "NaN" parses but NaN > x is always false
1731        let result3 =
1732            run_assert_semantic(&h, &d, serde_json::json!("NaN"), "greater_than", Some("0"))
1733                .await
1734                .unwrap();
1735        assert_eq!(result3["passed"], false);
1736    }
1737
1738    #[tokio::test]
1739    async fn assert_semantic_not_equals_with_no_expected() {
1740        let (h, d) = make_handler_with_dispatch();
1741        // not_equals with no expected — is_some_and returns false
1742        let result = run_assert_semantic(&h, &d, serde_json::json!("x"), "not_equals", None)
1743            .await
1744            .unwrap();
1745        assert_eq!(result["passed"], false);
1746    }
1747
1748    #[tokio::test]
1749    async fn assert_semantic_contains_case_sensitive() {
1750        let (h, d) = make_handler_with_dispatch();
1751        let result = run_assert_semantic(
1752            &h,
1753            &d,
1754            serde_json::json!("Hello World"),
1755            "contains",
1756            Some("hello"),
1757        )
1758        .await
1759        .unwrap();
1760        // Contains is case-sensitive
1761        assert_eq!(result["passed"], false);
1762    }
1763
1764    // --- Type confusion & adversarial argument tests ---
1765
1766    #[tokio::test]
1767    async fn tool_with_action_as_number_errors() {
1768        let handler = make_handler();
1769        // action field is a number instead of string
1770        let result = handler
1771            .execute_tool(
1772                "interact",
1773                serde_json::json!({"action": 42, "ref_id": "e0"}),
1774            )
1775            .await;
1776        // as_str on number returns None → "missing action"
1777        assert!(result.is_err());
1778        assert!(result.unwrap_err().contains("action"));
1779    }
1780
1781    #[tokio::test]
1782    async fn tool_with_action_as_array_errors() {
1783        let handler = make_handler();
1784        let result = handler
1785            .execute_tool(
1786                "input",
1787                serde_json::json!({"action": ["fill"], "ref_id": "e0"}),
1788            )
1789            .await;
1790        assert!(result.is_err());
1791        assert!(result.unwrap_err().contains("action"));
1792    }
1793
1794    #[tokio::test]
1795    async fn tool_with_action_as_null_errors() {
1796        let handler = make_handler();
1797        let result = handler
1798            .execute_tool("css", serde_json::json!({"action": null}))
1799            .await;
1800        assert!(result.is_err());
1801        assert!(result.unwrap_err().contains("action"));
1802    }
1803
1804    #[tokio::test]
1805    async fn eval_js_code_as_number_errors() {
1806        let handler = make_handler();
1807        let result = handler
1808            .execute_tool("eval_js", serde_json::json!({"code": 42}))
1809            .await;
1810        assert!(result.is_err());
1811        assert!(result.unwrap_err().contains("code"));
1812    }
1813
1814    #[tokio::test]
1815    async fn eval_js_code_as_object_errors() {
1816        let handler = make_handler();
1817        let result = handler
1818            .execute_tool("eval_js", serde_json::json!({"code": {"expr": "1+1"}}))
1819            .await;
1820        assert!(result.is_err());
1821        assert!(result.unwrap_err().contains("code"));
1822    }
1823
1824    #[tokio::test]
1825    async fn navigate_go_to_url_as_object_errors() {
1826        let handler = make_handler();
1827        let result = handler
1828            .execute_tool(
1829                "navigate",
1830                serde_json::json!({"action": "go_to", "url": {"href": "https://x.com"}}),
1831            )
1832            .await;
1833        assert!(result.is_err());
1834        assert!(result.unwrap_err().contains("url"));
1835    }
1836
1837    #[tokio::test]
1838    async fn input_fill_value_as_number_errors() {
1839        let handler = make_handler();
1840        let result = handler
1841            .execute_tool(
1842                "input",
1843                serde_json::json!({"action": "fill", "ref_id": "e0", "value": 42}),
1844            )
1845            .await;
1846        assert!(result.is_err());
1847        assert!(result.unwrap_err().contains("value"));
1848    }
1849
1850    #[tokio::test]
1851    async fn css_inject_css_as_array_errors() {
1852        let handler = make_handler();
1853        let result = handler
1854            .execute_tool(
1855                "css",
1856                serde_json::json!({"action": "inject", "css": ["body{}", "div{}"]}),
1857            )
1858            .await;
1859        assert!(result.is_err());
1860        assert!(result.unwrap_err().contains("css"));
1861    }
1862
1863    #[tokio::test]
1864    async fn unknown_tool_name_errors() {
1865        let handler = make_handler();
1866        let result = handler
1867            .execute_tool("drop_table", serde_json::json!({}))
1868            .await;
1869        assert!(result.is_err());
1870        assert!(result.unwrap_err().contains("unknown tool"));
1871    }
1872
1873    #[tokio::test]
1874    async fn empty_tool_name_errors() {
1875        let handler = make_handler();
1876        let result = handler.execute_tool("", serde_json::json!({})).await;
1877        assert!(result.is_err());
1878        assert!(result.unwrap_err().contains("unknown tool"));
1879    }
1880
1881    #[tokio::test]
1882    async fn tool_name_case_sensitive() {
1883        let handler = make_handler();
1884        // Tools are case-sensitive — "Eval_Js" is not "eval_js"
1885        let result = handler
1886            .execute_tool("Eval_Js", serde_json::json!({"code": "1"}))
1887            .await;
1888        assert!(result.is_err());
1889        assert!(result.unwrap_err().contains("unknown tool"));
1890    }
1891
1892    #[tokio::test]
1893    async fn tool_invocations_count_includes_failures() {
1894        let handler = make_handler();
1895        // Even failed calls should increment the counter
1896        let _ = handler
1897            .execute_tool("nonexistent", serde_json::json!({}))
1898            .await;
1899        let _ = handler.execute_tool("eval_js", serde_json::json!({})).await;
1900        let info = handler
1901            .execute_tool("get_plugin_info", serde_json::json!({}))
1902            .await
1903            .unwrap();
1904        assert_eq!(info["invocations"], 3);
1905    }
1906
1907    #[tokio::test]
1908    async fn tabs_with_populated_manager_shows_count() {
1909        let tab_mgr = Arc::new(TabManager::new());
1910        let dispatch = Arc::new(BridgeDispatch::new(tokio::io::stdout()));
1911
1912        tab_mgr.on_tab_created(1, "https://a.com", "A").await;
1913        tab_mgr.on_tab_created(2, "https://b.com", "B").await;
1914
1915        let handler = VictauriBrowserHandler::new(tab_mgr, dispatch);
1916        let info = handler
1917            .execute_tool("get_plugin_info", serde_json::json!({}))
1918            .await
1919            .unwrap();
1920        assert_eq!(info["tab_count"], 2);
1921    }
1922
1923    #[tokio::test]
1924    async fn tabs_list_with_active_tab_marked() {
1925        let tab_mgr = Arc::new(TabManager::new());
1926        let dispatch = Arc::new(BridgeDispatch::new(tokio::io::stdout()));
1927
1928        tab_mgr.on_tab_created(10, "https://a.com", "A").await;
1929        tab_mgr.on_tab_created(20, "https://b.com", "B").await;
1930        tab_mgr.on_tab_activated(20).await;
1931
1932        let handler = VictauriBrowserHandler::new(tab_mgr, dispatch);
1933        let result = handler
1934            .execute_tool("tabs", serde_json::json!({"action": "list"}))
1935            .await
1936            .unwrap();
1937        let tabs = result.as_array().unwrap();
1938        let active: Vec<_> = tabs.iter().filter(|t| t["active"] == true).collect();
1939        assert_eq!(active.len(), 1);
1940        assert_eq!(active[0]["tab_id"], 20);
1941    }
1942
1943    #[tokio::test]
1944    async fn all_action_tools_reject_empty_string_action() {
1945        let handler = make_handler();
1946        let tools_with_actions = [
1947            "interact",
1948            "input",
1949            "inspect",
1950            "css",
1951            "logs",
1952            "storage",
1953            "navigate",
1954            "recording",
1955        ];
1956        for tool in tools_with_actions {
1957            let result = handler
1958                .execute_tool(tool, serde_json::json!({"action": ""}))
1959                .await;
1960            assert!(result.is_err(), "{tool} should reject empty string action");
1961            let err = result.unwrap_err();
1962            assert!(
1963                err.contains("unknown") || err.contains("action"),
1964                "{tool} error should mention action: {err}"
1965            );
1966        }
1967    }
1968
1969    #[tokio::test]
1970    async fn concurrent_tool_invocation_counter() {
1971        let handler = Arc::new(make_handler());
1972        let mut handles = vec![];
1973        for _ in 0..100 {
1974            let h = Arc::clone(&handler);
1975            handles.push(tokio::spawn(async move {
1976                h.execute_tool("get_plugin_info", serde_json::json!({}))
1977                    .await
1978                    .unwrap()
1979            }));
1980        }
1981        for h in handles {
1982            h.await.unwrap();
1983        }
1984        let info = handler
1985            .execute_tool("get_plugin_info", serde_json::json!({}))
1986            .await
1987            .unwrap();
1988        assert_eq!(info["invocations"], 101);
1989    }
1990
1991    #[tokio::test]
1992    async fn all_bridge_tools_dispatch_recognized() {
1993        // Verify every tool that dispatches to the bridge is recognized (not "unknown tool").
1994        // Uses a spawned task to resolve the pending command immediately with mock data.
1995        let tab_mgr = Arc::new(TabManager::new());
1996        let dispatch = Arc::new(BridgeDispatch::new(tokio::io::stdout()));
1997        let handler = VictauriBrowserHandler::new(Arc::clone(&tab_mgr), Arc::clone(&dispatch));
1998
1999        let bridge_tools: Vec<(&str, serde_json::Value)> = vec![
2000            ("get_diagnostics", serde_json::json!({})),
2001            ("get_memory_stats", serde_json::json!({})),
2002            ("screenshot", serde_json::json!({})),
2003            ("page_info", serde_json::json!({})),
2004            ("cookies", serde_json::json!({})),
2005            ("dom_snapshot", serde_json::json!({})),
2006            ("find_elements", serde_json::json!({"text": "x"})),
2007            (
2008                "wait_for",
2009                serde_json::json!({"condition": "selector", "value": "body"}),
2010            ),
2011        ];
2012
2013        for (tool_name, args) in bridge_tools {
2014            let d = Arc::clone(&dispatch);
2015            // Spawn a task that resolves whatever pending command appears
2016            let resolver = tokio::spawn(async move {
2017                tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2018                let ids = d.pending_ids().await;
2019                for id in ids {
2020                    d.on_response(
2021                        &id,
2022                        Some(serde_json::json!({"mock": true, "js_heap": {}})),
2023                        None,
2024                    )
2025                    .await;
2026                }
2027            });
2028
2029            let result = handler.execute_tool(tool_name, args).await;
2030            resolver.await.unwrap();
2031
2032            match result {
2033                Ok(_) => {} // Tool recognized and got mock response
2034                Err(e) => {
2035                    assert!(
2036                        !e.contains("unknown tool"),
2037                        "{tool_name} should be recognized: {e}"
2038                    );
2039                }
2040            }
2041        }
2042    }
2043}