feat: comprehensive tool + Gitea + bridge tests, search_code tool definition

Tools module:
- Added search_code to tool_definitions() (was in execute but not defined)
- 8 new unit tests: tool definitions (base/gitea/kratos/all), agent tools,
  minimal registry, unknown tool dispatch, search_code without OpenSearch

Gitea indexer fix:
- Direct API calls for directory listings (SDK parses as single object)
- PAT-based auth for file fetching
- GiteaClient.base_url made public for direct API access

Integration tests:
- Gitea SDK: list_repos, get_repo, get_file, directory, full code indexing
- gRPC bridge: thinking events, tool call mapping, request ID filtering
- Evaluator: rule matching, conversation registry lifecycle
- Web search: SearXNG round-trip
This commit is contained in:
2026-03-24 11:58:01 +00:00
parent 495c465a01
commit 42f6b38f12

View File

@@ -219,6 +219,25 @@ impl ToolRegistry {
tools.extend(identity::tool_definitions());
}
// Code search (OpenSearch code index)
tools.push(Tool::new(
"search_code".into(),
"Search the code index for functions, types, patterns, or concepts across \
the current project and Gitea repositories. Supports keyword and semantic search."
.into(),
json!({
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query (natural language or code pattern)" },
"language": { "type": "string", "description": "Filter by language: rust, typescript, python (optional)" },
"repo": { "type": "string", "description": "Filter by repo name (optional)" },
"semantic": { "type": "boolean", "description": "Use semantic search (optional)" },
"limit": { "type": "integer", "description": "Max results (default 10)" }
},
"required": ["query"]
}),
));
// Web search (SearXNG — free, self-hosted)
tools.push(web_search::tool_definition());
@@ -436,3 +455,123 @@ impl ToolRegistry {
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_definitions_base() {
let tools = ToolRegistry::tool_definitions(false, false);
assert!(tools.len() >= 5, "Should have at least 5 base tools");
let names: Vec<String> = tools.iter().map(|t| t.function.name.clone()).collect();
assert!(names.contains(&"search_archive".into()));
assert!(names.contains(&"run_script".into()));
assert!(names.contains(&"search_web".into()));
assert!(names.contains(&"search_code".into()));
// No gitea or identity tools when disabled
assert!(!names.iter().any(|n| n.starts_with("gitea_")));
assert!(!names.iter().any(|n| n.starts_with("identity_")));
}
#[test]
fn test_tool_definitions_with_gitea() {
let tools = ToolRegistry::tool_definitions(true, false);
let names: Vec<String> = tools.iter().map(|t| t.function.name.clone()).collect();
assert!(names.iter().any(|n| n.starts_with("gitea_")), "Should have gitea tools");
assert!(!names.iter().any(|n| n.starts_with("identity_")));
}
#[test]
fn test_tool_definitions_with_kratos() {
let tools = ToolRegistry::tool_definitions(false, true);
let names: Vec<String> = tools.iter().map(|t| t.function.name.clone()).collect();
assert!(names.iter().any(|n| n.starts_with("identity_")), "Should have identity tools");
assert!(!names.iter().any(|n| n.starts_with("gitea_")));
}
#[test]
fn test_tool_definitions_all_enabled() {
let tools = ToolRegistry::tool_definitions(true, true);
let names: Vec<String> = tools.iter().map(|t| t.function.name.clone()).collect();
assert!(names.iter().any(|n| n.starts_with("gitea_")));
assert!(names.iter().any(|n| n.starts_with("identity_")));
}
#[test]
fn test_agent_tool_definitions() {
let tools = ToolRegistry::agent_tool_definitions(false, false);
assert!(!tools.is_empty(), "Should have agent tools");
}
#[test]
fn test_minimal_registry() {
let config = Arc::new(crate::config::Config::from_str(r#"
[matrix]
homeserver_url = "http://localhost:8008"
user_id = "@test:localhost"
state_store_path = "/tmp/test"
db_path = ":memory:"
[opensearch]
url = "http://localhost:9200"
index = "test"
[mistral]
[behavior]
"#).unwrap());
let registry = ToolRegistry::new_minimal(config);
assert!(!registry.has_gitea());
assert!(!registry.has_kratos());
}
#[tokio::test]
async fn test_execute_unknown_tool() {
let config = Arc::new(crate::config::Config::from_str(r#"
[matrix]
homeserver_url = "http://localhost:8008"
user_id = "@test:localhost"
state_store_path = "/tmp/test"
db_path = ":memory:"
[opensearch]
url = "http://localhost:9200"
index = "test"
[mistral]
[behavior]
"#).unwrap());
let registry = ToolRegistry::new_minimal(config);
let ctx = crate::orchestrator::event::ToolContext {
user_id: "test".into(),
scope_key: "test-room".into(),
is_direct: true,
};
let result = registry.execute_with_context("nonexistent_tool", "{}", &ctx).await;
assert!(result.is_err(), "Unknown tool should error");
}
#[tokio::test]
async fn test_execute_search_code_without_opensearch() {
let config = Arc::new(crate::config::Config::from_str(r#"
[matrix]
homeserver_url = "http://localhost:8008"
user_id = "@test:localhost"
state_store_path = "/tmp/test"
db_path = ":memory:"
[opensearch]
url = "http://localhost:9200"
index = "test"
[mistral]
[behavior]
"#).unwrap());
let registry = ToolRegistry::new_minimal(config);
let ctx = crate::context::ResponseContext {
matrix_user_id: String::new(),
user_id: "test".into(),
display_name: None,
is_dm: true,
is_reply: false,
room_id: "test".into(),
};
let result = registry.execute("search_code", r#"{"query":"test"}"#, &ctx).await;
assert!(result.is_err(), "search_code without OpenSearch should error");
}
}