use std::sync::Arc;
use async_trait::async_trait;
use meerkat_core::ToolDef;
use meerkat_core::types::{ToolProvenance, ToolSourceKind};
use serde::Deserialize;
use serde_json::Value;
use crate::builtin::store::TaskStore;
use crate::builtin::types::TaskId;
use crate::builtin::{BuiltinTool, BuiltinToolError, ToolOutput};
#[derive(Debug, Deserialize, schemars::JsonSchema)]
struct TaskGetParams {
#[schemars(description = "The task ID")]
id: String,
}
pub struct TaskGetTool {
store: Arc<dyn TaskStore>,
}
impl TaskGetTool {
pub fn new(store: Arc<dyn TaskStore>) -> Self {
Self { store }
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl BuiltinTool for TaskGetTool {
fn name(&self) -> &'static str {
"task_get"
}
fn def(&self) -> ToolDef {
ToolDef {
name: "task_get".into(),
description: "Get a task by its ID".into(),
input_schema: crate::schema::schema_for::<TaskGetParams>(),
provenance: Some(ToolProvenance {
kind: ToolSourceKind::Builtin,
source_id: "builtin".into(),
}),
}
}
fn default_enabled(&self) -> bool {
true
}
async fn call(&self, args: Value) -> Result<ToolOutput, BuiltinToolError> {
let params: TaskGetParams = serde_json::from_value(args)
.map_err(|e| BuiltinToolError::InvalidArgs(e.to_string()))?;
let id = TaskId(params.id);
let task = self
.store
.get(&id)
.await
.map_err(|e| BuiltinToolError::TaskError(e.to_string()))?;
match task {
Some(t) => serde_json::to_value(&t)
.map(ToolOutput::Json)
.map_err(|e| BuiltinToolError::ExecutionFailed(e.to_string())),
None => Err(BuiltinToolError::ExecutionFailed(format!(
"Task not found: {id}"
))),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::builtin::memory_store::MemoryTaskStore;
use crate::builtin::types::{Task, TaskPriority, TaskStatus};
use serde_json::json;
fn create_test_store() -> Arc<MemoryTaskStore> {
Arc::new(MemoryTaskStore::new())
}
fn create_test_store_with_task() -> (Arc<MemoryTaskStore>, Task) {
let task = Task {
id: TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAV"),
subject: "Test task".to_string(),
description: "A test task description".to_string(),
status: TaskStatus::InProgress,
priority: TaskPriority::High,
labels: vec!["test".to_string(), "important".to_string()],
blocks: vec![TaskId::from_string("01ARZ3NDEKTSV4RRFFQ69G5FAW")],
owner: Some("alice".to_string()),
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
created_at: "2025-01-23T10:00:00Z".to_string(),
updated_at: "2025-01-23T11:00:00Z".to_string(),
created_by_session: Some("session-123".to_string()),
updated_by_session: Some("session-456".to_string()),
};
let store = Arc::new(MemoryTaskStore::with_tasks(vec![task.clone()]));
(store, task)
}
#[test]
fn test_task_get_tool_def() {
let store = create_test_store();
let tool = TaskGetTool::new(store);
assert_eq!(tool.name(), "task_get");
let def = tool.def();
assert_eq!(def.name, "task_get");
assert_eq!(def.description, "Get a task by its ID");
let schema = def.input_schema;
assert_eq!(schema["type"], "object");
assert!(schema["properties"]["id"].is_object());
assert_eq!(schema["properties"]["id"]["type"], "string");
assert_eq!(schema["properties"]["id"]["description"], "The task ID");
assert_eq!(schema["required"], json!(["id"]));
assert!(tool.default_enabled());
}
#[tokio::test]
async fn test_task_get_existing() {
let (store, expected_task) = create_test_store_with_task();
let tool = TaskGetTool::new(store);
let args = json!({
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
});
let result = tool.call(args).await.unwrap().into_json().unwrap();
assert_eq!(result["id"], "01ARZ3NDEKTSV4RRFFQ69G5FAV");
assert_eq!(result["subject"], expected_task.subject);
assert_eq!(result["description"], expected_task.description);
assert_eq!(result["status"], "in_progress");
assert_eq!(result["priority"], "high");
assert_eq!(result["labels"], json!(["test", "important"]));
assert_eq!(result["blocks"], json!(["01ARZ3NDEKTSV4RRFFQ69G5FAW"]));
assert_eq!(result["created_at"], expected_task.created_at);
assert_eq!(result["updated_at"], expected_task.updated_at);
assert_eq!(result["created_by_session"], "session-123");
assert_eq!(result["updated_by_session"], "session-456");
}
#[tokio::test]
async fn test_task_get_not_found() {
let store = create_test_store();
let tool = TaskGetTool::new(store);
let args = json!({
"id": "nonexistent-task-id"
});
let result = tool.call(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BuiltinToolError::ExecutionFailed(msg) => {
assert!(msg.contains("Task not found"));
assert!(msg.contains("nonexistent-task-id"));
}
_ => unreachable!("Expected ExecutionFailed error, got {:?}", err),
}
}
#[tokio::test]
async fn test_task_get_missing_id() {
let store = create_test_store();
let tool = TaskGetTool::new(store);
let args = json!({});
let result = tool.call(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BuiltinToolError::InvalidArgs(msg) => {
assert!(msg.contains("id") || msg.contains("missing"));
}
_ => unreachable!("Expected InvalidArgs error, got {:?}", err),
}
}
#[tokio::test]
async fn test_task_get_wrong_type_id() {
let store = create_test_store();
let tool = TaskGetTool::new(store);
let args = json!({
"id": 12345
});
let result = tool.call(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BuiltinToolError::InvalidArgs(msg) => {
assert!(msg.contains("string") || msg.contains("invalid type"));
}
_ => unreachable!("Expected InvalidArgs error, got {:?}", err),
}
}
#[tokio::test]
async fn test_task_get_null_id() {
let store = create_test_store();
let tool = TaskGetTool::new(store);
let args = json!({
"id": null
});
let result = tool.call(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BuiltinToolError::InvalidArgs(_) => {}
_ => unreachable!("Expected InvalidArgs error, got {:?}", err),
}
}
#[tokio::test]
async fn test_task_get_extra_fields_ignored() {
let (store, expected_task) = create_test_store_with_task();
let tool = TaskGetTool::new(store);
let args = json!({
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"extra_field": "should be ignored",
"another_extra": 123
});
let result = tool.call(args).await.unwrap().into_json().unwrap();
assert_eq!(result["id"], "01ARZ3NDEKTSV4RRFFQ69G5FAV");
assert_eq!(result["subject"], expected_task.subject);
}
#[tokio::test]
async fn test_task_get_empty_string_id() {
let store = create_test_store();
let tool = TaskGetTool::new(store);
let args = json!({
"id": ""
});
let result = tool.call(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
match err {
BuiltinToolError::ExecutionFailed(msg) => {
assert!(msg.contains("Task not found"));
}
_ => unreachable!("Expected ExecutionFailed error, got {:?}", err),
}
}
#[tokio::test]
async fn test_task_get_returns_complete_task_structure() {
let (store, _) = create_test_store_with_task();
let tool = TaskGetTool::new(store);
let args = json!({
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV"
});
let result = tool.call(args).await.unwrap().into_json().unwrap();
assert!(result.get("id").is_some());
assert!(result.get("subject").is_some());
assert!(result.get("description").is_some());
assert!(result.get("status").is_some());
assert!(result.get("priority").is_some());
assert!(result.get("labels").is_some());
assert!(result.get("blocks").is_some());
assert!(result.get("created_at").is_some());
assert!(result.get("updated_at").is_some());
assert!(result.get("created_by_session").is_some());
assert!(result.get("updated_by_session").is_some());
}
}