|
| 1 | +//! Integration tests for web_search tool with headless engines |
| 2 | +//! |
| 3 | +//! Run basic tests: cargo test -p a3s-code-core --test test_web_search_headless |
| 4 | +//! Run with actual browser: cargo test -p a3s-code-core --test test_web_search_headless -- --ignored |
| 5 | +
|
| 6 | +use a3s_code_core::config::{HeadlessConfig, SearchConfig}; |
| 7 | +use a3s_code_core::tools::ToolExecutor; |
| 8 | +use a3s_code_core::ToolContext; |
| 9 | + |
| 10 | +use std::collections::HashMap; |
| 11 | +use std::path::PathBuf; |
| 12 | + |
| 13 | +/// Helper to create a ToolContext with headless search config |
| 14 | +fn make_context(headless: Option<HeadlessConfig>) -> ToolContext { |
| 15 | + let search_config = headless.map(|h| SearchConfig { |
| 16 | + timeout: 30, |
| 17 | + health: None, |
| 18 | + engines: HashMap::new(), |
| 19 | + headless: Some(h), |
| 20 | + }); |
| 21 | + |
| 22 | + ToolContext { |
| 23 | + workspace: PathBuf::from("/tmp"), |
| 24 | + session_id: Some("test-session".to_string()), |
| 25 | + event_tx: None, |
| 26 | + agent_event_tx: None, |
| 27 | + search_config, |
| 28 | + sandbox: None, |
| 29 | + command_env: None, |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +#[tokio::test] |
| 34 | +async fn test_web_search_tool_creation() { |
| 35 | + let executor = ToolExecutor::new("/tmp".to_string()); |
| 36 | + let definitions = executor.definitions(); |
| 37 | + |
| 38 | + assert!( |
| 39 | + definitions.iter().any(|t| t.name == "web_search"), |
| 40 | + "web_search tool should be registered" |
| 41 | + ); |
| 42 | +} |
| 43 | + |
| 44 | +#[tokio::test] |
| 45 | +async fn test_web_search_http_engine() { |
| 46 | + let executor = ToolExecutor::new("/tmp".to_string()); |
| 47 | + |
| 48 | + // HTTP engine (duckduckgo) doesn't need headless config |
| 49 | + let args = serde_json::json!({ |
| 50 | + "query": "test", |
| 51 | + "engines": "duckduckgo" |
| 52 | + }); |
| 53 | + |
| 54 | + let result = executor.execute("web_search", &args).await; |
| 55 | + |
| 56 | + match result { |
| 57 | + Ok(output) => { |
| 58 | + println!("✅ DuckDuckGo search succeeded!"); |
| 59 | + println!("Output length: {}", output.output.len()); |
| 60 | + println!("Exit code: {}", output.exit_code); |
| 61 | + } |
| 62 | + Err(e) => { |
| 63 | + println!("⚠️ DuckDuckGo search failed: {:?}", e); |
| 64 | + } |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +#[tokio::test] |
| 69 | +async fn test_web_search_with_baidu_headless_engine() { |
| 70 | + let executor = ToolExecutor::new("/tmp".to_string()); |
| 71 | + |
| 72 | + let headless_config = HeadlessConfig { |
| 73 | + max_tabs: 2, |
| 74 | + obscura_path: None, |
| 75 | + proxy_url: None, |
| 76 | + }; |
| 77 | + |
| 78 | + let context = make_context(Some(headless_config)); |
| 79 | + |
| 80 | + let args = serde_json::json!({ |
| 81 | + "query": "test query", |
| 82 | + "engines": "baidu" |
| 83 | + }); |
| 84 | + |
| 85 | + // This will attempt to use baidu with headless browser |
| 86 | + let result = executor |
| 87 | + .execute_with_context("web_search", &args, &context) |
| 88 | + .await; |
| 89 | + |
| 90 | + match result { |
| 91 | + Ok(output) => { |
| 92 | + println!("✅ Baidu headless search succeeded!"); |
| 93 | + println!("Output length: {}", output.output.len()); |
| 94 | + println!("Exit code: {:?}", output.exit_code); |
| 95 | + } |
| 96 | + Err(e) => { |
| 97 | + // If obscura is not available, this will fail - which is expected in CI |
| 98 | + println!( |
| 99 | + "⚠️ Baidu headless search failed (obscura may not be available): {:?}", |
| 100 | + e |
| 101 | + ); |
| 102 | + } |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +#[tokio::test] |
| 107 | +#[ignore] // Requires obscura browser to be installed |
| 108 | +async fn test_baidu_headless_search_actual() { |
| 109 | + let executor = ToolExecutor::new("/tmp".to_string()); |
| 110 | + |
| 111 | + let headless_config = HeadlessConfig { |
| 112 | + max_tabs: 2, |
| 113 | + obscura_path: None, |
| 114 | + proxy_url: None, |
| 115 | + }; |
| 116 | + |
| 117 | + let context = make_context(Some(headless_config)); |
| 118 | + |
| 119 | + let args = serde_json::json!({ |
| 120 | + "query": "rust programming language", |
| 121 | + "engines": "baidu" |
| 122 | + }); |
| 123 | + |
| 124 | + let result = executor |
| 125 | + .execute_with_context("web_search", &args, &context) |
| 126 | + .await |
| 127 | + .unwrap(); |
| 128 | + |
| 129 | + assert_eq!( |
| 130 | + result.exit_code, 0, |
| 131 | + "Search should succeed, got exit_code={}, output={}", |
| 132 | + result.exit_code, result.output |
| 133 | + ); |
| 134 | + assert!( |
| 135 | + !result.output.trim().is_empty(), |
| 136 | + "Search output should not be empty" |
| 137 | + ); |
| 138 | + println!("Search output: {}", result.output); |
| 139 | +} |
| 140 | + |
| 141 | +#[tokio::test] |
| 142 | +#[ignore] // Requires obscura browser to be installed |
| 143 | +async fn test_google_headless_search_actual() { |
| 144 | + let executor = ToolExecutor::new("/tmp".to_string()); |
| 145 | + |
| 146 | + let headless_config = HeadlessConfig { |
| 147 | + max_tabs: 2, |
| 148 | + obscura_path: None, |
| 149 | + proxy_url: None, |
| 150 | + }; |
| 151 | + |
| 152 | + let context = make_context(Some(headless_config)); |
| 153 | + |
| 154 | + let args = serde_json::json!({ |
| 155 | + "query": "rust programming language", |
| 156 | + "engines": "google" |
| 157 | + }); |
| 158 | + |
| 159 | + let result = executor |
| 160 | + .execute_with_context("web_search", &args, &context) |
| 161 | + .await |
| 162 | + .unwrap(); |
| 163 | + |
| 164 | + assert_eq!( |
| 165 | + result.exit_code, 0, |
| 166 | + "Search should succeed, got exit_code={}, output={}", |
| 167 | + result.exit_code, result.output |
| 168 | + ); |
| 169 | + assert!( |
| 170 | + !result.output.trim().is_empty(), |
| 171 | + "Search output should not be empty" |
| 172 | + ); |
| 173 | + println!("Search output: {}", result.output); |
| 174 | +} |
0 commit comments