Skip to main content

lc_tools/extended/computer/
mod.rs

1//! Computer Use tool for screen interaction via Anthropic API or native mode.
2
3pub mod actions;
4pub mod screen;
5
6pub use actions::{ComputerMode, ComputerUseInput, ComputerUseOutput};
7pub use screen::ComputerUseTool;
8
9// ---------------------------------------------------------------------------
10// Tests
11// ---------------------------------------------------------------------------
12
13#[cfg(test)]
14mod tests {
15    use super::*;
16    use lc_core::tools::ToolError;
17    use lc_core::BaseTool;
18
19    fn tool() -> ComputerUseTool {
20        ComputerUseTool::new_anthropic("test-key", 1024, 768)
21    }
22
23    #[test]
24    fn test_name() {
25        let t = tool();
26        assert_eq!(t.name(), "computer_use");
27    }
28
29    #[test]
30    fn test_description_contains_actions() {
31        let t = tool();
32        let desc = t.description();
33        for action in &["screenshot", "click", "type", "scroll", "key_press", "wait"] {
34            assert!(
35                desc.contains(action),
36                "description should mention '{}'",
37                action
38            );
39        }
40    }
41
42    #[test]
43    fn test_args_schema_has_action_field() {
44        let t = tool();
45        let schema = t.args_schema().expect("schema should be present");
46        let props = schema
47            .get("properties")
48            .expect("schema should have properties");
49        assert!(
50            props.get("action").is_some(),
51            "schema should define 'action' property"
52        );
53    }
54
55    #[tokio::test]
56    async fn test_run_invalid_json() {
57        let t = tool();
58        let result = t.run("not json".to_string()).await;
59        assert!(result.is_err());
60        let err = result.unwrap_err();
61        assert!(matches!(err, ToolError::InvalidInput(_)));
62    }
63
64    #[tokio::test]
65    async fn test_run_unknown_action() {
66        let t = tool();
67        let input = r#"{"action":"fly"}"#.to_string();
68        let result = t.run(input).await;
69        assert!(result.is_err());
70        let err = result.unwrap_err();
71        assert!(matches!(err, ToolError::InvalidInput(_)));
72        assert!(
73            err.to_string().contains("Unknown action"),
74            "error should mention unknown action, got: {}",
75            err
76        );
77    }
78
79    #[tokio::test]
80    async fn test_run_click_missing_coordinate() {
81        let t = tool();
82        let input = r#"{"action":"click"}"#.to_string();
83        let result = t.run(input).await;
84        assert!(result.is_err());
85        let err = result.unwrap_err();
86        assert!(matches!(err, ToolError::InvalidInput(_)));
87        assert!(
88            err.to_string().contains("coordinate"),
89            "error should mention missing coordinate, got: {}",
90            err
91        );
92    }
93
94    #[tokio::test]
95    async fn test_run_type_missing_text() {
96        let t = tool();
97        let input = r#"{"action":"type"}"#.to_string();
98        let result = t.run(input).await;
99        assert!(result.is_err());
100        let err = result.unwrap_err();
101        assert!(matches!(err, ToolError::InvalidInput(_)));
102        assert!(
103            err.to_string().contains("text"),
104            "error should mention missing text, got: {}",
105            err
106        );
107    }
108
109    #[tokio::test]
110    async fn test_run_key_press_missing_keys() {
111        let t = tool();
112        let input = r#"{"action":"key_press"}"#.to_string();
113        let result = t.run(input).await;
114        assert!(result.is_err());
115        let err = result.unwrap_err();
116        assert!(matches!(err, ToolError::InvalidInput(_)));
117        assert!(
118            err.to_string().contains("keys"),
119            "error should mention missing keys, got: {}",
120            err
121        );
122    }
123
124    #[tokio::test]
125    async fn test_run_scroll_missing_direction() {
126        let t = tool();
127        let input = r#"{"action":"scroll","coordinate":[100,200]}"#.to_string();
128        let result = t.run(input).await;
129        assert!(result.is_err());
130        let err = result.unwrap_err();
131        assert!(matches!(err, ToolError::InvalidInput(_)));
132        assert!(
133            err.to_string().contains("direction"),
134            "error should mention missing direction, got: {}",
135            err
136        );
137    }
138
139    #[tokio::test]
140    async fn test_run_coordinate_out_of_bounds() {
141        let t = tool();
142        let input = r#"{"action":"click","coordinate":[2000,500]}"#.to_string();
143        let result = t.run(input).await;
144        assert!(result.is_err());
145        let err = result.unwrap_err();
146        assert!(matches!(err, ToolError::InvalidInput(_)));
147        assert!(
148            err.to_string().contains("out of bounds"),
149            "error should mention out of bounds, got: {}",
150            err
151        );
152    }
153
154    #[tokio::test]
155    async fn test_run_coordinate_wrong_length() {
156        let t = tool();
157        let input = r#"{"action":"click","coordinate":[100]}"#.to_string();
158        let result = t.run(input).await;
159        assert!(result.is_err());
160        let err = result.unwrap_err();
161        assert!(matches!(err, ToolError::InvalidInput(_)));
162        assert!(
163            err.to_string().contains("exactly [x, y]"),
164            "error should mention coordinate format, got: {}",
165            err
166        );
167    }
168
169    #[tokio::test]
170    async fn test_run_empty_api_key() {
171        let t = ComputerUseTool::new_anthropic("", 1024, 768);
172        let input = r#"{"action":"screenshot"}"#.to_string();
173        let result = t.run(input).await;
174        assert!(result.is_err());
175        let err = result.unwrap_err();
176        assert!(matches!(err, ToolError::InvalidInput(_)));
177        assert!(
178            err.to_string().contains("API key"),
179            "error should mention API key, got: {}",
180            err
181        );
182    }
183
184    #[test]
185    fn test_build_tool_input_screenshot() {
186        let t = tool();
187        let input = ComputerUseInput {
188            action: "screenshot".to_string(),
189            coordinate: None,
190            text: None,
191            keys: None,
192            direction: None,
193            amount: None,
194            duration_ms: None,
195        };
196        let result = t.build_tool_input(&input).unwrap();
197        assert_eq!(result["action"], "screenshot");
198    }
199
200    #[test]
201    fn test_build_tool_input_click() {
202        let t = tool();
203        let input = ComputerUseInput {
204            action: "click".to_string(),
205            coordinate: Some(vec![100, 200]),
206            text: None,
207            keys: None,
208            direction: None,
209            amount: None,
210            duration_ms: None,
211        };
212        let result = t.build_tool_input(&input).unwrap();
213        assert_eq!(result["action"], "mouse_click");
214        assert_eq!(result["coordinate"], serde_json::json!([100, 200]));
215        assert_eq!(result["text"], "left");
216    }
217
218    #[test]
219    fn test_build_tool_input_click_right_button() {
220        let t = tool();
221        let input = ComputerUseInput {
222            action: "click".to_string(),
223            coordinate: Some(vec![50, 75]),
224            text: Some("right".to_string()),
225            keys: None,
226            direction: None,
227            amount: None,
228            duration_ms: None,
229        };
230        let result = t.build_tool_input(&input).unwrap();
231        assert_eq!(result["text"], "right");
232    }
233
234    #[test]
235    fn test_build_tool_input_type() {
236        let t = tool();
237        let input = ComputerUseInput {
238            action: "type".to_string(),
239            coordinate: None,
240            text: Some("hello world".to_string()),
241            keys: None,
242            direction: None,
243            amount: None,
244            duration_ms: None,
245        };
246        let result = t.build_tool_input(&input).unwrap();
247        assert_eq!(result["action"], "type");
248        assert_eq!(result["text"], "hello world");
249    }
250
251    #[test]
252    fn test_build_tool_input_scroll() {
253        let t = tool();
254        let input = ComputerUseInput {
255            action: "scroll".to_string(),
256            coordinate: Some(vec![500, 300]),
257            text: None,
258            keys: None,
259            direction: Some("up".to_string()),
260            amount: Some(5),
261            duration_ms: None,
262        };
263        let result = t.build_tool_input(&input).unwrap();
264        assert_eq!(result["action"], "scroll");
265        assert_eq!(result["coordinate"], serde_json::json!([500, 300]));
266        assert_eq!(result["direction"], "up");
267        assert_eq!(result["amount"], 5);
268    }
269
270    #[test]
271    fn test_build_tool_input_key_press() {
272        let t = tool();
273        let input = ComputerUseInput {
274            action: "key_press".to_string(),
275            coordinate: None,
276            text: None,
277            keys: Some(vec!["ctrl".to_string(), "c".to_string()]),
278            direction: None,
279            amount: None,
280            duration_ms: None,
281        };
282        let result = t.build_tool_input(&input).unwrap();
283        assert_eq!(result["action"], "key");
284        assert_eq!(result["text"], "ctrl+c");
285    }
286
287    #[test]
288    fn test_build_tool_input_wait() {
289        let t = tool();
290        let input = ComputerUseInput {
291            action: "wait".to_string(),
292            coordinate: None,
293            text: None,
294            keys: None,
295            direction: None,
296            amount: None,
297            duration_ms: Some(2000),
298        };
299        let result = t.build_tool_input(&input).unwrap();
300        assert_eq!(result["action"], "wait");
301        assert_eq!(result["duration_ms"], 2000);
302    }
303
304    #[test]
305    fn test_build_tool_input_unknown_action() {
306        let t = tool();
307        let input = ComputerUseInput {
308            action: "fly".to_string(),
309            coordinate: None,
310            text: None,
311            keys: None,
312            direction: None,
313            amount: None,
314            duration_ms: None,
315        };
316        let result = t.build_tool_input(&input);
317        assert!(result.is_err());
318        assert!(result.unwrap_err().to_string().contains("Unknown action"));
319    }
320
321    #[test]
322    fn test_default() {
323        let t = ComputerUseTool::default();
324        assert_eq!(t.name(), "computer_use");
325        assert_eq!(t.display_width, 1024);
326        assert_eq!(t.display_height, 768);
327    }
328
329    #[test]
330    fn test_with_base_url() {
331        let t = ComputerUseTool::new_anthropic("key", 1920, 1080)
332            .with_base_url("https://custom.api.com");
333        assert_eq!(t.base_url, "https://custom.api.com");
334    }
335
336    #[test]
337    fn test_mode_returns_anthropic() {
338        let t = tool();
339        assert!(matches!(t.mode(), ComputerMode::AnthropicApi));
340    }
341
342    #[test]
343    fn test_build_anthropic_request_contains_model() {
344        let t = tool();
345        let input = ComputerUseInput {
346            action: "screenshot".to_string(),
347            coordinate: None,
348            text: None,
349            keys: None,
350            direction: None,
351            amount: None,
352            duration_ms: None,
353        };
354        let body = t.build_anthropic_request(&input).unwrap();
355        assert_eq!(body["model"], "claude-sonnet-4-20250514");
356        assert!(body["tools"].is_array());
357        assert_eq!(body["tools"][0]["type"], "computer_20250124");
358    }
359}