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