Skip to main content

rustant_tools/
browser.rs

1//! Browser automation tools — 20 tools wrapping the CdpClient trait.
2//!
3//! All tools share a `BrowserToolContext` holding an `Arc<dyn CdpClient>` and
4//! an `Arc<BrowserSecurityGuard>` for security enforcement.
5
6use async_trait::async_trait;
7use rustant_core::browser::{BrowserSecurityGuard, CdpClient, SnapshotMode};
8use rustant_core::error::ToolError;
9use rustant_core::types::{RiskLevel, ToolOutput};
10use std::sync::Arc;
11use std::time::Duration;
12
13use crate::registry::Tool;
14
15/// Shared context for all browser tools.
16#[derive(Clone)]
17pub struct BrowserToolContext {
18    pub client: Arc<dyn CdpClient>,
19    pub security: Arc<BrowserSecurityGuard>,
20}
21
22impl BrowserToolContext {
23    pub fn new(client: Arc<dyn CdpClient>, security: Arc<BrowserSecurityGuard>) -> Self {
24        Self { client, security }
25    }
26}
27
28// ---------------------------------------------------------------------------
29// Helper to convert BrowserError → ToolError
30// ---------------------------------------------------------------------------
31fn browser_err(name: &str, e: impl std::fmt::Display) -> ToolError {
32    ToolError::ExecutionFailed {
33        name: name.to_string(),
34        message: e.to_string(),
35    }
36}
37
38fn missing_arg(tool: &str, param: &str) -> ToolError {
39    ToolError::InvalidArguments {
40        name: tool.to_string(),
41        reason: format!("missing required '{}' parameter", param),
42    }
43}
44
45// ============================================================================
46// 1. browser_navigate
47// ============================================================================
48pub struct BrowserNavigateTool {
49    ctx: BrowserToolContext,
50}
51
52impl BrowserNavigateTool {
53    pub fn new(ctx: BrowserToolContext) -> Self {
54        Self { ctx }
55    }
56}
57
58#[async_trait]
59impl Tool for BrowserNavigateTool {
60    fn name(&self) -> &str {
61        "browser_navigate"
62    }
63    fn description(&self) -> &str {
64        "Navigate the browser to a URL."
65    }
66    fn parameters_schema(&self) -> serde_json::Value {
67        serde_json::json!({
68            "type": "object",
69            "properties": {
70                "url": { "type": "string", "description": "The URL to navigate to" }
71            },
72            "required": ["url"]
73        })
74    }
75    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
76        let url = args["url"]
77            .as_str()
78            .ok_or_else(|| missing_arg("browser_navigate", "url"))?;
79        self.ctx
80            .security
81            .check_url(url)
82            .map_err(|e| browser_err("browser_navigate", e))?;
83        self.ctx
84            .client
85            .navigate(url)
86            .await
87            .map_err(|e| browser_err("browser_navigate", e))?;
88        Ok(ToolOutput::text(format!("Navigated to {}", url)))
89    }
90    fn risk_level(&self) -> RiskLevel {
91        RiskLevel::Write
92    }
93    fn timeout(&self) -> Duration {
94        Duration::from_secs(30)
95    }
96}
97
98// ============================================================================
99// 2. browser_back
100// ============================================================================
101pub struct BrowserBackTool {
102    ctx: BrowserToolContext,
103}
104impl BrowserBackTool {
105    pub fn new(ctx: BrowserToolContext) -> Self {
106        Self { ctx }
107    }
108}
109#[async_trait]
110impl Tool for BrowserBackTool {
111    fn name(&self) -> &str {
112        "browser_back"
113    }
114    fn description(&self) -> &str {
115        "Go back in browser history."
116    }
117    fn parameters_schema(&self) -> serde_json::Value {
118        serde_json::json!({"type": "object", "properties": {}})
119    }
120    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
121        self.ctx
122            .client
123            .go_back()
124            .await
125            .map_err(|e| browser_err("browser_back", e))?;
126        Ok(ToolOutput::text("Navigated back"))
127    }
128    fn risk_level(&self) -> RiskLevel {
129        RiskLevel::Write
130    }
131}
132
133// ============================================================================
134// 3. browser_forward
135// ============================================================================
136pub struct BrowserForwardTool {
137    ctx: BrowserToolContext,
138}
139impl BrowserForwardTool {
140    pub fn new(ctx: BrowserToolContext) -> Self {
141        Self { ctx }
142    }
143}
144#[async_trait]
145impl Tool for BrowserForwardTool {
146    fn name(&self) -> &str {
147        "browser_forward"
148    }
149    fn description(&self) -> &str {
150        "Go forward in browser history."
151    }
152    fn parameters_schema(&self) -> serde_json::Value {
153        serde_json::json!({"type": "object", "properties": {}})
154    }
155    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
156        self.ctx
157            .client
158            .go_forward()
159            .await
160            .map_err(|e| browser_err("browser_forward", e))?;
161        Ok(ToolOutput::text("Navigated forward"))
162    }
163    fn risk_level(&self) -> RiskLevel {
164        RiskLevel::Write
165    }
166}
167
168// ============================================================================
169// 4. browser_refresh
170// ============================================================================
171pub struct BrowserRefreshTool {
172    ctx: BrowserToolContext,
173}
174impl BrowserRefreshTool {
175    pub fn new(ctx: BrowserToolContext) -> Self {
176        Self { ctx }
177    }
178}
179#[async_trait]
180impl Tool for BrowserRefreshTool {
181    fn name(&self) -> &str {
182        "browser_refresh"
183    }
184    fn description(&self) -> &str {
185        "Refresh the current page."
186    }
187    fn parameters_schema(&self) -> serde_json::Value {
188        serde_json::json!({"type": "object", "properties": {}})
189    }
190    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
191        self.ctx
192            .client
193            .refresh()
194            .await
195            .map_err(|e| browser_err("browser_refresh", e))?;
196        Ok(ToolOutput::text("Page refreshed"))
197    }
198    fn risk_level(&self) -> RiskLevel {
199        RiskLevel::Write
200    }
201}
202
203// ============================================================================
204// 5. browser_click
205// ============================================================================
206pub struct BrowserClickTool {
207    ctx: BrowserToolContext,
208}
209impl BrowserClickTool {
210    pub fn new(ctx: BrowserToolContext) -> Self {
211        Self { ctx }
212    }
213}
214#[async_trait]
215impl Tool for BrowserClickTool {
216    fn name(&self) -> &str {
217        "browser_click"
218    }
219    fn description(&self) -> &str {
220        "Click an element matching a CSS selector."
221    }
222    fn parameters_schema(&self) -> serde_json::Value {
223        serde_json::json!({
224            "type": "object",
225            "properties": {
226                "selector": { "type": "string", "description": "CSS selector of the element to click" }
227            },
228            "required": ["selector"]
229        })
230    }
231    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
232        let selector = args["selector"]
233            .as_str()
234            .ok_or_else(|| missing_arg("browser_click", "selector"))?;
235        self.ctx
236            .client
237            .click(selector)
238            .await
239            .map_err(|e| browser_err("browser_click", e))?;
240        Ok(ToolOutput::text(format!("Clicked '{}'", selector)))
241    }
242    fn risk_level(&self) -> RiskLevel {
243        RiskLevel::Write
244    }
245}
246
247// ============================================================================
248// 6. browser_type
249// ============================================================================
250pub struct BrowserTypeTool {
251    ctx: BrowserToolContext,
252}
253impl BrowserTypeTool {
254    pub fn new(ctx: BrowserToolContext) -> Self {
255        Self { ctx }
256    }
257}
258#[async_trait]
259impl Tool for BrowserTypeTool {
260    fn name(&self) -> &str {
261        "browser_type"
262    }
263    fn description(&self) -> &str {
264        "Type text into an element matching a CSS selector."
265    }
266    fn parameters_schema(&self) -> serde_json::Value {
267        serde_json::json!({
268            "type": "object",
269            "properties": {
270                "selector": { "type": "string", "description": "CSS selector" },
271                "text": { "type": "string", "description": "Text to type" }
272            },
273            "required": ["selector", "text"]
274        })
275    }
276    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
277        let selector = args["selector"]
278            .as_str()
279            .ok_or_else(|| missing_arg("browser_type", "selector"))?;
280        let text = args["text"]
281            .as_str()
282            .ok_or_else(|| missing_arg("browser_type", "text"))?;
283        self.ctx
284            .client
285            .type_text(selector, text)
286            .await
287            .map_err(|e| browser_err("browser_type", e))?;
288        Ok(ToolOutput::text(format!(
289            "Typed '{}' into '{}'",
290            text, selector
291        )))
292    }
293    fn risk_level(&self) -> RiskLevel {
294        RiskLevel::Write
295    }
296}
297
298// ============================================================================
299// 7. browser_fill
300// ============================================================================
301pub struct BrowserFillTool {
302    ctx: BrowserToolContext,
303}
304impl BrowserFillTool {
305    pub fn new(ctx: BrowserToolContext) -> Self {
306        Self { ctx }
307    }
308}
309#[async_trait]
310impl Tool for BrowserFillTool {
311    fn name(&self) -> &str {
312        "browser_fill"
313    }
314    fn description(&self) -> &str {
315        "Clear a form field and fill it with a new value."
316    }
317    fn parameters_schema(&self) -> serde_json::Value {
318        serde_json::json!({
319            "type": "object",
320            "properties": {
321                "selector": { "type": "string", "description": "CSS selector of the input" },
322                "value": { "type": "string", "description": "Value to fill in" }
323            },
324            "required": ["selector", "value"]
325        })
326    }
327    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
328        let selector = args["selector"]
329            .as_str()
330            .ok_or_else(|| missing_arg("browser_fill", "selector"))?;
331        let value = args["value"]
332            .as_str()
333            .ok_or_else(|| missing_arg("browser_fill", "value"))?;
334        self.ctx
335            .client
336            .fill(selector, value)
337            .await
338            .map_err(|e| browser_err("browser_fill", e))?;
339        Ok(ToolOutput::text(format!(
340            "Filled '{}' with '{}'",
341            selector, value
342        )))
343    }
344    fn risk_level(&self) -> RiskLevel {
345        RiskLevel::Write
346    }
347}
348
349// ============================================================================
350// 8. browser_select
351// ============================================================================
352pub struct BrowserSelectTool {
353    ctx: BrowserToolContext,
354}
355impl BrowserSelectTool {
356    pub fn new(ctx: BrowserToolContext) -> Self {
357        Self { ctx }
358    }
359}
360#[async_trait]
361impl Tool for BrowserSelectTool {
362    fn name(&self) -> &str {
363        "browser_select"
364    }
365    fn description(&self) -> &str {
366        "Select an option in a <select> element."
367    }
368    fn parameters_schema(&self) -> serde_json::Value {
369        serde_json::json!({
370            "type": "object",
371            "properties": {
372                "selector": { "type": "string", "description": "CSS selector of the select element" },
373                "value": { "type": "string", "description": "Option value to select" }
374            },
375            "required": ["selector", "value"]
376        })
377    }
378    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
379        let selector = args["selector"]
380            .as_str()
381            .ok_or_else(|| missing_arg("browser_select", "selector"))?;
382        let value = args["value"]
383            .as_str()
384            .ok_or_else(|| missing_arg("browser_select", "value"))?;
385        self.ctx
386            .client
387            .select_option(selector, value)
388            .await
389            .map_err(|e| browser_err("browser_select", e))?;
390        Ok(ToolOutput::text(format!(
391            "Selected '{}' in '{}'",
392            value, selector
393        )))
394    }
395    fn risk_level(&self) -> RiskLevel {
396        RiskLevel::Write
397    }
398}
399
400// ============================================================================
401// 9. browser_scroll
402// ============================================================================
403pub struct BrowserScrollTool {
404    ctx: BrowserToolContext,
405}
406impl BrowserScrollTool {
407    pub fn new(ctx: BrowserToolContext) -> Self {
408        Self { ctx }
409    }
410}
411#[async_trait]
412impl Tool for BrowserScrollTool {
413    fn name(&self) -> &str {
414        "browser_scroll"
415    }
416    fn description(&self) -> &str {
417        "Scroll the page by the given x and y pixel offsets."
418    }
419    fn parameters_schema(&self) -> serde_json::Value {
420        serde_json::json!({
421            "type": "object",
422            "properties": {
423                "x": { "type": "integer", "description": "Horizontal scroll offset", "default": 0 },
424                "y": { "type": "integer", "description": "Vertical scroll offset", "default": 0 }
425            }
426        })
427    }
428    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
429        let x = args["x"].as_i64().unwrap_or(0) as i32;
430        let y = args["y"].as_i64().unwrap_or(0) as i32;
431        self.ctx
432            .client
433            .scroll(x, y)
434            .await
435            .map_err(|e| browser_err("browser_scroll", e))?;
436        Ok(ToolOutput::text(format!("Scrolled by ({}, {})", x, y)))
437    }
438    fn risk_level(&self) -> RiskLevel {
439        RiskLevel::Write
440    }
441}
442
443// ============================================================================
444// 10. browser_hover
445// ============================================================================
446pub struct BrowserHoverTool {
447    ctx: BrowserToolContext,
448}
449impl BrowserHoverTool {
450    pub fn new(ctx: BrowserToolContext) -> Self {
451        Self { ctx }
452    }
453}
454#[async_trait]
455impl Tool for BrowserHoverTool {
456    fn name(&self) -> &str {
457        "browser_hover"
458    }
459    fn description(&self) -> &str {
460        "Hover over an element matching a CSS selector."
461    }
462    fn parameters_schema(&self) -> serde_json::Value {
463        serde_json::json!({
464            "type": "object",
465            "properties": {
466                "selector": { "type": "string", "description": "CSS selector" }
467            },
468            "required": ["selector"]
469        })
470    }
471    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
472        let selector = args["selector"]
473            .as_str()
474            .ok_or_else(|| missing_arg("browser_hover", "selector"))?;
475        self.ctx
476            .client
477            .hover(selector)
478            .await
479            .map_err(|e| browser_err("browser_hover", e))?;
480        Ok(ToolOutput::text(format!("Hovered over '{}'", selector)))
481    }
482    fn risk_level(&self) -> RiskLevel {
483        RiskLevel::Write
484    }
485}
486
487// ============================================================================
488// 11. browser_press_key
489// ============================================================================
490pub struct BrowserPressKeyTool {
491    ctx: BrowserToolContext,
492}
493impl BrowserPressKeyTool {
494    pub fn new(ctx: BrowserToolContext) -> Self {
495        Self { ctx }
496    }
497}
498#[async_trait]
499impl Tool for BrowserPressKeyTool {
500    fn name(&self) -> &str {
501        "browser_press_key"
502    }
503    fn description(&self) -> &str {
504        "Press a keyboard key (e.g., 'Enter', 'Tab', 'Escape')."
505    }
506    fn parameters_schema(&self) -> serde_json::Value {
507        serde_json::json!({
508            "type": "object",
509            "properties": {
510                "key": { "type": "string", "description": "Key to press" }
511            },
512            "required": ["key"]
513        })
514    }
515    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
516        let key = args["key"]
517            .as_str()
518            .ok_or_else(|| missing_arg("browser_press_key", "key"))?;
519        self.ctx
520            .client
521            .press_key(key)
522            .await
523            .map_err(|e| browser_err("browser_press_key", e))?;
524        Ok(ToolOutput::text(format!("Pressed key '{}'", key)))
525    }
526    fn risk_level(&self) -> RiskLevel {
527        RiskLevel::Write
528    }
529}
530
531// ============================================================================
532// 12. browser_snapshot
533// ============================================================================
534pub struct BrowserSnapshotTool {
535    ctx: BrowserToolContext,
536}
537impl BrowserSnapshotTool {
538    pub fn new(ctx: BrowserToolContext) -> Self {
539        Self { ctx }
540    }
541}
542#[async_trait]
543impl Tool for BrowserSnapshotTool {
544    fn name(&self) -> &str {
545        "browser_snapshot"
546    }
547    fn description(&self) -> &str {
548        "Take a snapshot of the page content in the specified mode (html, text, aria_tree)."
549    }
550    fn parameters_schema(&self) -> serde_json::Value {
551        serde_json::json!({
552            "type": "object",
553            "properties": {
554                "mode": {
555                    "type": "string",
556                    "description": "Snapshot mode: html, text, or aria_tree",
557                    "default": "text",
558                    "enum": ["html", "text", "aria_tree"]
559                }
560            }
561        })
562    }
563    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
564        let mode_str = args["mode"].as_str().unwrap_or("text");
565        let mode = match mode_str {
566            "html" => SnapshotMode::Html,
567            "aria_tree" => SnapshotMode::AriaTree,
568            _ => SnapshotMode::Text,
569        };
570        let content = match mode {
571            SnapshotMode::Html => self
572                .ctx
573                .client
574                .get_html()
575                .await
576                .map_err(|e| browser_err("browser_snapshot", e))?,
577            SnapshotMode::Text => self
578                .ctx
579                .client
580                .get_text()
581                .await
582                .map_err(|e| browser_err("browser_snapshot", e))?,
583            SnapshotMode::AriaTree => self
584                .ctx
585                .client
586                .get_aria_tree()
587                .await
588                .map_err(|e| browser_err("browser_snapshot", e))?,
589            SnapshotMode::Screenshot => {
590                return Err(ToolError::ExecutionFailed {
591                    name: "browser_snapshot".into(),
592                    message: "Screenshot mode is handled separately by the screenshot tool".into(),
593                });
594            }
595        };
596        let masked = BrowserSecurityGuard::mask_credentials(&content);
597        Ok(ToolOutput::text(masked))
598    }
599    fn risk_level(&self) -> RiskLevel {
600        RiskLevel::ReadOnly
601    }
602}
603
604// ============================================================================
605// 13. browser_url
606// ============================================================================
607pub struct BrowserUrlTool {
608    ctx: BrowserToolContext,
609}
610impl BrowserUrlTool {
611    pub fn new(ctx: BrowserToolContext) -> Self {
612        Self { ctx }
613    }
614}
615#[async_trait]
616impl Tool for BrowserUrlTool {
617    fn name(&self) -> &str {
618        "browser_url"
619    }
620    fn description(&self) -> &str {
621        "Get the current page URL."
622    }
623    fn parameters_schema(&self) -> serde_json::Value {
624        serde_json::json!({"type": "object", "properties": {}})
625    }
626    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
627        let url = self
628            .ctx
629            .client
630            .get_url()
631            .await
632            .map_err(|e| browser_err("browser_url", e))?;
633        Ok(ToolOutput::text(url))
634    }
635    fn risk_level(&self) -> RiskLevel {
636        RiskLevel::ReadOnly
637    }
638}
639
640// ============================================================================
641// 14. browser_title
642// ============================================================================
643pub struct BrowserTitleTool {
644    ctx: BrowserToolContext,
645}
646impl BrowserTitleTool {
647    pub fn new(ctx: BrowserToolContext) -> Self {
648        Self { ctx }
649    }
650}
651#[async_trait]
652impl Tool for BrowserTitleTool {
653    fn name(&self) -> &str {
654        "browser_title"
655    }
656    fn description(&self) -> &str {
657        "Get the current page title."
658    }
659    fn parameters_schema(&self) -> serde_json::Value {
660        serde_json::json!({"type": "object", "properties": {}})
661    }
662    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
663        let title = self
664            .ctx
665            .client
666            .get_title()
667            .await
668            .map_err(|e| browser_err("browser_title", e))?;
669        Ok(ToolOutput::text(title))
670    }
671    fn risk_level(&self) -> RiskLevel {
672        RiskLevel::ReadOnly
673    }
674}
675
676// ============================================================================
677// 15. browser_screenshot
678// ============================================================================
679pub struct BrowserScreenshotTool {
680    ctx: BrowserToolContext,
681}
682impl BrowserScreenshotTool {
683    pub fn new(ctx: BrowserToolContext) -> Self {
684        Self { ctx }
685    }
686}
687#[async_trait]
688impl Tool for BrowserScreenshotTool {
689    fn name(&self) -> &str {
690        "browser_screenshot"
691    }
692    fn description(&self) -> &str {
693        "Take a screenshot of the current page and return it as base64-encoded PNG."
694    }
695    fn parameters_schema(&self) -> serde_json::Value {
696        serde_json::json!({"type": "object", "properties": {}})
697    }
698    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
699        let bytes = self
700            .ctx
701            .client
702            .screenshot()
703            .await
704            .map_err(|e| browser_err("browser_screenshot", e))?;
705        let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
706        Ok(ToolOutput::text(b64))
707    }
708    fn risk_level(&self) -> RiskLevel {
709        RiskLevel::ReadOnly
710    }
711}
712
713// ============================================================================
714// 16. browser_js_eval
715// ============================================================================
716pub struct BrowserJsEvalTool {
717    ctx: BrowserToolContext,
718}
719impl BrowserJsEvalTool {
720    pub fn new(ctx: BrowserToolContext) -> Self {
721        Self { ctx }
722    }
723}
724#[async_trait]
725impl Tool for BrowserJsEvalTool {
726    fn name(&self) -> &str {
727        "browser_js_eval"
728    }
729    fn description(&self) -> &str {
730        "Evaluate a JavaScript expression in the page context and return the result."
731    }
732    fn parameters_schema(&self) -> serde_json::Value {
733        serde_json::json!({
734            "type": "object",
735            "properties": {
736                "script": { "type": "string", "description": "JavaScript code to evaluate" }
737            },
738            "required": ["script"]
739        })
740    }
741    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
742        let script = args["script"]
743            .as_str()
744            .ok_or_else(|| missing_arg("browser_js_eval", "script"))?;
745        let result = self
746            .ctx
747            .client
748            .evaluate_js(script)
749            .await
750            .map_err(|e| browser_err("browser_js_eval", e))?;
751        Ok(ToolOutput::text(
752            serde_json::to_string_pretty(&result).unwrap_or_default(),
753        ))
754    }
755    fn risk_level(&self) -> RiskLevel {
756        RiskLevel::Execute
757    }
758    fn timeout(&self) -> Duration {
759        Duration::from_secs(30)
760    }
761}
762
763// ============================================================================
764// 17. browser_wait
765// ============================================================================
766pub struct BrowserWaitTool {
767    ctx: BrowserToolContext,
768}
769impl BrowserWaitTool {
770    pub fn new(ctx: BrowserToolContext) -> Self {
771        Self { ctx }
772    }
773}
774#[async_trait]
775impl Tool for BrowserWaitTool {
776    fn name(&self) -> &str {
777        "browser_wait"
778    }
779    fn description(&self) -> &str {
780        "Wait for an element matching a CSS selector to appear on the page."
781    }
782    fn parameters_schema(&self) -> serde_json::Value {
783        serde_json::json!({
784            "type": "object",
785            "properties": {
786                "selector": { "type": "string", "description": "CSS selector to wait for" },
787                "timeout_ms": { "type": "integer", "description": "Timeout in milliseconds", "default": 5000 }
788            },
789            "required": ["selector"]
790        })
791    }
792    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
793        let selector = args["selector"]
794            .as_str()
795            .ok_or_else(|| missing_arg("browser_wait", "selector"))?;
796        let timeout_ms = args["timeout_ms"].as_u64().unwrap_or(5000);
797        self.ctx
798            .client
799            .wait_for_selector(selector, timeout_ms)
800            .await
801            .map_err(|e| browser_err("browser_wait", e))?;
802        Ok(ToolOutput::text(format!("Element '{}' found", selector)))
803    }
804    fn risk_level(&self) -> RiskLevel {
805        RiskLevel::ReadOnly
806    }
807    fn timeout(&self) -> Duration {
808        Duration::from_secs(60)
809    }
810}
811
812// ============================================================================
813// 18. browser_file_upload
814// ============================================================================
815pub struct BrowserFileUploadTool {
816    ctx: BrowserToolContext,
817}
818impl BrowserFileUploadTool {
819    pub fn new(ctx: BrowserToolContext) -> Self {
820        Self { ctx }
821    }
822}
823#[async_trait]
824impl Tool for BrowserFileUploadTool {
825    fn name(&self) -> &str {
826        "browser_file_upload"
827    }
828    fn description(&self) -> &str {
829        "Upload a file to a file input element."
830    }
831    fn parameters_schema(&self) -> serde_json::Value {
832        serde_json::json!({
833            "type": "object",
834            "properties": {
835                "selector": { "type": "string", "description": "CSS selector of the file input" },
836                "path": { "type": "string", "description": "Local file path to upload" }
837            },
838            "required": ["selector", "path"]
839        })
840    }
841    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
842        let selector = args["selector"]
843            .as_str()
844            .ok_or_else(|| missing_arg("browser_file_upload", "selector"))?;
845        let path = args["path"]
846            .as_str()
847            .ok_or_else(|| missing_arg("browser_file_upload", "path"))?;
848        self.ctx
849            .client
850            .upload_file(selector, path)
851            .await
852            .map_err(|e| browser_err("browser_file_upload", e))?;
853        Ok(ToolOutput::text(format!(
854            "Uploaded '{}' to '{}'",
855            path, selector
856        )))
857    }
858    fn risk_level(&self) -> RiskLevel {
859        RiskLevel::Network
860    }
861}
862
863// ============================================================================
864// 19. browser_download
865// ============================================================================
866pub struct BrowserDownloadTool {
867    ctx: BrowserToolContext,
868}
869impl BrowserDownloadTool {
870    pub fn new(ctx: BrowserToolContext) -> Self {
871        Self { ctx }
872    }
873}
874#[async_trait]
875impl Tool for BrowserDownloadTool {
876    fn name(&self) -> &str {
877        "browser_download"
878    }
879    fn description(&self) -> &str {
880        "Trigger a download by clicking a link element."
881    }
882    fn parameters_schema(&self) -> serde_json::Value {
883        serde_json::json!({
884            "type": "object",
885            "properties": {
886                "selector": { "type": "string", "description": "CSS selector of the download link/button" }
887            },
888            "required": ["selector"]
889        })
890    }
891    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
892        let selector = args["selector"]
893            .as_str()
894            .ok_or_else(|| missing_arg("browser_download", "selector"))?;
895        self.ctx
896            .client
897            .click(selector)
898            .await
899            .map_err(|e| browser_err("browser_download", e))?;
900        Ok(ToolOutput::text(format!(
901            "Download triggered via '{}'",
902            selector
903        )))
904    }
905    fn risk_level(&self) -> RiskLevel {
906        RiskLevel::Network
907    }
908}
909
910// ============================================================================
911// 20. browser_close
912// ============================================================================
913pub struct BrowserCloseTool {
914    ctx: BrowserToolContext,
915}
916impl BrowserCloseTool {
917    pub fn new(ctx: BrowserToolContext) -> Self {
918        Self { ctx }
919    }
920}
921#[async_trait]
922impl Tool for BrowserCloseTool {
923    fn name(&self) -> &str {
924        "browser_close"
925    }
926    fn description(&self) -> &str {
927        "Close the current browser page/tab."
928    }
929    fn parameters_schema(&self) -> serde_json::Value {
930        serde_json::json!({"type": "object", "properties": {}})
931    }
932    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
933        self.ctx
934            .client
935            .close()
936            .await
937            .map_err(|e| browser_err("browser_close", e))?;
938        Ok(ToolOutput::text("Browser page closed"))
939    }
940    fn risk_level(&self) -> RiskLevel {
941        RiskLevel::Write
942    }
943}
944
945// ============================================================================
946// 21. browser_new_tab
947// ============================================================================
948pub struct BrowserNewTabTool {
949    ctx: BrowserToolContext,
950}
951impl BrowserNewTabTool {
952    pub fn new(ctx: BrowserToolContext) -> Self {
953        Self { ctx }
954    }
955}
956#[async_trait]
957impl Tool for BrowserNewTabTool {
958    fn name(&self) -> &str {
959        "browser_new_tab"
960    }
961    fn description(&self) -> &str {
962        "Open a new browser tab and navigate to a URL. Returns the tab ID."
963    }
964    fn parameters_schema(&self) -> serde_json::Value {
965        serde_json::json!({
966            "type": "object",
967            "properties": {
968                "url": { "type": "string", "description": "URL to open in the new tab (default: about:blank)" }
969            }
970        })
971    }
972    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
973        let url = args["url"].as_str().unwrap_or("about:blank");
974        if url != "about:blank" {
975            self.ctx
976                .security
977                .check_url(url)
978                .map_err(|e| browser_err("browser_new_tab", e))?;
979        }
980        let tab_id = self
981            .ctx
982            .client
983            .new_tab(url)
984            .await
985            .map_err(|e| browser_err("browser_new_tab", e))?;
986        Ok(ToolOutput::text(format!(
987            "Opened new tab (id: {}) at {}",
988            tab_id, url
989        )))
990    }
991    fn risk_level(&self) -> RiskLevel {
992        RiskLevel::Write
993    }
994}
995
996// ============================================================================
997// 22. browser_list_tabs
998// ============================================================================
999pub struct BrowserListTabsTool {
1000    ctx: BrowserToolContext,
1001}
1002impl BrowserListTabsTool {
1003    pub fn new(ctx: BrowserToolContext) -> Self {
1004        Self { ctx }
1005    }
1006}
1007#[async_trait]
1008impl Tool for BrowserListTabsTool {
1009    fn name(&self) -> &str {
1010        "browser_list_tabs"
1011    }
1012    fn description(&self) -> &str {
1013        "List all open browser tabs with their IDs, URLs, titles, and which is active."
1014    }
1015    fn parameters_schema(&self) -> serde_json::Value {
1016        serde_json::json!({"type": "object", "properties": {}})
1017    }
1018    async fn execute(&self, _args: serde_json::Value) -> Result<ToolOutput, ToolError> {
1019        let tabs = self
1020            .ctx
1021            .client
1022            .list_tabs()
1023            .await
1024            .map_err(|e| browser_err("browser_list_tabs", e))?;
1025        let mut output = format!("{} tab(s) open:\n", tabs.len());
1026        for tab in &tabs {
1027            let marker = if tab.active { " [ACTIVE]" } else { "" };
1028            output.push_str(&format!(
1029                "  - {} | {} | {}{}\n",
1030                tab.id, tab.url, tab.title, marker
1031            ));
1032        }
1033        Ok(ToolOutput::text(output))
1034    }
1035    fn risk_level(&self) -> RiskLevel {
1036        RiskLevel::ReadOnly
1037    }
1038}
1039
1040// ============================================================================
1041// 23. browser_switch_tab
1042// ============================================================================
1043pub struct BrowserSwitchTabTool {
1044    ctx: BrowserToolContext,
1045}
1046impl BrowserSwitchTabTool {
1047    pub fn new(ctx: BrowserToolContext) -> Self {
1048        Self { ctx }
1049    }
1050}
1051#[async_trait]
1052impl Tool for BrowserSwitchTabTool {
1053    fn name(&self) -> &str {
1054        "browser_switch_tab"
1055    }
1056    fn description(&self) -> &str {
1057        "Switch the active browser tab by tab ID. Use browser_list_tabs to see available IDs."
1058    }
1059    fn parameters_schema(&self) -> serde_json::Value {
1060        serde_json::json!({
1061            "type": "object",
1062            "properties": {
1063                "tab_id": { "type": "string", "description": "ID of the tab to switch to" }
1064            },
1065            "required": ["tab_id"]
1066        })
1067    }
1068    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
1069        let tab_id = args["tab_id"]
1070            .as_str()
1071            .ok_or_else(|| missing_arg("browser_switch_tab", "tab_id"))?;
1072        self.ctx
1073            .client
1074            .switch_tab(tab_id)
1075            .await
1076            .map_err(|e| browser_err("browser_switch_tab", e))?;
1077        Ok(ToolOutput::text(format!("Switched to tab {}", tab_id)))
1078    }
1079    fn risk_level(&self) -> RiskLevel {
1080        RiskLevel::ReadOnly
1081    }
1082}
1083
1084// ============================================================================
1085// 24. browser_close_tab
1086// ============================================================================
1087pub struct BrowserCloseTabTool {
1088    ctx: BrowserToolContext,
1089}
1090impl BrowserCloseTabTool {
1091    pub fn new(ctx: BrowserToolContext) -> Self {
1092        Self { ctx }
1093    }
1094}
1095#[async_trait]
1096impl Tool for BrowserCloseTabTool {
1097    fn name(&self) -> &str {
1098        "browser_close_tab"
1099    }
1100    fn description(&self) -> &str {
1101        "Close a specific browser tab by its ID."
1102    }
1103    fn parameters_schema(&self) -> serde_json::Value {
1104        serde_json::json!({
1105            "type": "object",
1106            "properties": {
1107                "tab_id": { "type": "string", "description": "ID of the tab to close" }
1108            },
1109            "required": ["tab_id"]
1110        })
1111    }
1112    async fn execute(&self, args: serde_json::Value) -> Result<ToolOutput, ToolError> {
1113        let tab_id = args["tab_id"]
1114            .as_str()
1115            .ok_or_else(|| missing_arg("browser_close_tab", "tab_id"))?;
1116        self.ctx
1117            .client
1118            .close_tab(tab_id)
1119            .await
1120            .map_err(|e| browser_err("browser_close_tab", e))?;
1121        Ok(ToolOutput::text(format!("Closed tab {}", tab_id)))
1122    }
1123    fn risk_level(&self) -> RiskLevel {
1124        RiskLevel::Write
1125    }
1126}
1127
1128// ============================================================================
1129// Registration helper
1130// ============================================================================
1131
1132/// Create all 24 browser tools for registration.
1133pub fn create_browser_tools(ctx: BrowserToolContext) -> Vec<Arc<dyn Tool>> {
1134    vec![
1135        Arc::new(BrowserNavigateTool::new(ctx.clone())),
1136        Arc::new(BrowserBackTool::new(ctx.clone())),
1137        Arc::new(BrowserForwardTool::new(ctx.clone())),
1138        Arc::new(BrowserRefreshTool::new(ctx.clone())),
1139        Arc::new(BrowserClickTool::new(ctx.clone())),
1140        Arc::new(BrowserTypeTool::new(ctx.clone())),
1141        Arc::new(BrowserFillTool::new(ctx.clone())),
1142        Arc::new(BrowserSelectTool::new(ctx.clone())),
1143        Arc::new(BrowserScrollTool::new(ctx.clone())),
1144        Arc::new(BrowserHoverTool::new(ctx.clone())),
1145        Arc::new(BrowserPressKeyTool::new(ctx.clone())),
1146        Arc::new(BrowserSnapshotTool::new(ctx.clone())),
1147        Arc::new(BrowserUrlTool::new(ctx.clone())),
1148        Arc::new(BrowserTitleTool::new(ctx.clone())),
1149        Arc::new(BrowserScreenshotTool::new(ctx.clone())),
1150        Arc::new(BrowserJsEvalTool::new(ctx.clone())),
1151        Arc::new(BrowserWaitTool::new(ctx.clone())),
1152        Arc::new(BrowserFileUploadTool::new(ctx.clone())),
1153        Arc::new(BrowserDownloadTool::new(ctx.clone())),
1154        Arc::new(BrowserCloseTool::new(ctx.clone())),
1155        // Tab management tools
1156        Arc::new(BrowserNewTabTool::new(ctx.clone())),
1157        Arc::new(BrowserListTabsTool::new(ctx.clone())),
1158        Arc::new(BrowserSwitchTabTool::new(ctx.clone())),
1159        Arc::new(BrowserCloseTabTool::new(ctx)),
1160    ]
1161}
1162
1163/// Register all browser tools into a ToolRegistry.
1164pub fn register_browser_tools(
1165    registry: &mut crate::registry::ToolRegistry,
1166    ctx: BrowserToolContext,
1167) {
1168    let tools = create_browser_tools(ctx);
1169    for tool in tools {
1170        if let Err(e) = registry.register(tool) {
1171            tracing::warn!("Failed to register browser tool: {}", e);
1172        }
1173    }
1174}
1175
1176// ============================================================================
1177// Tests
1178// ============================================================================
1179#[cfg(test)]
1180mod tests {
1181    use super::*;
1182    use crate::registry::ToolRegistry;
1183    use rustant_core::browser::MockCdpClient;
1184    use rustant_core::error::BrowserError;
1185
1186    fn make_ctx() -> (BrowserToolContext, Arc<MockCdpClient>) {
1187        let client = Arc::new(MockCdpClient::new());
1188        let security = Arc::new(BrowserSecurityGuard::default());
1189        let ctx = BrowserToolContext::new(client.clone() as Arc<dyn CdpClient>, security);
1190        (ctx, client)
1191    }
1192
1193    fn make_ctx_with_security(
1194        security: BrowserSecurityGuard,
1195    ) -> (BrowserToolContext, Arc<MockCdpClient>) {
1196        let client = Arc::new(MockCdpClient::new());
1197        let security = Arc::new(security);
1198        let ctx = BrowserToolContext::new(client.clone() as Arc<dyn CdpClient>, security);
1199        (ctx, client)
1200    }
1201
1202    #[tokio::test]
1203    async fn test_navigate_tool_calls_cdp_navigate() {
1204        let (ctx, client) = make_ctx();
1205        let tool = BrowserNavigateTool::new(ctx);
1206        let result = tool
1207            .execute(serde_json::json!({"url": "https://example.com"}))
1208            .await
1209            .unwrap();
1210        assert!(result.content.contains("Navigated to"));
1211        assert_eq!(*client.current_url.lock().unwrap(), "https://example.com");
1212    }
1213
1214    #[tokio::test]
1215    async fn test_navigate_tool_blocked_url() {
1216        let security = BrowserSecurityGuard::new(vec![], vec!["evil.com".to_string()]);
1217        let (ctx, _client) = make_ctx_with_security(security);
1218        let tool = BrowserNavigateTool::new(ctx);
1219        let result = tool
1220            .execute(serde_json::json!({"url": "https://evil.com"}))
1221            .await;
1222        assert!(result.is_err());
1223    }
1224
1225    #[tokio::test]
1226    async fn test_click_tool_calls_cdp_click() {
1227        let (ctx, client) = make_ctx();
1228        let tool = BrowserClickTool::new(ctx);
1229        tool.execute(serde_json::json!({"selector": "#submit"}))
1230            .await
1231            .unwrap();
1232        assert_eq!(client.call_count("click"), 1);
1233    }
1234
1235    #[tokio::test]
1236    async fn test_click_tool_element_not_found() {
1237        let (ctx, client) = make_ctx();
1238        client.set_click_error(BrowserError::ElementNotFound {
1239            selector: "#missing".to_string(),
1240        });
1241        let tool = BrowserClickTool::new(ctx);
1242        let result = tool
1243            .execute(serde_json::json!({"selector": "#missing"}))
1244            .await;
1245        assert!(result.is_err());
1246    }
1247
1248    #[tokio::test]
1249    async fn test_type_tool_calls_cdp_type() {
1250        let (ctx, client) = make_ctx();
1251        let tool = BrowserTypeTool::new(ctx);
1252        tool.execute(serde_json::json!({"selector": "#input", "text": "hello"}))
1253            .await
1254            .unwrap();
1255        assert_eq!(client.call_count("type_text"), 1);
1256    }
1257
1258    #[tokio::test]
1259    async fn test_fill_tool_clears_and_types() {
1260        let (ctx, client) = make_ctx();
1261        let tool = BrowserFillTool::new(ctx);
1262        let result = tool
1263            .execute(serde_json::json!({"selector": "#email", "value": "a@b.com"}))
1264            .await
1265            .unwrap();
1266        assert!(result.content.contains("Filled"));
1267        assert_eq!(client.call_count("fill"), 1);
1268    }
1269
1270    #[tokio::test]
1271    async fn test_screenshot_tool_returns_base64() {
1272        let (ctx, client) = make_ctx();
1273        client.set_screenshot(vec![0x89, 0x50, 0x4E, 0x47]);
1274        let tool = BrowserScreenshotTool::new(ctx);
1275        let result = tool.execute(serde_json::json!({})).await.unwrap();
1276        // Base64 of PNG magic bytes
1277        assert!(!result.content.is_empty());
1278        // Should be valid base64
1279        let decoded =
1280            base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &result.content)
1281                .unwrap();
1282        assert_eq!(decoded, vec![0x89, 0x50, 0x4E, 0x47]);
1283    }
1284
1285    #[tokio::test]
1286    async fn test_snapshot_tool_html_mode() {
1287        let (ctx, client) = make_ctx();
1288        client.set_html("<html><body>Hello</body></html>");
1289        let tool = BrowserSnapshotTool::new(ctx);
1290        let result = tool
1291            .execute(serde_json::json!({"mode": "html"}))
1292            .await
1293            .unwrap();
1294        assert!(result.content.contains("Hello"));
1295    }
1296
1297    #[tokio::test]
1298    async fn test_snapshot_tool_aria_mode() {
1299        let (ctx, client) = make_ctx();
1300        client.set_aria_tree("document\n  heading 'Welcome'");
1301        let tool = BrowserSnapshotTool::new(ctx);
1302        let result = tool
1303            .execute(serde_json::json!({"mode": "aria_tree"}))
1304            .await
1305            .unwrap();
1306        assert!(result.content.contains("heading"));
1307    }
1308
1309    #[tokio::test]
1310    async fn test_snapshot_tool_text_mode() {
1311        let (ctx, client) = make_ctx();
1312        client.set_text("Welcome to the page");
1313        let tool = BrowserSnapshotTool::new(ctx);
1314        let result = tool
1315            .execute(serde_json::json!({"mode": "text"}))
1316            .await
1317            .unwrap();
1318        assert_eq!(result.content, "Welcome to the page");
1319    }
1320
1321    #[tokio::test]
1322    async fn test_js_eval_tool_returns_result() {
1323        let (ctx, client) = make_ctx();
1324        client.add_js_result("document.title", serde_json::json!("My Page"));
1325        let tool = BrowserJsEvalTool::new(ctx);
1326        let result = tool
1327            .execute(serde_json::json!({"script": "document.title"}))
1328            .await
1329            .unwrap();
1330        assert!(result.content.contains("My Page"));
1331    }
1332
1333    #[tokio::test]
1334    async fn test_url_tool_returns_current_url() {
1335        let (ctx, client) = make_ctx();
1336        client.set_url("https://example.com/page");
1337        let tool = BrowserUrlTool::new(ctx);
1338        let result = tool.execute(serde_json::json!({})).await.unwrap();
1339        assert_eq!(result.content, "https://example.com/page");
1340    }
1341
1342    #[tokio::test]
1343    async fn test_wait_tool_times_out() {
1344        let (ctx, client) = make_ctx();
1345        client.set_wait_error(BrowserError::Timeout { timeout_secs: 5 });
1346        let tool = BrowserWaitTool::new(ctx);
1347        let result = tool
1348            .execute(serde_json::json!({"selector": "#never", "timeout_ms": 5000}))
1349            .await;
1350        assert!(result.is_err());
1351    }
1352
1353    #[tokio::test]
1354    async fn test_all_browser_tools_register() {
1355        let (ctx, _client) = make_ctx();
1356        let mut registry = ToolRegistry::new();
1357        register_browser_tools(&mut registry, ctx);
1358        assert_eq!(registry.len(), 24);
1359
1360        // Verify no duplicate names
1361        let names = registry.list_names();
1362        let unique: std::collections::HashSet<_> = names.iter().collect();
1363        assert_eq!(unique.len(), 24);
1364    }
1365
1366    #[tokio::test]
1367    async fn test_browser_tool_risk_levels() {
1368        let (ctx, _client) = make_ctx();
1369        let tools = create_browser_tools(ctx);
1370        let mut risk_map = std::collections::HashMap::new();
1371        for tool in &tools {
1372            risk_map.insert(tool.name().to_string(), tool.risk_level());
1373        }
1374        // Read-only tools
1375        assert_eq!(risk_map["browser_snapshot"], RiskLevel::ReadOnly);
1376        assert_eq!(risk_map["browser_url"], RiskLevel::ReadOnly);
1377        assert_eq!(risk_map["browser_title"], RiskLevel::ReadOnly);
1378        assert_eq!(risk_map["browser_screenshot"], RiskLevel::ReadOnly);
1379        assert_eq!(risk_map["browser_wait"], RiskLevel::ReadOnly);
1380        // Write tools
1381        assert_eq!(risk_map["browser_navigate"], RiskLevel::Write);
1382        assert_eq!(risk_map["browser_click"], RiskLevel::Write);
1383        assert_eq!(risk_map["browser_type"], RiskLevel::Write);
1384        assert_eq!(risk_map["browser_fill"], RiskLevel::Write);
1385        assert_eq!(risk_map["browser_close"], RiskLevel::Write);
1386        // Execute tools
1387        assert_eq!(risk_map["browser_js_eval"], RiskLevel::Execute);
1388        // Network tools
1389        assert_eq!(risk_map["browser_file_upload"], RiskLevel::Network);
1390        assert_eq!(risk_map["browser_download"], RiskLevel::Network);
1391        // Tab management tools
1392        assert_eq!(risk_map["browser_new_tab"], RiskLevel::Write);
1393        assert_eq!(risk_map["browser_list_tabs"], RiskLevel::ReadOnly);
1394        assert_eq!(risk_map["browser_switch_tab"], RiskLevel::ReadOnly);
1395        assert_eq!(risk_map["browser_close_tab"], RiskLevel::Write);
1396    }
1397
1398    #[tokio::test]
1399    async fn test_browser_back_forward_refresh() {
1400        let (ctx, client) = make_ctx();
1401        BrowserBackTool::new(ctx.clone())
1402            .execute(serde_json::json!({}))
1403            .await
1404            .unwrap();
1405        BrowserForwardTool::new(ctx.clone())
1406            .execute(serde_json::json!({}))
1407            .await
1408            .unwrap();
1409        BrowserRefreshTool::new(ctx)
1410            .execute(serde_json::json!({}))
1411            .await
1412            .unwrap();
1413        assert_eq!(client.call_count("go_back"), 1);
1414        assert_eq!(client.call_count("go_forward"), 1);
1415        assert_eq!(client.call_count("refresh"), 1);
1416    }
1417
1418    #[tokio::test]
1419    async fn test_scroll_tool() {
1420        let (ctx, client) = make_ctx();
1421        let tool = BrowserScrollTool::new(ctx);
1422        tool.execute(serde_json::json!({"x": 0, "y": 500}))
1423            .await
1424            .unwrap();
1425        let calls = client.calls();
1426        let scroll_call = calls.iter().find(|(m, _)| m == "scroll").unwrap();
1427        assert_eq!(scroll_call.1, vec!["0", "500"]);
1428    }
1429
1430    #[tokio::test]
1431    async fn test_hover_tool() {
1432        let (ctx, client) = make_ctx();
1433        let tool = BrowserHoverTool::new(ctx);
1434        tool.execute(serde_json::json!({"selector": "#menu"}))
1435            .await
1436            .unwrap();
1437        assert_eq!(client.call_count("hover"), 1);
1438    }
1439
1440    #[tokio::test]
1441    async fn test_press_key_tool() {
1442        let (ctx, client) = make_ctx();
1443        let tool = BrowserPressKeyTool::new(ctx);
1444        tool.execute(serde_json::json!({"key": "Enter"}))
1445            .await
1446            .unwrap();
1447        assert_eq!(client.call_count("press_key"), 1);
1448    }
1449
1450    #[tokio::test]
1451    async fn test_select_tool() {
1452        let (ctx, client) = make_ctx();
1453        let tool = BrowserSelectTool::new(ctx);
1454        tool.execute(serde_json::json!({"selector": "#country", "value": "US"}))
1455            .await
1456            .unwrap();
1457        assert_eq!(client.call_count("select_option"), 1);
1458    }
1459
1460    #[tokio::test]
1461    async fn test_title_tool() {
1462        let (ctx, client) = make_ctx();
1463        client.set_title("Test Page");
1464        let tool = BrowserTitleTool::new(ctx);
1465        let result = tool.execute(serde_json::json!({})).await.unwrap();
1466        assert_eq!(result.content, "Test Page");
1467    }
1468
1469    #[tokio::test]
1470    async fn test_file_upload_tool() {
1471        let (ctx, client) = make_ctx();
1472        let tool = BrowserFileUploadTool::new(ctx);
1473        tool.execute(serde_json::json!({"selector": "#file", "path": "/tmp/test.txt"}))
1474            .await
1475            .unwrap();
1476        assert_eq!(client.call_count("upload_file"), 1);
1477    }
1478
1479    #[tokio::test]
1480    async fn test_download_tool() {
1481        let (ctx, client) = make_ctx();
1482        let tool = BrowserDownloadTool::new(ctx);
1483        tool.execute(serde_json::json!({"selector": "#download-btn"}))
1484            .await
1485            .unwrap();
1486        assert_eq!(client.call_count("click"), 1);
1487    }
1488
1489    #[tokio::test]
1490    async fn test_close_tool() {
1491        let (ctx, client) = make_ctx();
1492        let tool = BrowserCloseTool::new(ctx);
1493        tool.execute(serde_json::json!({})).await.unwrap();
1494        assert!(*client.closed.lock().unwrap());
1495    }
1496}