初始化提交
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
This commit is contained in:
541
tests/e2e_api_test.rs
Normal file
541
tests/e2e_api_test.rs
Normal file
@@ -0,0 +1,541 @@
|
||||
//! Basic API endpoint tests for OpenFang.
|
||||
//!
|
||||
//! These tests verify the core API endpoints work correctly:
|
||||
//! - Health and status endpoints
|
||||
//! - Agent CRUD operations
|
||||
//! - Session management
|
||||
//! - Error handling
|
||||
|
||||
use super::e2e_common::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health & Status Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_endpoint_returns_ok() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/health").await;
|
||||
|
||||
assert_eq!(response["status"], "ok");
|
||||
assert!(response["version"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_detail_endpoint() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/health/detail").await;
|
||||
|
||||
assert_eq!(response["status"], "ok");
|
||||
// Detailed health includes more information
|
||||
assert!(response["version"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_status_endpoint() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/status").await;
|
||||
|
||||
assert_eq!(response["status"], "running");
|
||||
assert_eq!(response["agent_count"], 0);
|
||||
assert!(response["uptime_seconds"].is_number());
|
||||
assert!(response["default_provider"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_version_endpoint() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/version").await;
|
||||
|
||||
assert!(response["version"].is_string());
|
||||
assert!(response["commit"].is_string() || response["commit"].is_null());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent CRUD Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_spawn_agent() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
|
||||
|
||||
assert!(!agent.id.is_empty());
|
||||
assert_eq!(agent.name, "test-agent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_agents_empty() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let agents = list_agents(&daemon).await;
|
||||
|
||||
assert_eq!(agents.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_agents_with_agents() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
// Create two agents
|
||||
let agent1 = create_named_test_agent(&daemon, "agent-1").await;
|
||||
let agent2 = create_named_test_agent(&daemon, "agent-2").await;
|
||||
|
||||
let agents = list_agents(&daemon).await;
|
||||
|
||||
assert_eq!(agents.len(), 2);
|
||||
|
||||
// Verify both agents are present
|
||||
let ids: Vec<&str> = agents.iter().filter_map(|a| a["id"].as_str()).collect();
|
||||
assert!(ids.contains(&agent1.id.as_str()));
|
||||
assert!(ids.contains(&agent2.id.as_str()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_agent_by_id() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
|
||||
|
||||
let response = get(&daemon, &format!("/api/agents/{}", agent.id)).await;
|
||||
|
||||
assert_eq!(response["id"], agent.id);
|
||||
assert_eq!(response["name"], "test-agent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_kill_agent() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
|
||||
assert_eq!(agent_count(&daemon).await, 1);
|
||||
|
||||
kill_agent(&daemon, &agent.id).await;
|
||||
|
||||
assert_eq!(agent_count(&daemon).await, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_kill_nonexistent_agent_returns_404() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let fake_id = uuid::Uuid::new_v4().to_string();
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let resp = client
|
||||
.delete(format!("{}/api/agents/{}", daemon.base_url(), fake_id))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 404);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_spawn_multiple_agents() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
// Create 5 agents
|
||||
let mut agent_ids = Vec::new();
|
||||
for i in 0..5 {
|
||||
let agent = create_named_test_agent(&daemon, &format!("agent-{}", i)).await;
|
||||
agent_ids.push(agent.id);
|
||||
}
|
||||
|
||||
assert_eq!(agent_count(&daemon).await, 5);
|
||||
|
||||
// Kill all agents
|
||||
for id in agent_ids {
|
||||
kill_agent(&daemon, &id).await;
|
||||
}
|
||||
|
||||
assert_eq!(agent_count(&daemon).await, 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Error Handling Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid_agent_id_returns_400() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Invalid (non-UUID) agent ID
|
||||
let resp = client
|
||||
.get(format!("{}/api/agents/not-a-uuid", daemon.base_url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 400);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
assert!(body["error"].as_str().unwrap().contains("Invalid"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid_manifest_returns_400() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let resp = client
|
||||
.post(format!("{}/api/agents", daemon.base_url()))
|
||||
.json(&serde_json::json!({"manifest_toml": "invalid {{ toml"}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 400);
|
||||
let body: serde_json::Value = resp.json().await.unwrap();
|
||||
assert!(body["error"].as_str().unwrap().contains("Invalid manifest"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_message_to_nonexistent_agent_returns_404() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let fake_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let resp = client
|
||||
.post(format!(
|
||||
"{}/api/agents/{}/message",
|
||||
daemon.base_url(),
|
||||
fake_id
|
||||
))
|
||||
.json(&serde_json::json!({"message": "hello"}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 404);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_agent_session_empty_initially() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
|
||||
|
||||
let response = get(&daemon, &format!("/api/agents/{}/session", agent.id)).await;
|
||||
|
||||
assert_eq!(response["message_count"], 0);
|
||||
assert_eq!(response["messages"].as_array().unwrap().len(), 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_is_public_without_auth() {
|
||||
// Start daemon with auth enabled
|
||||
let config = DaemonConfig::default().with_auth_key("secret-key-123");
|
||||
let daemon = spawn_daemon_with_config(config).await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
// Health endpoint should be accessible without auth
|
||||
let response = get(&daemon, "/api/health").await;
|
||||
assert_eq!(response["status"], "ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_protected_endpoint_requires_auth() {
|
||||
// Start daemon with auth enabled
|
||||
let config = DaemonConfig::default().with_auth_key("secret-key-123");
|
||||
let daemon = spawn_daemon_with_config(config).await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Protected endpoint without auth header should return 401
|
||||
let resp = client
|
||||
.get(format!("{}/api/models", daemon.base_url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 401);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_protected_endpoint_with_correct_auth() {
|
||||
// Start daemon with auth enabled
|
||||
let config = DaemonConfig::default().with_auth_key("secret-key-123");
|
||||
let daemon = spawn_daemon_with_config(config).await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Protected endpoint with correct auth header should succeed
|
||||
let resp = client
|
||||
.get(format!("{}/api/models", daemon.base_url()))
|
||||
.header("authorization", "Bearer secret-key-123")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 200);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_protected_endpoint_rejects_wrong_auth() {
|
||||
// Start daemon with auth enabled
|
||||
let config = DaemonConfig::default().with_auth_key("secret-key-123");
|
||||
let daemon = spawn_daemon_with_config(config).await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Protected endpoint with wrong auth header should return 401
|
||||
let resp = client
|
||||
.get(format!("{}/api/models", daemon.base_url()))
|
||||
.header("authorization", "Bearer wrong-key")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resp.status(), 401);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Request ID Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_id_header_is_present() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let resp = client
|
||||
.get(format!("{}/api/health", daemon.base_url()))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let request_id = resp
|
||||
.headers()
|
||||
.get("x-request-id")
|
||||
.expect("x-request-id header should be present");
|
||||
|
||||
let id_str = request_id.to_str().unwrap();
|
||||
assert!(
|
||||
uuid::Uuid::parse_str(id_str).is_ok(),
|
||||
"x-request-id should be a valid UUID, got: {}",
|
||||
id_str
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_config() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/config").await;
|
||||
|
||||
// Config should have basic fields
|
||||
assert!(response.is_object());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider & Model Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_providers() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/providers").await;
|
||||
|
||||
assert!(response.is_array());
|
||||
// Should have at least the default provider
|
||||
let providers = response.as_array().unwrap();
|
||||
assert!(!providers.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_models() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/models").await;
|
||||
|
||||
assert!(response.is_array());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workflow_crud() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
// Create agent for workflow
|
||||
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
|
||||
|
||||
// Create workflow
|
||||
let workflow = create_test_workflow(&agent.name);
|
||||
let response = post(&daemon, "/api/workflows", &workflow).await;
|
||||
|
||||
assert!(response["workflow_id"].is_string());
|
||||
let workflow_id = response["workflow_id"].as_str().unwrap();
|
||||
|
||||
// List workflows
|
||||
let workflows = get(&daemon, "/api/workflows").await;
|
||||
assert_eq!(workflows.as_array().unwrap().len(), 1);
|
||||
assert_eq!(workflows[0]["name"], "test-workflow");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trigger Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_trigger_crud() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
// Create agent for trigger
|
||||
let agent = create_test_agent(&daemon, DEFAULT_MANIFEST).await;
|
||||
|
||||
// Create trigger
|
||||
let trigger = create_test_trigger(&agent.id);
|
||||
let response = post(&daemon, "/api/triggers", &trigger).await;
|
||||
|
||||
assert!(response["trigger_id"].is_string());
|
||||
assert_eq!(response["agent_id"], agent.id);
|
||||
|
||||
// List triggers
|
||||
let triggers = get(&daemon, "/api/triggers").await;
|
||||
assert_eq!(triggers.as_array().unwrap().len(), 1);
|
||||
assert_eq!(triggers[0]["enabled"], true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Budget Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_budget_status() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/budget").await;
|
||||
|
||||
// Budget should have basic fields
|
||||
assert!(response["enabled"].is_boolean() || response["daily_limit"].is_number());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Usage Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_usage_stats() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/usage").await;
|
||||
|
||||
assert!(response.is_object());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_usage_summary() {
|
||||
let daemon = spawn_daemon().await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let response = get(&daemon, "/api/usage/summary").await;
|
||||
|
||||
assert!(response.is_object());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LLM Integration Tests (require API key)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Requires GROQ_API_KEY environment variable"]
|
||||
async fn test_send_message_with_real_llm() {
|
||||
if !can_run_llm_tests() {
|
||||
eprintln!("{}", llm_skip_reason());
|
||||
return;
|
||||
}
|
||||
|
||||
let config = DaemonConfig::groq();
|
||||
let daemon = spawn_daemon_with_config(config).await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
let agent = create_test_agent(&daemon, LLM_MANIFEST).await;
|
||||
let response = send_message(&daemon, &agent.id, "Say hello in exactly 3 words.").await;
|
||||
|
||||
assert!(!response.response.is_empty(), "LLM response should not be empty");
|
||||
assert!(response.input_tokens > 0, "Should have input tokens");
|
||||
assert!(response.output_tokens > 0, "Should have output tokens");
|
||||
|
||||
// Verify session has messages
|
||||
let session = get(&daemon, &format!("/api/agents/{}/session", agent.id)).await;
|
||||
assert!(session["message_count"].as_u64().unwrap() > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "Requires GROQ_API_KEY environment variable"]
|
||||
async fn test_budget_increases_after_llm_call() {
|
||||
if !can_run_llm_tests() {
|
||||
eprintln!("{}", llm_skip_reason());
|
||||
return;
|
||||
}
|
||||
|
||||
let config = DaemonConfig::groq();
|
||||
let daemon = spawn_daemon_with_config(config).await;
|
||||
wait_for_health(daemon.base_url()).await;
|
||||
|
||||
// Get initial budget
|
||||
let initial_budget = get(&daemon, "/api/budget").await;
|
||||
|
||||
// Make an LLM call
|
||||
let agent = create_test_agent(&daemon, LLM_MANIFEST).await;
|
||||
let _ = send_message(&daemon, &agent.id, "Hello").await;
|
||||
|
||||
// Get updated budget
|
||||
let updated_budget = get(&daemon, "/api/budget").await;
|
||||
|
||||
// Budget tracking should reflect the usage
|
||||
// (The exact structure depends on the implementation)
|
||||
assert!(updated_budget.is_object());
|
||||
}
|
||||
Reference in New Issue
Block a user