use std::process::ExitCode;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use aa_cli::commands::topology::render::{
self, AgentLineage, AgentNode, AgentTree, LineageStep, TeamSummary, TopologyOverview, TopologyStats,
};
use aa_cli::output::OutputFormat;
fn make_context(api_url: &str) -> aa_cli::config::ResolvedContext {
aa_cli::config::ResolvedContext {
name: None,
api_url: api_url.to_string(),
api_key: None,
}
}
#[tokio::test]
async fn overview_json_and_yaml_succeed() {
let body = serde_json::json!({
"team_count": 1,
"root_agent_count": 1,
"total_agent_count": 2,
"teams": [{"team_id": "team-alpha", "agent_count": 2, "root_agent_count": 1}],
"standalone_root_agents": []
});
for fmt in [OutputFormat::Json, OutputFormat::Yaml] {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/topology/overview"))
.respond_with(ResponseTemplate::new(200).set_body_json(body.clone()))
.expect(1)
.mount(&server)
.await;
let uri = server.uri();
let code = std::thread::spawn(move || {
let args = aa_cli::commands::topology::overview::OverviewArgs {
status: None,
show_budget: false,
};
aa_cli::commands::topology::overview::run(args, &make_context(&uri), fmt)
})
.join()
.unwrap();
assert_eq!(code, ExitCode::SUCCESS);
}
}
#[tokio::test]
async fn tree_json_and_yaml_succeed() {
let root_id = "0102030405060708090a0b0c0d0e0f10";
let body = serde_json::json!({
"id": root_id,
"name": "root-agent",
"depth": 0,
"status": "active",
"team_id": "team-alpha",
"delegation_reason": null,
"spawned_by_tool": null,
"children": []
});
for fmt in [OutputFormat::Json, OutputFormat::Yaml] {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/api/v1/topology/tree/{root_id}")))
.respond_with(ResponseTemplate::new(200).set_body_json(body.clone()))
.expect(1)
.mount(&server)
.await;
let uri = server.uri();
let code = std::thread::spawn(move || {
let args = aa_cli::commands::topology::tree::TreeArgs {
agent_id: root_id.to_string(),
depth: None,
status: None,
show_budget: false,
};
aa_cli::commands::topology::tree::run(args, &make_context(&uri), fmt)
})
.join()
.unwrap();
assert_eq!(code, ExitCode::SUCCESS);
}
}
#[tokio::test]
async fn team_json_and_yaml_succeed() {
let body = serde_json::json!({
"team_id": "team-alpha",
"agent_count": 1,
"members": [
{"id": "aabb", "name": "agent-1", "depth": 0, "status": "active", "team_id": "team-alpha"}
]
});
for fmt in [OutputFormat::Json, OutputFormat::Yaml] {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/topology/team/team-alpha"))
.respond_with(ResponseTemplate::new(200).set_body_json(body.clone()))
.expect(1)
.mount(&server)
.await;
let uri = server.uri();
let code = std::thread::spawn(move || {
let args = aa_cli::commands::topology::team::TeamArgs {
team_id: "team-alpha".to_string(),
status: None,
show_budget: false,
};
aa_cli::commands::topology::team::run(args, &make_context(&uri), fmt)
})
.join()
.unwrap();
assert_eq!(code, ExitCode::SUCCESS);
}
}
#[tokio::test]
async fn lineage_json_and_yaml_succeed() {
let agent_id = "aabbccdd00112233aabbccdd00112233";
let body = serde_json::json!({
"agent_id": agent_id,
"ancestor_count": 1,
"ancestors": [
{"id": agent_id, "name": "root", "depth": 0, "delegation_reason": null, "team_id": null}
]
});
for fmt in [OutputFormat::Json, OutputFormat::Yaml] {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/api/v1/topology/lineage/{agent_id}")))
.respond_with(ResponseTemplate::new(200).set_body_json(body.clone()))
.expect(1)
.mount(&server)
.await;
let uri = server.uri();
let code = std::thread::spawn(move || {
let args = aa_cli::commands::topology::lineage::LineageArgs {
agent_id: agent_id.to_string(),
show_permissions: false,
};
aa_cli::commands::topology::lineage::run(args, &make_context(&uri), fmt)
})
.join()
.unwrap();
assert_eq!(code, ExitCode::SUCCESS);
}
}
#[tokio::test]
async fn stats_json_and_yaml_succeed() {
let body = serde_json::json!({
"total_agents": 3,
"root_agent_count": 1,
"max_depth": 2,
"active_count": 3,
"suspended_count": 0,
"deregistered_count": 0,
"team_count": 1,
"team_sizes": {"team-alpha": 3},
"depth_histogram": {"0": 1, "1": 2},
"team_size_histogram": {"3": 1},
"spawn_count_histogram": {"0": 1},
"orphan_count": 0,
"avg_children_per_parent": 1.0
});
for fmt in [OutputFormat::Json, OutputFormat::Yaml] {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/topology/stats"))
.respond_with(ResponseTemplate::new(200).set_body_json(body.clone()))
.expect(1)
.mount(&server)
.await;
let uri = server.uri();
let code = std::thread::spawn(move || {
let args = aa_cli::commands::topology::stats::StatsArgs {};
aa_cli::commands::topology::stats::run(args, &make_context(&uri), fmt)
})
.join()
.unwrap();
assert_eq!(code, ExitCode::SUCCESS);
}
}
fn node(id: &str, name: &str, status: &str, team: Option<&str>) -> AgentNode {
AgentNode {
id: id.to_string(),
name: name.to_string(),
depth: 0,
status: status.to_string(),
team_id: team.map(str::to_string),
governance_level: None,
}
}
#[test]
fn overview_table_renders_standalone_root_agents() {
let overview = TopologyOverview {
team_count: 1,
root_agent_count: 2,
total_agent_count: 3,
teams: vec![TeamSummary {
team_id: "team-alpha".to_string(),
agent_count: 2,
root_agent_count: 1,
}],
standalone_root_agents: vec![
node("dead", "loner-1", "active", None),
node("beef", "loner-2", "deregistered", Some("team-x")),
],
};
render::table::render_overview_table(&overview);
}
#[test]
fn lineage_table_handles_root_and_long_reason() {
let root = AgentLineage {
agent_id: "root0000000000000000000000000000".to_string(),
ancestor_count: 1,
ancestors: vec![LineageStep {
id: "root0000000000000000000000000000".to_string(),
name: "root".to_string(),
depth: 0,
delegation_reason: None,
team_id: None,
}],
};
render::table::render_lineage_table(&root);
let long_reason = "x".repeat(120);
let chain = AgentLineage {
agent_id: "child".to_string(),
ancestor_count: 2,
ancestors: vec![
LineageStep {
id: "root".to_string(),
name: "root".to_string(),
depth: 0,
delegation_reason: None,
team_id: Some("team-alpha".to_string()),
},
LineageStep {
id: "child".to_string(),
name: "child".to_string(),
depth: 1,
delegation_reason: Some(long_reason),
team_id: None,
},
],
};
render::table::render_lineage_table(&chain);
}
#[test]
fn lineage_table_handles_empty_ancestry() {
let empty = AgentLineage {
agent_id: "ghost".to_string(),
ancestor_count: 0,
ancestors: vec![],
};
render::table::render_lineage_table(&empty);
}
#[test]
fn stats_table_renders_histograms() {
use std::collections::{BTreeMap, HashMap};
let mut team_sizes = HashMap::new();
team_sizes.insert("team-alpha".to_string(), 5usize);
let mut depth_histogram = BTreeMap::new();
depth_histogram.insert("0".to_string(), 1u32);
depth_histogram.insert("1".to_string(), 4u32);
let mut team_size_histogram = BTreeMap::new();
team_size_histogram.insert("5".to_string(), 1u32);
let stats = TopologyStats {
total_agents: 5,
root_agent_count: 1,
max_depth: 1,
active_count: 4,
suspended_count: 1,
deregistered_count: 0,
team_count: 1,
team_sizes,
depth_histogram,
team_size_histogram,
spawn_count_histogram: BTreeMap::new(),
orphan_count: 0,
avg_children_per_parent: 4.0,
};
render::table::render_stats_table(&stats);
}
fn tree_node(name: &str, team: Option<&str>, children: Vec<AgentTree>) -> AgentTree {
AgentTree {
id: format!("id-{name}"),
name: name.to_string(),
depth: 0,
status: "active".to_string(),
team_id: team.map(str::to_string),
delegation_reason: None,
spawned_by_tool: None,
governance_level: None,
children,
}
}
#[test]
fn agent_tree_renders_nested_children() {
let tree = tree_node(
"root",
Some("team-alpha"),
vec![
tree_node("child-a", None, vec![tree_node("grandchild", None, vec![])]),
tree_node("child-b", Some("team-beta"), vec![]),
],
);
render::tree::render_agent_tree(&tree, "", true);
}
#[test]
fn lineage_chain_renders_with_and_without_reason() {
let lineage = AgentLineage {
agent_id: "child".to_string(),
ancestor_count: 2,
ancestors: vec![
LineageStep {
id: "root".to_string(),
name: "root".to_string(),
depth: 0,
delegation_reason: None,
team_id: None,
},
LineageStep {
id: "child".to_string(),
name: "child".to_string(),
depth: 1,
delegation_reason: Some("orchestrate".to_string()),
team_id: Some("team-alpha".to_string()),
},
],
};
render::tree::render_lineage_chain(&lineage);
}