use crate::schema::empty_object_schema;
use async_trait::async_trait;
use meerkat_core::ToolDef;
use meerkat_core::types::{ToolProvenance, ToolSourceKind};
use serde_json::Value;
use std::sync::Arc;
use super::job_manager::JobManager;
use crate::builtin::{BuiltinTool, BuiltinToolError, ToolOutput};
#[derive(Debug)]
pub struct ShellJobsListTool {
job_manager: Arc<JobManager>,
}
impl ShellJobsListTool {
pub fn new(job_manager: Arc<JobManager>) -> Self {
Self { job_manager }
}
}
#[async_trait]
impl BuiltinTool for ShellJobsListTool {
fn name(&self) -> &'static str {
"shell_jobs"
}
fn def(&self) -> ToolDef {
ToolDef {
name: "shell_jobs".into(),
description: "List all background shell jobs".into(),
input_schema: empty_object_schema(),
provenance: Some(ToolProvenance {
kind: ToolSourceKind::Shell,
source_id: "shell".into(),
}),
}
}
fn default_enabled(&self) -> bool {
false
}
async fn call(&self, _args: Value) -> Result<ToolOutput, BuiltinToolError> {
let jobs = self
.job_manager
.list_jobs()
.await
.map_err(|error| BuiltinToolError::execution_failed(error.to_string()))?;
serde_json::to_value(jobs)
.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_jobs_list_tool_struct() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let _tool = ShellJobsListTool::new(Arc::clone(&manager));
assert!(Arc::strong_count(&manager) >= 2);
}
#[test]
fn test_shell_jobs_list_tool_builtin() {
fn assert_builtin_tool<T: BuiltinTool>() {}
assert_builtin_tool::<ShellJobsListTool>();
}
#[test]
fn test_shell_jobs_list_tool_name() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobsListTool::new(manager);
assert_eq!(tool.name(), "shell_jobs");
}
#[test]
fn test_shell_jobs_list_tool_schema() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobsListTool::new(manager);
let def = tool.def();
assert_eq!(def.name, "shell_jobs");
assert!(def.description.contains("List") || def.description.contains("list"));
let schema = &def.input_schema;
assert_eq!(schema["type"], "object");
let props = schema.get("properties").unwrap();
assert!(props.as_object().is_none_or(serde_json::Map::is_empty));
assert_eq!(schema["required"], serde_json::json!([]));
}
#[tokio::test]
#[cfg(feature = "integration-real-tests")]
#[ignore = "integration-real: spawns shell processes"]
async fn integration_real_shell_jobs_list_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 = ShellJobsListTool::new(Arc::clone(&manager));
let result = tool.call(json!({})).await.unwrap().into_json().unwrap();
let jobs = result.as_array().unwrap();
assert!(jobs.is_empty());
let _id1 = manager.spawn_job("echo one", None, 30).await.unwrap();
let _id2 = manager.spawn_job("echo two", None, 30).await.unwrap();
let result = tool.call(json!({})).await.unwrap().into_json().unwrap();
let jobs = result.as_array().unwrap();
assert_eq!(jobs.len(), 2);
for job in jobs {
assert!(job.get("id").is_some());
assert!(job.get("command").is_some());
assert!(job.get("status").is_some());
assert!(job.get("started_at_unix").is_some());
}
}
#[tokio::test]
async fn test_shell_jobs_list_tool_empty_list() {
let config = ShellConfig::default();
let manager = Arc::new(JobManager::new(config));
let tool = ShellJobsListTool::new(manager);
let result = tool.call(json!({})).await.unwrap().into_json().unwrap();
let jobs = result.as_array().unwrap();
assert!(jobs.is_empty());
}
}