use async_trait::async_trait;
use meerkat_core::ToolDef;
use serde::Deserialize;
use serde_json::Value;
use std::sync::Arc;
use super::config::ShellError;
use super::job_manager::JobManager;
use super::types::JobId;
use crate::builtin::{BuiltinTool, BuiltinToolError, ToolOutput};
#[derive(Debug)]
pub struct ShellJobStatusTool {
job_manager: Arc<JobManager>,
}
impl ShellJobStatusTool {
pub fn new(job_manager: Arc<JobManager>) -> Self {
Self { job_manager }
}
}
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema)]
struct JobStatusInput {
#[schemars(description = "The job ID to check")]
job_id: String,
}
#[async_trait]
impl BuiltinTool for ShellJobStatusTool {
fn name(&self) -> &'static str {
"shell_job_status"
}
fn def(&self) -> ToolDef {
ToolDef {
name: "shell_job_status".into(),
description: "Check status of a background shell job".into(),
input_schema: crate::schema::schema_for::<JobStatusInput>(),
}
}
fn default_enabled(&self) -> bool {
false
}
async fn call(&self, args: Value) -> Result<ToolOutput, BuiltinToolError> {
let input: JobStatusInput = serde_json::from_value(args)
.map_err(|e| BuiltinToolError::invalid_args(e.to_string()))?;
let job_id = JobId::from_string(&input.job_id);
let job = self.job_manager.get_status(&job_id).await.ok_or_else(|| {
BuiltinToolError::execution_failed(
ShellError::JobNotFound(input.job_id.clone()).to_string(),
)
})?;
serde_json::to_value(job)
.map(ToolOutput::Json)
.map_err(|e| BuiltinToolError::execution_failed(e.to_string()))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::builtin::shell::config::ShellConfig;
use serde_json::json;
#[cfg(feature = "integration-real-tests")]
use tempfile::TempDir;
#[test]
fn test_shell_job_status_tool_struct() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let _tool = ShellJobStatusTool::new(Arc::clone(&manager));
assert!(Arc::strong_count(&manager) >= 2);
}
#[test]
fn test_shell_job_status_tool_builtin() {
fn assert_builtin_tool<T: BuiltinTool>() {}
assert_builtin_tool::<ShellJobStatusTool>();
}
#[test]
fn test_shell_job_status_tool_name() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobStatusTool::new(manager);
assert_eq!(tool.name(), "shell_job_status");
}
#[test]
fn test_shell_job_status_tool_schema() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobStatusTool::new(manager);
let def = tool.def();
assert_eq!(def.name, "shell_job_status");
assert!(def.description.contains("status"));
let schema = &def.input_schema;
assert_eq!(schema["type"], "object");
let props = &schema["properties"];
assert!(props.get("job_id").is_some());
assert_eq!(props["job_id"]["type"], "string");
let required = schema["required"].as_array().unwrap();
assert!(required.contains(&json!("job_id")));
}
#[tokio::test]
#[cfg(feature = "integration-real-tests")]
#[ignore = "integration-real: spawns shell processes"]
async fn integration_real_shell_job_status_tool_output() {
let temp_dir = TempDir::new().unwrap();
let mut config = ShellConfig::with_project_root(temp_dir.path().to_path_buf());
config.shell = "sh".to_string();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobStatusTool::new(Arc::clone(&manager));
let job_id = manager.spawn_job("echo test", None, 30).await.unwrap();
let result = tool
.call(json!({
"job_id": job_id.0
}))
.await
.unwrap();
let result = result.into_json().unwrap();
assert!(result.get("id").is_some());
assert!(result.get("command").is_some());
assert!(result.get("status").is_some());
assert!(result.get("timeout_secs").is_some());
assert_eq!(result["id"], job_id.0);
assert_eq!(result["command"], "echo test");
}
#[tokio::test]
async fn test_shell_job_status_tool_not_found() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobStatusTool::new(manager);
let result = tool
.call(json!({
"job_id": "job_nonexistent123456789012"
}))
.await;
assert!(matches!(result, Err(BuiltinToolError::ExecutionFailed(_))));
if let Err(BuiltinToolError::ExecutionFailed(msg)) = result {
assert!(msg.contains("not found") || msg.contains("Job not found"));
}
}
#[tokio::test]
async fn test_shell_job_status_tool_invalid_args() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobStatusTool::new(manager);
let result = tool.call(json!({})).await;
assert!(matches!(result, Err(BuiltinToolError::InvalidArgs(_))));
}
}