use async_trait::async_trait;
use klieo_bus_memory::MemoryBus;
use klieo_core::error::ToolError;
use klieo_core::tool::{Tool, ToolCtx, ToolInvoker};
use klieo_tools::ChainedInvoker;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
#[derive(Deserialize)]
struct GreetArgs {
name: String,
excited: Option<bool>,
}
#[derive(Serialize, Debug, PartialEq)]
struct GreetResult {
message: String,
}
struct GreetTool;
#[async_trait]
impl Tool for GreetTool {
fn name(&self) -> &str {
"greet"
}
fn description(&self) -> &str {
"greet a person"
}
fn json_schema(&self) -> &serde_json::Value {
static SCHEMA: std::sync::OnceLock<serde_json::Value> = std::sync::OnceLock::new();
SCHEMA.get_or_init(|| {
serde_json::json!({
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1 },
"excited": { "type": "boolean" }
},
"required": ["name"],
"additionalProperties": false,
})
})
}
async fn invoke(
&self,
args: serde_json::Value,
_ctx: ToolCtx,
) -> Result<serde_json::Value, ToolError> {
let parsed: GreetArgs =
serde_json::from_value(args).map_err(|e| ToolError::InvalidArgs(e.to_string()))?;
let suffix = if parsed.excited.unwrap_or(false) {
"!"
} else {
"."
};
let result = GreetResult {
message: format!("Hello, {}{}", parsed.name, suffix),
};
serde_json::to_value(result).map_err(|e| ToolError::Permanent(e.to_string()))
}
}
fn ctx() -> ToolCtx {
let bus = MemoryBus::new();
ToolCtx::new(bus.pubsub, bus.kv, bus.jobs)
}
#[tokio::test]
async fn end_to_end_greet_tool_succeeds() {
let inv = ChainedInvoker::new()
.with_default_timeout(Duration::from_secs(1))
.with_tool(Arc::new(GreetTool))
.unwrap();
let out = inv
.invoke(
"greet",
serde_json::json!({ "name": "world", "excited": true }),
ctx(),
)
.await
.unwrap();
assert_eq!(out, serde_json::json!({ "message": "Hello, world!" }));
}
#[tokio::test]
async fn end_to_end_missing_required_field_rejected_at_schema() {
let inv = ChainedInvoker::new()
.with_tool(Arc::new(GreetTool))
.unwrap();
let err = inv
.invoke("greet", serde_json::json!({"excited": true}), ctx())
.await
.unwrap_err();
assert!(matches!(err, ToolError::InvalidArgs(_)));
}
#[tokio::test]
async fn end_to_end_min_length_violation_rejected_at_schema() {
let inv = ChainedInvoker::new()
.with_tool(Arc::new(GreetTool))
.unwrap();
let err = inv
.invoke("greet", serde_json::json!({"name": ""}), ctx())
.await
.unwrap_err();
assert!(matches!(err, ToolError::InvalidArgs(_)));
}
#[tokio::test]
async fn catalogue_includes_greet_tool() {
let inv = ChainedInvoker::new()
.with_tool(Arc::new(GreetTool))
.unwrap();
let cat = inv.catalogue();
assert_eq!(cat.len(), 1);
assert_eq!(cat[0].name, "greet");
assert!(cat[0]
.json_schema
.get("required")
.and_then(|r| r.as_array())
.map(|a| a.iter().any(|s| s == "name"))
.unwrap_or(false));
}