use std::path::Path;
use apcore_toolkit::ScannedModule;
use apexe::a2a::A2aServerBuilder;
use apexe::adapter::CliToolConverter;
use apexe::models::ScannedCLITool;
use apexe::output::YamlOutput;
use serde_json::json;
use tempfile::TempDir;
fn write_echo_binding(dir: &Path) {
let module = ScannedModule::new(
"cli.echo".to_string(),
"Echo a message".to_string(),
json!({
"type": "object",
"properties": { "message": { "type": "string" } },
"additionalProperties": false
}),
json!({ "type": "object" }),
vec!["cli".to_string()],
"exec:///bin/echo".to_string(),
);
YamlOutput::without_verification()
.write(&[module], dir, false)
.expect("binding should be written");
}
#[tokio::test]
async fn test_a2a_agent_card_exposes_scanned_skill() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let card = A2aServerBuilder::new()
.name("apexe-test")
.modules_dir(dir.path())
.agent_card()
.await
.expect("agent card should build from bindings");
let blob = serde_json::to_string(&card).unwrap();
assert!(
blob.contains("skills"),
"agent card should carry a skills list: {blob}"
);
assert!(
blob.contains("echo"),
"scanned echo module should be exposed as a skill: {blob}"
);
}
#[tokio::test]
async fn test_a2a_agent_card_reports_the_apexe_version() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let card = A2aServerBuilder::new()
.name("apexe-test")
.modules_dir(dir.path())
.agent_card()
.await
.expect("agent card should build from bindings");
assert_eq!(
card.get("version").and_then(serde_json::Value::as_str),
Some(apexe::VERSION),
"agent card must advertise apexe's version: {card}"
);
assert_eq!(
card.get("name").and_then(serde_json::Value::as_str),
Some("apexe-test"),
"agent card must advertise the configured agent name: {card}"
);
}
#[tokio::test]
async fn test_a2a_agent_card_skill_id_is_the_module_id() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let card = A2aServerBuilder::new()
.modules_dir(dir.path())
.agent_card()
.await
.expect("agent card should build from bindings");
let skills = card
.get("skills")
.and_then(serde_json::Value::as_array)
.expect("card should carry a skills array");
assert_eq!(skills.len(), 1, "one binding -> one skill: {card}");
assert_eq!(
skills[0].get("id").and_then(serde_json::Value::as_str),
Some("cli.echo"),
"skill id must be the module_id verbatim: {card}"
);
}
#[tokio::test]
async fn test_a2a_empty_registry_errors() {
let dir = TempDir::new().unwrap();
let result = A2aServerBuilder::new()
.modules_dir(dir.path())
.agent_card()
.await;
assert!(
result.is_err(),
"an empty registry must error, not build an empty agent"
);
}
#[tokio::test]
async fn test_a2a_serve_fails_fast_on_enable_approval_without_store() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let result = A2aServerBuilder::new()
.modules_dir(dir.path())
.enable_approval(true)
.agent_card()
.await;
assert!(
result.is_err(),
"enable_approval without a store must fail fast"
);
}
fn write_true_binding(dir: &Path) {
let module = ScannedModule::new(
"sys.true".to_string(),
"Do nothing, successfully".to_string(),
json!({ "type": "object", "properties": {}, "additionalProperties": false }),
json!({ "type": "object" }),
vec!["sys".to_string()],
"exec:///usr/bin/true".to_string(),
);
YamlOutput::without_verification()
.write(&[module], dir, false)
.expect("binding should be written");
}
#[tokio::test]
async fn test_a2a_prefix_filter_excludes_a_module_from_the_card() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
write_true_binding(dir.path());
let card = A2aServerBuilder::new()
.modules_dir(dir.path())
.prefix("cli.")
.agent_card()
.await
.expect("agent card should build from bindings");
let blob = serde_json::to_string(&card).unwrap();
assert!(blob.contains("cli.echo"), "the admitted skill: {blob}");
assert!(
!blob.contains("sys.true"),
"an excluded module must not be advertised: {blob}"
);
}
#[tokio::test]
async fn test_a2a_tags_filter_excludes_a_module_from_the_card() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
write_true_binding(dir.path());
let card = A2aServerBuilder::new()
.modules_dir(dir.path())
.tags(vec!["sys".to_string()])
.agent_card()
.await
.expect("agent card should build from bindings");
let blob = serde_json::to_string(&card).unwrap();
assert!(blob.contains("sys.true"), "the admitted skill: {blob}");
assert!(
!blob.contains("cli.echo"),
"an excluded module must not be advertised: {blob}"
);
}
#[tokio::test]
async fn test_a2a_filter_that_admits_nothing_leaves_an_empty_registry() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let result = A2aServerBuilder::new()
.modules_dir(dir.path())
.prefix("nomatch.")
.agent_card()
.await;
assert!(
result.is_err(),
"a filter admitting nothing must leave no registered module"
);
}
#[tokio::test]
async fn test_every_skill_advertises_json_only_input() {
let dir = TempDir::new().unwrap();
write_echo_binding(dir.path());
let card = A2aServerBuilder::new()
.modules_dir(dir.path())
.agent_card()
.await
.expect("agent card should build from bindings");
let skills = card["skills"].as_array().expect("the card lists skills");
assert!(
!skills.is_empty(),
"the fixture registers one skill: {card}"
);
for skill in skills {
let modes: Vec<&str> = skill["inputModes"]
.as_array()
.expect("every skill declares inputModes")
.iter()
.filter_map(|m| m.as_str())
.collect();
assert_eq!(
modes,
["application/json"],
"an apexe skill takes a JSON object, so prose in a TextPart is not a \
mode it can honour: {skill}"
);
}
let default_modes: Vec<&str> = card["defaultInputModes"]
.as_array()
.expect("the card declares defaultInputModes")
.iter()
.filter_map(|m| m.as_str())
.collect();
assert!(
default_modes.contains(&"text/plain"),
"if upstream narrowed the agent default, §11's caveat should be deleted \
rather than left explaining a disagreement that no longer exists: {card}"
);
}
#[test]
fn test_a_scanned_tool_always_yields_an_object_rooted_schema() {
use apexe::models::{ScannedFlag, ValueType};
let tool = ScannedCLITool {
name: "probe".to_string(),
description: "A tool with exactly one string flag".to_string(),
binary_path: "/usr/bin/true".to_string(),
global_flags: vec![ScannedFlag {
long_name: Some("--only".to_string()),
description: "The single option.".to_string(),
value_type: ValueType::String,
..Default::default()
}],
..Default::default()
};
for module in CliToolConverter::new().convert(&tool) {
assert_eq!(
module.input_schema["type"], "object",
"a string-rooted schema would make the skill advertise text/plain, \
which apexe cannot honour: {}",
module.input_schema
);
}
}