use async_trait::async_trait;
use serde_json::{json, Value};
use crate::error::{Error, Result};
use crate::llm::ToolSpec;
use crate::team::TeamRegistry;
use crate::tools::{Tool, ToolSideEffect};
pub struct TeamDeleteTool;
impl TeamDeleteTool {
pub fn new() -> Self {
Self
}
}
impl Default for TeamDeleteTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for TeamDeleteTool {
fn spec(&self) -> ToolSpec {
ToolSpec {
name: "team_delete".into(),
description: concat!(
"Delete a team and its roster file. Idempotent: deleting a ",
"non-existent team is a no-op (returns an informative message)."
)
.into(),
parameters: json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The team name to delete."
}
},
"required": ["name"]
}),
}
}
fn side_effect_class(&self) -> ToolSideEffect {
ToolSideEffect::Mutating
}
async fn execute(&self, arguments: Value) -> Result<String> {
let name = arguments["name"]
.as_str()
.ok_or_else(|| Error::BadToolArgs {
name: "team_delete".into(),
message: "missing required parameter: name".to_string(),
})?
.to_string();
if name.is_empty() || name.contains('/') || name.contains('\\') || name.contains("..") {
return Err(Error::BadToolArgs {
name: "team_delete".into(),
message: format!("invalid team name: '{name}'"),
});
}
let existed = TeamRegistry::delete(&name).await?;
if existed {
Ok(format!("Deleted team '{name}'."))
} else {
Ok(format!("Team '{name}' did not exist (no-op)."))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn with_temp_teams_dir() -> crate::test_util::PinnedTeamsDir {
crate::test_util::PinnedTeamsDir::new()
}
#[tokio::test]
async fn delete_existing_team() {
let _g = with_temp_teams_dir();
TeamRegistry::create("alpha").await.unwrap();
assert!(crate::team::team_file_path("alpha").exists());
let tool = TeamDeleteTool::new();
let result = tool.execute(json!({ "name": "alpha" })).await.unwrap();
assert!(result.contains("Deleted"));
assert!(!crate::team::team_file_path("alpha").exists());
}
#[tokio::test]
async fn delete_nonexistent_is_idempotent() {
let _g = with_temp_teams_dir();
let tool = TeamDeleteTool::new();
let result = tool.execute(json!({ "name": "ghost" })).await.unwrap();
assert!(result.contains("did not exist"));
let result2 = tool.execute(json!({ "name": "ghost" })).await.unwrap();
assert!(result2.contains("did not exist"));
}
#[tokio::test]
async fn delete_rejects_invalid_name() {
let _g = with_temp_teams_dir();
let tool = TeamDeleteTool::new();
for bad in ["", "../etc", "a/b"] {
let res = tool.execute(json!({ "name": bad })).await;
assert!(res.is_err(), "name '{bad}' should be rejected");
}
}
#[tokio::test]
async fn delete_missing_name_field_errors() {
let _g = with_temp_teams_dir();
let tool = TeamDeleteTool::new();
let res = tool.execute(json!({})).await;
assert!(
matches!(res, Err(Error::BadToolArgs { .. })),
"missing 'name' field must return BadToolArgs"
);
}
#[tokio::test]
async fn delete_rejects_backslash_name() {
let _g = with_temp_teams_dir();
let tool = TeamDeleteTool::new();
let res = tool.execute(json!({ "name": "a\\b" })).await;
assert!(res.is_err(), "backslash in name must be rejected");
}
}