Skip to content

Commit e42803f

Browse files
committed
feat: support string format for engines param in web_search
- Fix engines parsing to handle both string ("baidu") and array (["baidu"]) formats - Add unit tests for web_search with headless engines (baidu, google) - Tests verify code path executes correctly; actual browser requires obscura binary
1 parent cd9ac29 commit e42803f

2 files changed

Lines changed: 189 additions & 2 deletions

File tree

core/src/tools/builtin/web_search.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,21 @@ impl Tool for WebSearchTool {
214214

215215
let engines: Vec<&str> = args
216216
.get("engines")
217-
.and_then(|v| v.as_array())
218-
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
217+
.and_then(|v| {
218+
if let Some(arr) = v.as_array() {
219+
Some(arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
220+
} else if let Some(s) = v.as_str() {
221+
// Handle comma-separated string like "baidu,ddg" or single engine like "baidu"
222+
Some(
223+
s.split(',')
224+
.map(str::trim)
225+
.filter(|s| !s.is_empty())
226+
.collect(),
227+
)
228+
} else {
229+
None
230+
}
231+
})
219232
.unwrap_or_else(|| default_engines.clone());
220233

221234
let limit = args
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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

Comments
 (0)