use crate::tools::ToolRegistry;
use async_trait::async_trait;
use std::sync::Arc;
use thulp_core::{Result as ThulpResult, ToolCall, ToolDefinition, ToolResult, Transport};
pub struct PawanTransport {
registry: Arc<ToolRegistry>,
}
impl PawanTransport {
pub fn new(registry: Arc<ToolRegistry>) -> Self {
Self { registry }
}
}
#[async_trait]
impl Transport for PawanTransport {
async fn connect(&mut self) -> ThulpResult<()> {
Ok(())
}
async fn disconnect(&mut self) -> ThulpResult<()> {
Ok(())
}
fn is_connected(&self) -> bool {
true
}
async fn list_tools(&self) -> ThulpResult<Vec<ToolDefinition>> {
let defs: Vec<ToolDefinition> = self
.registry
.get_all_definitions()
.iter()
.filter_map(|pawan_def| {
self.registry
.get(&pawan_def.name)
.map(|tool| tool.thulp_definition())
})
.collect();
Ok(defs)
}
async fn call(&self, call: &ToolCall) -> ThulpResult<ToolResult> {
match self.registry.execute(&call.tool, call.arguments.clone()).await {
Ok(value) => Ok(ToolResult::success(value)),
Err(e) => Ok(ToolResult::failure(format!("{}", e))),
}
}
}
pub mod built_in_skills {
use serde_json::json;
use thulp_skills::{Skill, SkillStep};
pub fn format_and_commit() -> Skill {
Skill::new("format_and_commit", "Format code, stage, and commit")
.with_input("message")
.with_step(SkillStep {
name: "format".to_string(),
tool: "bash".to_string(),
arguments: json!({ "command": "cargo fmt --all" }),
..Default::default()
})
.with_step(SkillStep {
name: "stage".to_string(),
tool: "git_add".to_string(),
arguments: json!({ "paths": ["."] }),
..Default::default()
})
.with_step(SkillStep {
name: "commit".to_string(),
tool: "git_commit".to_string(),
arguments: json!({ "message": "{{message}}" }),
..Default::default()
})
}
pub fn test_and_report() -> Skill {
Skill::new("test_and_report", "Run cargo test and capture output")
.with_step(SkillStep {
name: "test".to_string(),
tool: "bash".to_string(),
arguments: json!({ "command": "cargo test --workspace 2>&1 | tail -20" }),
..Default::default()
})
}
pub fn deagle_explore() -> Skill {
Skill::new("deagle_explore", "Index codebase and search for a symbol")
.with_input("symbol")
.with_step(SkillStep {
name: "index".to_string(),
tool: "deagle_map".to_string(),
arguments: json!({ "path": "." }),
..Default::default()
})
.with_step(SkillStep {
name: "stats".to_string(),
tool: "deagle_stats".to_string(),
arguments: json!({}),
..Default::default()
})
.with_step(SkillStep {
name: "search".to_string(),
tool: "deagle_search".to_string(),
arguments: json!({ "query": "{{symbol}}", "fuzzy": true }),
..Default::default()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::ToolRegistry;
use std::path::PathBuf;
#[test]
fn test_pawan_transport_always_connected() {
let tmp = tempfile::TempDir::new().unwrap();
let registry = Arc::new(ToolRegistry::with_defaults(tmp.path().to_path_buf()));
let transport = PawanTransport::new(registry);
assert!(transport.is_connected());
}
#[tokio::test]
async fn test_pawan_transport_connect_disconnect_noop() {
let tmp = tempfile::TempDir::new().unwrap();
let registry = Arc::new(ToolRegistry::with_defaults(tmp.path().to_path_buf()));
let mut transport = PawanTransport::new(registry);
assert!(transport.connect().await.is_ok());
assert!(transport.disconnect().await.is_ok());
}
#[tokio::test]
async fn test_pawan_transport_list_tools_returns_all() {
let tmp = tempfile::TempDir::new().unwrap();
let registry = Arc::new(ToolRegistry::with_defaults(tmp.path().to_path_buf()));
let transport = PawanTransport::new(registry);
let tools = transport.list_tools().await.unwrap();
assert!(tools.len() >= 30, "Expected at least 30 tools, got {}", tools.len());
}
#[test]
fn test_built_in_format_and_commit_skill() {
let skill = built_in_skills::format_and_commit();
assert_eq!(skill.name, "format_and_commit");
}
#[test]
fn test_built_in_test_and_report_skill() {
let skill = built_in_skills::test_and_report();
assert_eq!(skill.name, "test_and_report");
}
#[test]
fn test_built_in_deagle_explore_skill() {
let skill = built_in_skills::deagle_explore();
assert_eq!(skill.name, "deagle_explore");
}
#[tokio::test]
async fn test_pawan_transport_call_unknown_tool_returns_failure() {
let tmp = tempfile::TempDir::new().unwrap();
let registry = Arc::new(ToolRegistry::with_defaults(tmp.path().to_path_buf()));
let transport = PawanTransport::new(registry);
let call = ToolCall {
tool: "nonexistent_tool_xyz".to_string(),
arguments: serde_json::json!({}),
};
let result = transport.call(&call).await.unwrap();
assert!(!result.success, "unknown tool should produce a failure result");
}
#[tokio::test]
async fn test_pawan_transport_call_dispatches_to_read_file() {
let tmp = tempfile::TempDir::new().unwrap();
let file_path = tmp.path().join("hello.txt");
std::fs::write(&file_path, "hello from transport test\n").unwrap();
let registry = Arc::new(ToolRegistry::with_defaults(tmp.path().to_path_buf()));
let transport = PawanTransport::new(registry);
let call = ToolCall {
tool: "read_file".to_string(),
arguments: serde_json::json!({ "path": "hello.txt" }),
};
let result = transport.call(&call).await.unwrap();
assert!(result.success, "read_file via transport should succeed");
let result_str = format!("{:?}", result);
assert!(
result_str.contains("hello from transport test"),
"result should include file contents, got: {}",
result_str
);
}
#[tokio::test]
async fn test_pawan_transport_list_tools_names_match_registry() {
let tmp = tempfile::TempDir::new().unwrap();
let registry = Arc::new(ToolRegistry::with_defaults(tmp.path().to_path_buf()));
let transport = PawanTransport::new(Arc::clone(®istry));
let transport_names: std::collections::HashSet<String> = transport
.list_tools()
.await
.unwrap()
.into_iter()
.map(|d| d.name)
.collect();
let registry_names: std::collections::HashSet<String> = registry
.get_all_definitions()
.into_iter()
.map(|d| d.name)
.collect();
assert_eq!(
transport_names, registry_names,
"transport.list_tools() must match registry.get_all_definitions() 1:1"
);
}
#[test]
fn test_built_in_skills_are_distinct() {
let names: std::collections::HashSet<String> = [
built_in_skills::format_and_commit().name,
built_in_skills::test_and_report().name,
built_in_skills::deagle_explore().name,
]
.into_iter()
.collect();
assert_eq!(names.len(), 3, "all 3 built-in skills must have unique names");
}
#[allow(dead_code)]
fn _unused_pathbuf() -> PathBuf {
PathBuf::new()
}
#[test]
fn test_format_and_commit_skill_has_3_steps_with_expected_tools() {
let skill = built_in_skills::format_and_commit();
assert_eq!(skill.steps.len(), 3);
assert_eq!(skill.steps[0].name, "format");
assert_eq!(skill.steps[0].tool, "bash");
assert_eq!(skill.steps[1].name, "stage");
assert_eq!(skill.steps[1].tool, "git_add");
assert_eq!(skill.steps[2].name, "commit");
assert_eq!(skill.steps[2].tool, "git_commit");
}
#[test]
fn test_format_and_commit_requires_message_input() {
let skill = built_in_skills::format_and_commit();
assert!(skill.inputs.contains(&"message".to_string()));
let commit_args = &skill.steps[2].arguments;
let msg = commit_args["message"].as_str().unwrap();
assert!(msg.contains("{{message}}"), "commit should use {{{{message}}}} template, got: {}", msg);
}
#[test]
fn test_test_and_report_skill_has_exactly_one_step() {
let skill = built_in_skills::test_and_report();
assert_eq!(skill.steps.len(), 1);
assert_eq!(skill.steps[0].name, "test");
assert_eq!(skill.steps[0].tool, "bash");
assert!(skill.inputs.is_empty(), "test_and_report takes no inputs");
}
#[test]
fn test_deagle_explore_pipeline_order() {
let skill = built_in_skills::deagle_explore();
assert_eq!(skill.steps.len(), 3);
assert_eq!(skill.steps[0].tool, "deagle_map", "index must run first");
assert_eq!(skill.steps[1].tool, "deagle_stats", "stats runs after map");
assert_eq!(skill.steps[2].tool, "deagle_search", "search last");
assert!(skill.inputs.contains(&"symbol".to_string()));
let search_args = &skill.steps[2].arguments;
assert!(search_args["query"].as_str().unwrap().contains("{{symbol}}"));
}
#[test]
fn test_all_built_in_skill_tools_are_in_default_registry() {
let tmp = tempfile::TempDir::new().unwrap();
let registry = ToolRegistry::with_defaults(tmp.path().to_path_buf());
let all_skills = [
built_in_skills::format_and_commit(),
built_in_skills::test_and_report(),
built_in_skills::deagle_explore(),
];
for skill in &all_skills {
for step in &skill.steps {
assert!(
registry.has_tool(&step.tool),
"skill {:?} step {:?} references unregistered tool {:?}",
skill.name, step.name, step.tool,
);
}
}
}
#[tokio::test]
async fn test_pawan_transport_list_tools_all_have_descriptions() {
let tmp = tempfile::TempDir::new().unwrap();
let registry = Arc::new(ToolRegistry::with_defaults(tmp.path().to_path_buf()));
let transport = PawanTransport::new(registry);
let tools = transport.list_tools().await.unwrap();
assert!(!tools.is_empty());
for tool in &tools {
assert!(
!tool.description.is_empty(),
"tool {} has empty description",
tool.name,
);
}
}
}