use serde_json::json;
use std::collections::HashMap;
use tempfile::NamedTempFile;
use warmplane::{
config::{save_config, McpConfig, ServerConfig},
EmbeddedWarmplane,
};
#[tokio::test]
async fn test_embedded_warmplane_start_and_shutdown() {
let temp_config = NamedTempFile::new().unwrap();
let config_path = temp_config.path().to_str().unwrap().to_string();
let initial_config = McpConfig::default();
save_config(&config_path, &initial_config).unwrap();
let (cp, shutdown_token) = EmbeddedWarmplane::start(initial_config)
.await
.expect("EmbeddedWarmplane must boot cleanly");
assert!(!shutdown_token.is_cancelled());
let health = cp.health_status().await;
assert!(health.catalog_version.starts_with("sha256:") || health.catalog_version.is_empty());
assert_eq!(health.total_tool_calls, 0);
let list = cp
.list_capabilities(None)
.await
.expect("list_capabilities must succeed");
assert_eq!(list.version, "v1");
assert!(list.capabilities.is_empty());
cp.shutdown().await;
assert!(shutdown_token.is_cancelled());
}
#[tokio::test]
async fn test_embedded_warmplane_call_capability_lifecycle() {
let mut mcp_servers = HashMap::new();
mcp_servers.insert(
"offline_srv".to_string(),
ServerConfig {
command: None,
args: vec![],
env: HashMap::new(),
url: Some("http://127.0.0.1:59998/mcp".to_string()),
auth: None,
protocol_version: None,
allow_stateless: Some(true),
headers: HashMap::new(),
resilience: None,
},
);
let config = McpConfig {
mcp_servers,
..Default::default()
};
let (cp, _shutdown) = EmbeddedWarmplane::start(config)
.await
.expect("EmbeddedWarmplane must boot in degraded mode for unreachable server");
let env = cp
.call_capability(
"unknown_cap",
json!({}),
warmplane::engine::ExecutionOptions::default().with_request_id("req-123"),
)
.await;
assert!(!env.ok);
assert_eq!(env.request_id, Some("req-123".to_string()));
let err = env.error.expect("error must be populated");
assert_eq!(err.code, "TOOL_NOT_FOUND");
assert!(!err.retryable);
let desc_env = cp.describe_capability("unknown_cap", None).await;
assert!(!desc_env.ok);
assert_eq!(desc_env.error.expect("error").code, "TOOL_NOT_FOUND");
let batch_res = cp
.batch_call(
vec![warmplane::batch_executor::BatchStep {
id: "step1".to_string(),
capability_id: "missing_cap".to_string(),
args: json!({}),
continue_on_error: false,
}],
Some("batch-req-1".to_string()),
None,
None,
)
.await;
assert!(!batch_res.ok);
assert_eq!(batch_res.results.len(), 1);
assert_eq!(batch_res.results[0].id, "step1");
assert!(!batch_res.results[0].ok);
cp.shutdown().await;
}
#[tokio::test]
async fn test_embedded_warmplane_start_from_path() {
let temp_config = NamedTempFile::new().unwrap();
let config_path = temp_config.path().to_str().unwrap().to_string();
let config = McpConfig::default();
save_config(&config_path, &config).unwrap();
let (cp, _shutdown) = EmbeddedWarmplane::start_from_path(&config_path)
.await
.expect("EmbeddedWarmplane::start_from_path must succeed");
let list = cp.list_capabilities(None).await.unwrap();
assert_eq!(list.version, "v1");
cp.shutdown().await;
}
#[tokio::test]
async fn test_embedded_task_management_lifecycle() {
use std::collections::BTreeMap;
use warmplane::config::{PolicyConfig, ServerConfig};
use warmplane::daemon::CapabilityMeta;
use warmplane::engine::{ExecutionOptions, TaskStatus};
let mut mcp_servers = HashMap::new();
mcp_servers.insert(
"offline_srv".to_string(),
ServerConfig {
command: None,
args: vec![],
env: HashMap::new(),
url: Some("http://127.0.0.1:59997/mcp".to_string()),
auth: None,
protocol_version: None,
allow_stateless: Some(true),
headers: HashMap::new(),
resilience: None,
},
);
let policy = PolicyConfig {
require_approval: vec!["offline_srv/*".to_string()],
approval_timeout_secs: Some(30),
..Default::default()
};
let config = McpConfig {
mcp_servers,
policy: Some(policy),
state: Some(warmplane::config::StateConfig {
enabled: false,
dir: None,
}),
..Default::default()
};
let (cp, _shutdown) = EmbeddedWarmplane::start(config)
.await
.expect("EmbeddedWarmplane must boot cleanly");
{
let mut caps = cp.state().capabilities.write().await;
caps.insert(
"offline_srv/delete_record".to_string(),
CapabilityMeta {
server: "offline_srv".to_string(),
tool: "delete_record".to_string(),
summary: "Delete record".to_string(),
description: "Mutating tool requiring review".to_string(),
input_schema: json!({"type": "object"}),
tags: vec![],
examples: vec![],
},
);
}
let initial_tasks = cp.list_tasks().await;
assert!(initial_tasks.is_empty());
let async_res = cp
.call_capability(
"offline_srv/delete_record",
json!({"id": "rec-99"}),
ExecutionOptions::default()
.with_request_id("req-task-test")
.with_async_task(true),
)
.await;
assert!(async_res.ok);
let task_payload = async_res.data.expect("data must contain task info");
let task_id = task_payload
.get("taskId")
.and_then(|v| v.as_str())
.expect("taskId must be present");
let tasks = cp.list_tasks().await;
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].task_id, task_id);
assert_eq!(tasks[0].status, TaskStatus::InputRequired);
assert!(tasks[0].input_requests.is_some());
let get_env = cp.get_task(task_id).await;
assert!(get_env.ok);
let task_resp = get_env.data.expect("task response");
assert_eq!(task_resp.task_id, task_id);
assert_eq!(task_resp.status, TaskStatus::InputRequired);
let cancel_env = cp
.cancel_task(task_id, Some("Operator cancelled test run".to_string()))
.await;
assert!(cancel_env.ok);
assert!(cancel_env.data.unwrap_or(false));
let updated_task = cp.get_task(task_id).await;
assert_eq!(
updated_task.data.expect("task").status,
TaskStatus::Cancelled
);
let repeat_cancel = cp.cancel_task(task_id, None).await;
assert!(!repeat_cancel.ok);
assert_eq!(
repeat_cancel.error.expect("error").code,
"TASK_CANCEL_REJECTED"
);
let missing_env = cp.get_task("task-non-existent").await;
assert!(!missing_env.ok);
assert_eq!(missing_env.error.expect("error").code, "TASK_NOT_FOUND");
let mut fake_responses = BTreeMap::new();
fake_responses.insert("operator_approval".to_string(), json!({"approved": true}));
let missing_update = cp.update_task("task-non-existent", fake_responses).await;
assert!(!missing_update.ok);
assert_eq!(
missing_update.error.expect("error").code,
"TASK_UPDATE_REJECTED"
);
cp.shutdown().await;
}