use async_trait::async_trait;
use salvor_tools::{
Effect, HandlerError, Tool, ToolCtx, ToolHandler, ToolMeta, ToolOutcome, ToolSet,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct TicketRequest {
summary: String,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
struct TicketRef {
id: String,
}
#[derive(Tool)]
#[tool(effect = "write", description = "Create a Jira ticket")]
struct CreateTicket;
#[async_trait]
impl ToolHandler for CreateTicket {
type Input = TicketRequest;
type Output = TicketRef;
async fn call(
&self,
_ctx: &ToolCtx,
input: TicketRequest,
) -> Result<ToolOutcome<TicketRef>, HandlerError> {
Ok(ToolOutcome::Output(TicketRef {
id: format!("JIRA-{}", input.summary.len()),
}))
}
}
#[test]
fn derives_meta_from_attributes() {
assert_eq!(CreateTicket::NAME, "create_ticket");
assert_eq!(CreateTicket::DESCRIPTION, "Create a Jira ticket");
assert_eq!(CreateTicket::EFFECT, Effect::Write);
}
#[tokio::test]
async fn derived_tool_registers_and_dispatches() {
let mut tools = ToolSet::new();
tools.register(CreateTicket).expect("first registration");
let tool = tools.get("create_ticket").expect("tool is registered");
assert_eq!(tool.description(), "Create a Jira ticket");
assert_eq!(tool.effect(), Effect::Write);
let outcome = tool
.call_json(&ToolCtx::new(None), json!({ "summary": "fix login" }))
.await
.expect("dispatch succeeds");
match outcome {
ToolOutcome::Output(value) => assert_eq!(value, json!({ "id": "JIRA-9" })),
ToolOutcome::Suspend(_) => panic!("tool returned an output, not a suspension"),
}
}
#[derive(Tool)]
#[tool(
name = "make_ticket",
effect = "write",
description = "Create a ticket"
)]
struct RenamedTool;
#[test]
fn name_attribute_overrides_the_default() {
assert_eq!(RenamedTool::NAME, "make_ticket");
}
#[derive(Tool)]
#[tool(effect = "read", description = "Fetch over HTTP")]
struct HTTPFetch;
#[derive(Tool)]
#[tool(effect = "read", description = "A one-word tool")]
struct Ticket;
#[test]
fn default_name_is_snake_case() {
assert_eq!(CreateTicket::NAME, "create_ticket");
assert_eq!(HTTPFetch::NAME, "http_fetch");
assert_eq!(Ticket::NAME, "ticket");
}
mod hygiene {
use salvor_tools::{Tool, ToolMeta};
struct Effect;
#[derive(Tool)]
#[tool(effect = "idempotent", description = "Charge a card once")]
struct ChargeCard;
#[test]
fn generated_paths_ignore_a_shadowed_effect() {
let _shadow = Effect;
assert_eq!(ChargeCard::EFFECT, salvor_tools::Effect::Idempotent);
}
}