lc_tools/extended/computer/
mod.rs1pub mod actions;
4pub mod screen;
5
6pub use actions::{ComputerMode, ComputerUseInput, ComputerUseOutput};
7pub use screen::ComputerUseTool;
8
9#[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}