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