use std::sync::Arc;
use adk_gateway::agent_config::{AgentConfig, AgentRoleConfig, AgentType, LifecycleState};
use adk_gateway::agent_registry::AgentRegistry;
use adk_gateway::proxy_pool::RemoteAgentProxyPool;
use adk_gateway::rbac_bridge::{RbacBridge, SYSTEM_TOOLS};
use tempfile::TempDir;
fn make_config(id: &str) -> AgentConfig {
AgentConfig {
id: id.to_string(),
name: format!("Agent {}", id),
description: format!("Test agent {}", id),
agent_type: AgentType::Llm,
model: "test/model".to_string(),
api_key_env: "TEST_KEY".to_string(),
instruction: "do stuff".to_string(),
tools: vec![],
action_nodes: vec![],
workflow_edges: vec![],
sub_agents: vec![],
role: AgentRoleConfig {
allow: vec![],
deny: vec![],
},
channel_bindings: vec![],
auto_start: false,
temperature: None,
max_output_tokens: None,
model_override: None,
}
}
fn make_ctx(
tmp: &TempDir,
) -> (
Arc<AgentRegistry>,
Arc<RbacBridge>,
Arc<RemoteAgentProxyPool>,
) {
let workspace_root = tmp.path().to_path_buf();
let persist_dir = workspace_root.join("registry");
(
Arc::new(AgentRegistry::new(persist_dir)),
Arc::new(RbacBridge::new()),
Arc::new(RemoteAgentProxyPool::new()),
)
}
#[tokio::test]
async fn agent_lifecycle_create_start_stop_delete() {
let tmp = TempDir::new().unwrap();
let (registry, rbac, proxy_pool) = make_ctx(&tmp);
let config = make_config("lifecycle-agent");
registry.create_agent(config.clone()).unwrap();
rbac.register_agent("lifecycle-agent", &config.role);
let record = registry.get("lifecycle-agent").unwrap();
assert_eq!(record.state, LifecycleState::Created);
drop(record);
registry
.transition("lifecycle-agent", LifecycleState::Starting)
.unwrap();
registry
.transition("lifecycle-agent", LifecycleState::Running)
.unwrap();
proxy_pool.register("lifecycle-agent", 19050);
let record = registry.get("lifecycle-agent").unwrap();
assert_eq!(record.state, LifecycleState::Running);
drop(record);
let proxy = proxy_pool.get("lifecycle-agent").unwrap();
assert_eq!(proxy.agent_url(), "http://127.0.0.1:19050");
registry
.transition("lifecycle-agent", LifecycleState::Stopping)
.unwrap();
proxy_pool.remove("lifecycle-agent");
registry
.transition("lifecycle-agent", LifecycleState::Stopped)
.unwrap();
let record = registry.get("lifecycle-agent").unwrap();
assert_eq!(record.state, LifecycleState::Stopped);
drop(record);
assert!(proxy_pool.get("lifecycle-agent").is_none());
let delete_result = registry.delete("lifecycle-agent");
assert!(delete_result.is_ok());
rbac.remove_agent("lifecycle-agent");
assert!(registry.get("lifecycle-agent").is_none());
}
#[tokio::test]
async fn route_message_to_running_agent_via_proxy() {
let tmp = TempDir::new().unwrap();
let (registry, _rbac, proxy_pool) = make_ctx(&tmp);
let config_a = make_config("agent-a");
registry.create_agent(config_a).unwrap();
registry
.transition("agent-a", LifecycleState::Starting)
.unwrap();
registry
.transition("agent-a", LifecycleState::Running)
.unwrap();
proxy_pool.register("agent-a", 19001);
let config_b = make_config("agent-b");
registry.create_agent(config_b).unwrap();
registry
.transition("agent-b", LifecycleState::Starting)
.unwrap();
registry
.transition("agent-b", LifecycleState::Running)
.unwrap();
proxy_pool.register("agent-b", 19002);
let proxy_a = proxy_pool.get("agent-a").unwrap();
assert_eq!(proxy_a.agent_id(), "agent-a");
assert_eq!(proxy_a.agent_url(), "http://127.0.0.1:19001");
let proxy_b = proxy_pool.get("agent-b").unwrap();
assert_eq!(proxy_b.agent_id(), "agent-b");
assert_eq!(proxy_b.agent_url(), "http://127.0.0.1:19002");
let ids = proxy_pool.agent_ids();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&"agent-a".to_string()));
assert!(ids.contains(&"agent-b".to_string()));
assert!(proxy_pool.get("agent-c").is_none());
}
#[test]
fn rbac_denies_user_agent_system_tools() {
let rbac = RbacBridge::new();
rbac.register_system_agent("system");
rbac.register_agent(
"research",
&AgentRoleConfig {
allow: vec!["web_search".to_string()],
deny: vec![],
},
);
for tool in SYSTEM_TOOLS {
assert!(
rbac.check_tool("system", tool).is_ok(),
"system agent should be allowed to call '{}'",
tool
);
}
for tool in SYSTEM_TOOLS {
assert!(
rbac.check_tool("research", tool).is_err(),
"user agent should be denied from calling '{}'",
tool
);
}
assert!(rbac.check_tool("research", "web_search").is_ok());
assert!(rbac.check_tool("research", "code_exec").is_err());
}
#[test]
fn rbac_strips_system_tools_from_user_agent() {
let rbac = RbacBridge::new();
let stripped = rbac.register_agent(
"sneaky-agent",
&AgentRoleConfig {
allow: vec![
"web_search".to_string(),
"agent_create".to_string(),
"agent_start".to_string(),
"agent_stop".to_string(),
"agent_delete".to_string(),
"agent_list".to_string(),
"agent_configure".to_string(),
],
deny: vec![],
},
);
assert_eq!(stripped.len(), 6);
for tool in SYSTEM_TOOLS {
assert!(
rbac.check_tool("sneaky-agent", tool).is_err(),
"stripped system tool '{}' should still be denied",
tool
);
}
assert!(rbac.check_tool("sneaky-agent", "web_search").is_ok());
}
#[test]
fn agent_crash_triggers_error_state() {
let tmp = TempDir::new().unwrap();
let registry = AgentRegistry::new(tmp.path().join("registry"));
registry.create_agent(make_config("crashy")).unwrap();
registry
.transition("crashy", LifecycleState::Starting)
.unwrap();
registry
.transition("crashy", LifecycleState::Running)
.unwrap();
registry
.transition(
"crashy",
LifecycleState::Error {
message: "health check failed 3 consecutive times".to_string(),
},
)
.unwrap();
let record = registry.get("crashy").unwrap();
match &record.state {
LifecycleState::Error { message } => {
assert!(message.contains("health check failed"));
}
other => panic!("expected Error state, got {:?}", other),
}
drop(record);
registry
.transition("crashy", LifecycleState::Starting)
.unwrap();
let record = registry.get("crashy").unwrap();
assert_eq!(record.state, LifecycleState::Starting);
drop(record);
registry
.transition("crashy", LifecycleState::Running)
.unwrap();
let record = registry.get("crashy").unwrap();
assert_eq!(record.state, LifecycleState::Running);
}
#[test]
fn invalid_state_transitions_rejected() {
let tmp = TempDir::new().unwrap();
let registry = AgentRegistry::new(tmp.path().join("registry"));
registry.create_agent(make_config("strict")).unwrap();
assert!(registry
.transition("strict", LifecycleState::Running)
.is_err());
assert!(registry
.transition("strict", LifecycleState::Stopped)
.is_err());
registry
.transition("strict", LifecycleState::Starting)
.unwrap();
assert!(registry
.transition("strict", LifecycleState::Stopped)
.is_err());
}
#[test]
fn gateway_restart_restores_persisted_agents() {
let tmp = TempDir::new().unwrap();
let persist_dir = tmp.path().join("registry");
{
let reg = AgentRegistry::new(persist_dir.clone());
reg.create_agent(make_config("alpha")).unwrap();
reg.create_agent(make_config("beta")).unwrap();
reg.create_agent(make_config("gamma")).unwrap();
reg.transition("alpha", LifecycleState::Starting).unwrap();
reg.transition("alpha", LifecycleState::Running).unwrap();
reg.transition("beta", LifecycleState::Starting).unwrap();
reg.transition(
"beta",
LifecycleState::Error {
message: "compile failed".to_string(),
},
)
.unwrap();
}
let reg2 = AgentRegistry::new(persist_dir);
let loaded = reg2.load_from_disk().unwrap();
assert_eq!(loaded, 3, "should load all 3 agents");
let alpha = reg2.get("alpha").unwrap();
assert_eq!(alpha.state, LifecycleState::Running);
assert_eq!(alpha.config.name, "Agent alpha");
drop(alpha);
let beta = reg2.get("beta").unwrap();
match &beta.state {
LifecycleState::Error { message } => {
assert!(message.contains("compile failed"));
}
other => panic!("expected Error state for beta, got {:?}", other),
}
drop(beta);
let gamma = reg2.get("gamma").unwrap();
assert_eq!(gamma.state, LifecycleState::Created);
drop(gamma);
reg2.transition("alpha", LifecycleState::Stopping).unwrap();
reg2.transition("alpha", LifecycleState::Stopped).unwrap();
let alpha = reg2.get("alpha").unwrap();
assert_eq!(alpha.state, LifecycleState::Stopped);
}
#[test]
fn gateway_restart_rebuilds_rbac_from_registry() {
let tmp = TempDir::new().unwrap();
let persist_dir = tmp.path().join("registry");
{
let reg = AgentRegistry::new(persist_dir.clone());
let mut sys_config = make_config("system");
sys_config.role.allow = vec!["*".to_string()];
reg.register_system_agent(sys_config).unwrap();
let mut user_config = make_config("research");
user_config.role.allow = vec!["web_search".to_string(), "code_exec".to_string()];
reg.create_agent(user_config).unwrap();
}
let reg2 = AgentRegistry::new(persist_dir);
let loaded = reg2.load_from_disk().unwrap();
assert_eq!(loaded, 2);
let sys_record = reg2.get("system").unwrap();
let _sys_config = sys_record.config.clone();
drop(sys_record);
let rbac = RbacBridge::new();
rbac.register_system_agent("system");
let research_record = reg2.get("research").unwrap();
rbac.register_agent("research", &research_record.config.role);
drop(research_record);
assert!(rbac.check_tool("system", "agent_create").is_ok());
assert!(rbac.check_tool("system", "web_search").is_ok());
assert!(rbac.check_tool("research", "web_search").is_ok());
assert!(rbac.check_tool("research", "code_exec").is_ok());
assert!(rbac.check_tool("research", "agent_create").is_err());
assert!(rbac.check_tool("research", "agent_delete").is_err());
}