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 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 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 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1042
1043 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 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 #[tokio::test]
1518 async fn assert_semantic_numeric_from_non_string_value() {
1519 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 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 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 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 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 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 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 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 assert_eq!(result["passed"], false);
1664 }
1665
1666 #[tokio::test]
1669 async fn tool_with_action_as_number_errors() {
1670 let handler = make_handler();
1671 let result = handler
1673 .execute_tool(
1674 "interact",
1675 serde_json::json!({"action": 42, "ref_id": "e0"}),
1676 )
1677 .await;
1678 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 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 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 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 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(_) => {} Err(e) => {
1937 assert!(
1938 !e.contains("unknown tool"),
1939 "{tool_name} should be recognized: {e}"
1940 );
1941 }
1942 }
1943 }
1944 }
1945}