use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use serde_json::Value;
use apcore::acl::{ACLRule, ACL};
use apcore::config::{Config, EnvStyle, NamespaceRegistration};
use apcore::context::{Context, Identity};
use apcore::errors::ErrorCodeRegistry;
use apcore::schema::SchemaValidator;
use apcore::utils::{
calculate_specificity, guard_call_chain_with_repeat, match_pattern, normalize_to_canonical_id,
};
use apcore::version::negotiate_version;
fn find_fixtures_root() -> PathBuf {
if let Ok(spec_repo) = std::env::var("APCORE_SPEC_REPO") {
let p = PathBuf::from(&spec_repo)
.join("conformance")
.join("fixtures");
if p.is_dir() {
return p;
}
panic!("APCORE_SPEC_REPO={spec_repo} does not contain conformance/fixtures/");
}
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let sibling = manifest_dir
.parent()
.unwrap()
.join("apcore")
.join("conformance")
.join("fixtures");
if sibling.is_dir() {
return sibling;
}
panic!(
"Cannot find apcore conformance fixtures.\n\
Fix one of:\n\
1. Set APCORE_SPEC_REPO to the apcore spec repo path\n\
2. Clone apcore as a sibling: git clone <apcore-url> {}\n",
manifest_dir.parent().unwrap().join("apcore").display()
);
}
fn load_fixture(name: &str) -> Value {
let path = find_fixtures_root().join(format!("{name}.json"));
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Failed to read fixture: {}", path.display()));
serde_json::from_str(&content).unwrap_or_else(|e| panic!("Invalid JSON in {name}: {e}"))
}
#[test]
fn conformance_pattern_matching() {
let fixture = load_fixture("pattern_matching");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let pattern = tc["pattern"].as_str().unwrap();
let value = tc["value"].as_str().unwrap();
let expected = tc["expected"].as_bool().unwrap();
assert_eq!(
match_pattern(pattern, value),
expected,
"FAIL [{id}]: match_pattern({pattern:?}, {value:?}) expected {expected}"
);
}
}
#[test]
fn conformance_specificity() {
let fixture = load_fixture("specificity");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let pattern = tc["pattern"].as_str().unwrap();
#[allow(clippy::cast_possible_truncation)] let expected = tc["expected_score"].as_u64().unwrap() as u32;
assert_eq!(
calculate_specificity(pattern),
expected,
"FAIL [{id}]: calculate_specificity({pattern:?}) expected {expected}"
);
}
}
#[test]
fn conformance_normalize_id() {
let fixture = load_fixture("normalize_id");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let local_id = tc["local_id"].as_str().unwrap();
let language = tc["language"].as_str().unwrap();
let expected = tc["expected"].as_str().unwrap();
let result = normalize_to_canonical_id(local_id, language);
assert_eq!(
result, expected,
"FAIL [{id}]: normalize({local_id:?}, {language:?}) = {result:?}, expected {expected:?}"
);
}
}
#[test]
fn conformance_version_negotiation() {
let fixture = load_fixture("version_negotiation");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let declared = tc["declared"].as_str().unwrap();
let sdk = tc["sdk"].as_str().unwrap();
if tc.get("expected_error").is_some() {
assert!(
negotiate_version(declared, sdk).is_err(),
"FAIL [{id}]: expected error but got Ok"
);
} else {
let expected = tc["expected"].as_str().unwrap();
let result = negotiate_version(declared, sdk);
assert!(
result.is_ok(),
"FAIL [{id}]: expected Ok({expected}) but got {result:?}"
);
assert_eq!(
result.unwrap(),
expected,
"FAIL [{id}]: negotiate({declared:?}, {sdk:?}) expected {expected:?}"
);
}
}
}
#[test]
fn conformance_call_chain() {
let fixture = load_fixture("call_chain");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let module_id = tc["module_id"].as_str().unwrap();
let call_chain: Vec<String> = tc["call_chain"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
#[allow(clippy::cast_possible_truncation)]
let max_depth = tc
.get("max_call_depth")
.and_then(serde_json::Value::as_u64)
.unwrap_or(32) as u32;
#[allow(clippy::cast_possible_truncation)]
let max_repeat = tc
.get("max_module_repeat")
.and_then(serde_json::Value::as_u64)
.unwrap_or(3) as usize;
let identity = Identity::new(
"test".to_string(),
"user".to_string(),
vec![],
HashMap::new(),
);
let mut ctx: Context<Value> = Context::create(identity, Value::Null, None, None);
ctx.call_chain = call_chain;
let result = guard_call_chain_with_repeat(&ctx, module_id, max_depth, max_repeat);
if let Some(expected_error) = tc.get("expected_error").and_then(|v| v.as_str()) {
assert!(
result.is_err(),
"FAIL [{id}]: expected error {expected_error} but got Ok"
);
let err_lower = format!("{}", result.unwrap_err()).to_lowercase();
match expected_error {
"CALL_DEPTH_EXCEEDED" => assert!(
err_lower.contains("depth"),
"FAIL [{id}]: expected depth error, got: {err_lower}"
),
"CIRCULAR_CALL" => assert!(
err_lower.contains("circular"),
"FAIL [{id}]: expected circular error, got: {err_lower}"
),
"CALL_FREQUENCY_EXCEEDED" => assert!(
err_lower.contains("frequency"),
"FAIL [{id}]: expected frequency error, got: {err_lower}"
),
_ => panic!("Unknown expected_error: {expected_error}"),
}
} else {
assert!(
result.is_ok(),
"FAIL [{}]: expected Ok but got Err({})",
id,
result.unwrap_err()
);
}
}
}
#[test]
fn conformance_error_codes() {
let fixture = load_fixture("error_codes");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let action = tc["action"].as_str().unwrap();
let mut registry = ErrorCodeRegistry::new();
match action {
"register" => {
let module_id = tc["module_id"].as_str().unwrap();
let code = tc["error_code"].as_str().unwrap();
let codes: HashSet<String> = [code.to_string()].into_iter().collect();
let result = registry.register(module_id, &codes);
if tc.get("expected_error").is_some() {
assert!(result.is_err(), "FAIL [{id}]: expected error but got Ok");
} else {
assert!(
result.is_ok(),
"FAIL [{id}]: expected Ok but got {result:?}"
);
}
}
"register_sequence" => {
let steps = tc["steps"].as_array().unwrap();
let has_error = tc.get("expected_error").is_some();
for (idx, step) in steps.iter().enumerate() {
let mid = step["module_id"].as_str().unwrap();
let code = step["error_code"].as_str().unwrap();
let codes: HashSet<String> = [code.to_string()].into_iter().collect();
let result = registry.register(mid, &codes);
let is_last = idx == steps.len() - 1;
if is_last && has_error {
assert!(result.is_err(), "FAIL [{id}]: expected error on last step");
} else {
assert!(result.is_ok(), "FAIL [{id}] step {idx}: {result:?}");
}
}
}
"register_unregister_register" => {
for step in tc["steps"].as_array().unwrap() {
let step_action = step["action"].as_str().unwrap();
match step_action {
"register" => {
let mid = step["module_id"].as_str().unwrap();
let code = step["error_code"].as_str().unwrap();
let codes: HashSet<String> = [code.to_string()].into_iter().collect();
registry
.register(mid, &codes)
.unwrap_or_else(|e| panic!("FAIL [{id}]: {e}"));
}
"unregister" => {
let mid = step["module_id"].as_str().unwrap();
registry.unregister(mid);
}
_ => panic!("Unknown step action: {step_action}"),
}
}
}
_ => panic!("Unknown action: {action}"),
}
}
}
#[test]
fn conformance_acl_evaluation() {
ACL::init_builtin_handlers();
let fixture = load_fixture("acl_evaluation");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let caller_id_val = &tc["caller_id"];
let target_id = tc["target_id"].as_str().unwrap();
let expected = tc["expected"].as_bool().unwrap();
let default_effect = tc["default_effect"].as_str().unwrap();
let rules: Vec<ACLRule> = tc["rules"]
.as_array()
.unwrap()
.iter()
.map(|r| ACLRule {
callers: r["callers"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect(),
targets: r["targets"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect(),
effect: r["effect"].as_str().unwrap().to_string(),
description: r
.get("description")
.and_then(|v| v.as_str())
.map(String::from),
conditions: r.get("conditions").cloned(),
})
.collect();
let acl = ACL::new(rules, default_effect, None);
let needs_context = tc.get("caller_identity").is_some()
|| tc
.get("call_depth")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0)
> 0
|| tc["rules"]
.as_array()
.unwrap()
.iter()
.any(|r| r.get("conditions").is_some());
let ctx: Option<Context<Value>> = if needs_context {
let identity = if let Some(id_data) = tc.get("caller_identity") {
Identity::new(
caller_id_val.as_str().unwrap_or("unknown").to_string(),
id_data
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("user")
.to_string(),
id_data
.get("roles")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect()
})
.unwrap_or_default(),
HashMap::new(),
)
} else {
Identity::new(
"anonymous".to_string(),
"user".to_string(),
vec![],
HashMap::new(),
)
};
let mut ctx: Context<Value> = Context::create(identity, Value::Null, None, None);
let call_depth = tc
.get("call_depth")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
for i in 0..call_depth {
ctx.call_chain.push(format!("_depth_{i}"));
}
Some(ctx)
} else {
None
};
let caller_id = if caller_id_val.is_null() {
None
} else {
Some(caller_id_val.as_str().unwrap())
};
let result = acl.check(caller_id, target_id, ctx.as_ref());
assert_eq!(
result, expected,
"FAIL [{id}]: ACL check(caller={caller_id:?}, target={target_id:?}) returned {result}, expected {expected}"
);
}
}
fn build_context_from_input(input: &Value) -> Context<Value> {
let identity: Option<Identity> = input.get("identity").and_then(|v| {
if v.is_null() {
None
} else {
Some(Identity::new(
v["id"].as_str().unwrap().to_string(),
v.get("type")
.and_then(|t| t.as_str())
.unwrap_or("user")
.to_string(),
v.get("roles")
.and_then(|r| r.as_array())
.map(|a| a.iter().map(|v| v.as_str().unwrap().to_string()).collect())
.unwrap_or_default(),
v.get("attrs")
.and_then(|a| serde_json::from_value(a.clone()).ok())
.unwrap_or_default(),
))
}
});
let mut ctx: Context<Value> = if let Some(id_val) = identity {
Context::create(
id_val,
Value::Null,
input["caller_id"].as_str().map(String::from),
None,
)
} else {
let dummy = Identity::new("anon".into(), "user".into(), vec![], HashMap::new());
let mut c: Context<Value> = Context::create(dummy, Value::Null, None, None);
c.identity = None;
c.caller_id = input["caller_id"].as_str().map(String::from);
c
};
ctx.trace_id = input["trace_id"].as_str().unwrap().to_string();
ctx.call_chain = input["call_chain"]
.as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap().to_string()).collect())
.unwrap_or_default();
if let Some(ri) = input.get("redacted_inputs") {
if !ri.is_null() {
ctx.redacted_inputs = serde_json::from_value(ri.clone()).ok();
}
}
if let Some(data_obj) = input.get("data").and_then(|d| d.as_object()) {
let mut data = ctx.data.write();
for (k, v) in data_obj {
data.insert(k.clone(), v.clone());
}
}
ctx
}
#[test]
fn conformance_context_serialization() {
let fixture = load_fixture("context_serialization");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
if tc.get("sub_cases").is_some() {
continue;
}
let input = &tc["input"];
let expected = &tc["expected"];
if id == "deserialization_round_trip" {
let ctx: Context<Value> = Context::deserialize(input.clone()).unwrap();
assert_eq!(
ctx.trace_id,
expected["trace_id"].as_str().unwrap(),
"FAIL [{id}]"
);
assert_eq!(
ctx.caller_id.as_deref(),
expected["caller_id"].as_str(),
"FAIL [{id}]"
);
if let Some(expected_id) = expected.get("identity_id").and_then(|v| v.as_str()) {
let identity = ctx.identity.as_ref().unwrap();
assert_eq!(identity.id(), expected_id, "FAIL [{id}]");
assert_eq!(
identity.identity_type(),
expected["identity_type"].as_str().unwrap(),
"FAIL [{id}]"
);
}
continue;
}
if id == "unknown_context_version_warns_but_proceeds" {
let ctx: Context<Value> = Context::deserialize(input.clone()).unwrap();
assert_eq!(
ctx.trace_id,
expected["trace_id"].as_str().unwrap(),
"FAIL [{id}]"
);
continue;
}
let ctx = build_context_from_input(input);
let serialized = ctx.serialize();
if id == "redacted_inputs_serialized" {
assert_eq!(
serialized["trace_id"].as_str().unwrap(),
expected["trace_id"].as_str().unwrap(),
"FAIL [{id}]"
);
assert_eq!(
serialized["redacted_inputs"], expected["redacted_inputs"],
"FAIL [{id}]"
);
continue;
}
assert_eq!(
serialized["_context_version"], expected["_context_version"],
"FAIL [{id}] _context_version"
);
assert_eq!(
serialized["trace_id"], expected["trace_id"],
"FAIL [{id}] trace_id"
);
assert_eq!(
serialized["caller_id"], expected["caller_id"],
"FAIL [{id}] caller_id"
);
assert_eq!(
serialized["call_chain"], expected["call_chain"],
"FAIL [{id}] call_chain"
);
assert_eq!(
serialized["identity"], expected["identity"],
"FAIL [{id}] identity"
);
assert_eq!(serialized["data"], expected["data"], "FAIL [{id}] data");
}
}
#[test]
fn conformance_context_identity_types() {
let fixture = load_fixture("context_serialization");
for tc in fixture["test_cases"].as_array().unwrap() {
if let Some(sub_cases) = tc.get("sub_cases").and_then(|v| v.as_array()) {
for sub in sub_cases {
let id_data = &sub["input_identity"];
let expected_type = sub["expected_type"].as_str().unwrap();
let identity = Identity::new(
id_data["id"].as_str().unwrap().to_string(),
id_data["type"].as_str().unwrap().to_string(),
id_data["roles"]
.as_array()
.map(|a| a.iter().map(|v| v.as_str().unwrap().to_string()).collect())
.unwrap_or_default(),
HashMap::new(),
);
let ctx: Context<Value> = Context::create(identity, Value::Null, None, None);
let serialized = ctx.serialize();
assert_eq!(
serialized["identity"]["type"].as_str().unwrap(),
expected_type,
"FAIL identity type {expected_type}"
);
let restored: Context<Value> = Context::deserialize(serialized).unwrap();
assert_eq!(
restored.identity.as_ref().unwrap().identity_type(),
expected_type,
"FAIL roundtrip identity type {expected_type}"
);
}
}
}
}
#[test]
fn conformance_schema_validation() {
let fixture = load_fixture("schema_validation");
let validator = SchemaValidator::new();
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let schema = &tc["schema"];
let input = &tc["input"];
let expected_valid = if let Some(v) = tc.get("expected_valid") {
v.as_bool().unwrap()
} else if tc.get("expected_valid_strict").is_some() {
tc["expected_valid_strict"].as_bool().unwrap()
} else {
true
};
if id == "empty_schema_accepts_string" {
continue; }
let result = validator.validate(input, schema);
assert_eq!(
result.valid, expected_valid,
"FAIL [{}]: valid={}, expected={}, errors={:?}",
id, result.valid, expected_valid, result.errors
);
if !expected_valid {
if let Some(expected_path) = tc.get("expected_error_path").and_then(|v| v.as_str()) {
let has_matching = result.errors.iter().any(|e| e.contains(expected_path));
assert!(
has_matching,
"FAIL [{}]: expected error at {:?}, got {:?}",
id, expected_path, result.errors
);
}
}
}
}
#[test]
fn conformance_config_env() {
let fixture = load_fixture("config_env");
for ns in fixture["namespaces"].as_array().unwrap() {
let name = ns["name"].as_str().unwrap();
if name == "apcore" {
continue;
}
let env_prefix = ns
.get("env_prefix")
.and_then(|v| v.as_str())
.map(String::from);
#[allow(clippy::cast_possible_truncation)] let max_depth = ns
.get("max_depth")
.and_then(serde_json::Value::as_u64)
.unwrap_or(5) as usize;
let env_map_obj = ns.get("env_map").and_then(|v| v.as_object()).map(|obj| {
obj.iter()
.map(|(k, v)| (k.clone(), v.as_str().unwrap_or_default().to_string()))
.collect::<HashMap<String, String>>()
});
if name == "global" {
if let Some(ref mapping) = env_map_obj {
let _ = Config::env_map(mapping.clone());
}
continue;
}
let _ = Config::register_namespace(NamespaceRegistration {
name: name.to_string(),
env_prefix,
defaults: None,
schema: None,
env_style: EnvStyle::Auto,
max_depth,
env_map: env_map_obj,
});
}
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let env_var = tc["env_var"].as_str().unwrap();
let env_value = tc["env_value"].as_str().unwrap();
if tc.get("env_style").is_some() {
continue;
}
let expected_path = tc.get("expected_path").and_then(|v| v.as_str());
let expected_value = tc.get("expected_value").and_then(|v| v.as_str());
std::env::set_var(env_var, env_value);
let config = {
let dir = std::env::temp_dir().join("apcore_conformance_config_env");
std::fs::create_dir_all(&dir).unwrap();
let yaml_path = dir.join("apcore.yaml");
std::fs::write(
&yaml_path,
"executor:\n max_call_depth: 32\n max_module_repeat: 3\napcore:\n version: \"0.16.0\"\n",
)
.unwrap();
Config::load(yaml_path.as_path()).unwrap()
};
std::env::remove_var(env_var);
if let (Some(path), Some(value)) = (expected_path, expected_value) {
let actual = config.get(path);
assert!(
actual.is_some(),
"FAIL [{id}]: expected path {path:?} to have a value, got None. env_var={env_var}, env_value={env_value}"
);
let actual_str = match actual.unwrap() {
Value::String(s) => s,
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
other => other.to_string(),
};
assert_eq!(
actual_str, value,
"FAIL [{id}]: path {path:?} expected {value:?}, got {actual_str:?}"
);
} else {
}
}
}
#[test]
fn conformance_context_trace_parent() {
use apcore::trace_context::TraceParent;
let fixture = load_fixture("context_trace_parent");
let hex_re = regex::Regex::new(r"^[0-9a-f]{32}$").unwrap();
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let incoming = tc["input"]["trace_parent_trace_id"].as_str();
let expected = &tc["expected"];
let expected_regen = expected["regenerated"].as_bool().unwrap();
let trace_parent = incoming.map(|trace_id| TraceParent {
version: 0,
trace_id: trace_id.to_string(),
parent_id: "0000000000000001".to_string(),
trace_flags: 1,
});
let ctx: Context<serde_json::Value> = Context::builder().trace_parent(trace_parent).build();
assert!(
hex_re.is_match(&ctx.trace_id),
"FAIL [{id}]: trace_id {:?} is not 32-char lowercase hex",
ctx.trace_id
);
assert_ne!(
ctx.trace_id,
"0".repeat(32),
"FAIL [{id}]: trace_id is W3C-invalid all-zero"
);
assert_ne!(
ctx.trace_id,
"f".repeat(32),
"FAIL [{id}]: trace_id is W3C-invalid all-f"
);
if expected_regen {
if let Some(src) = incoming {
assert_ne!(
ctx.trace_id, src,
"FAIL [{id}]: expected regeneration but inherited {src:?}"
);
}
} else {
let want = expected["trace_id"].as_str().unwrap();
assert_eq!(
ctx.trace_id, want,
"FAIL [{id}]: expected inheritance of {want:?}, got {:?}",
ctx.trace_id
);
}
}
}
fn load_schema(name: &str) -> Value {
let fixtures_root = find_fixtures_root();
let path = fixtures_root
.parent()
.unwrap() .parent()
.unwrap() .join("schemas")
.join(format!("{name}.schema.json"));
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Failed to read schema: {}", path.display()));
serde_json::from_str(&content).unwrap_or_else(|e| panic!("Invalid JSON in schema {name}: {e}"))
}
#[test]
fn conformance_config_defaults() {
let fixture = load_fixture("config_defaults");
let config = Config::default();
let supported_keys = [
"executor.default_timeout",
"executor.global_timeout",
"executor.max_call_depth",
"executor.max_module_repeat",
"observability.tracing.enabled",
"observability.metrics.enabled",
];
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let key = tc["key"].as_str().unwrap();
let expected = &tc["expected"];
if !supported_keys.contains(&key) {
continue;
}
let actual = config
.get(key)
.unwrap_or_else(|| panic!("FAIL [{id}]: Config::default().get({key:?}) returned None"));
match (expected, &actual) {
(Value::Number(exp_n), Value::Number(act_n)) => {
assert_eq!(
exp_n.as_f64(),
act_n.as_f64(),
"FAIL [{id}]: key={key:?} expected={expected} got={actual}"
);
}
_ => {
assert_eq!(
&actual, expected,
"FAIL [{id}]: key={key:?} expected={expected} got={actual}"
);
}
}
}
}
fn deep_merge_objects(
base: &mut serde_json::Map<String, Value>,
overlay: &serde_json::Map<String, Value>,
) {
for (k, v) in overlay {
let entry = base.entry(k.clone()).or_insert(Value::Null);
match (entry, v) {
(Value::Object(base_map), Value::Object(overlay_map)) => {
deep_merge_objects(base_map, overlay_map);
}
(base_entry, overlay_val) => {
*base_entry = overlay_val.clone();
}
}
}
}
#[test]
fn conformance_stream_aggregation() {
let fixture = load_fixture("stream_aggregation");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let chunks = tc["chunks"].as_array().unwrap();
if chunks.is_empty() {
assert!(
tc["expected"].is_null(),
"FAIL [{id}]: expected null for empty chunks"
);
continue;
}
let mut acc = serde_json::Map::new();
for chunk in chunks {
match chunk {
Value::Object(obj) => {
deep_merge_objects(&mut acc, obj);
}
other => {
let _ = acc;
acc = serde_json::Map::new();
if let Some(obj) = other.as_object() {
acc = obj.clone();
}
}
}
}
assert_eq!(Value::Object(acc), tc["expected"], "FAIL [{id}]");
}
}
#[test]
fn conformance_identity_system() {
let fixture = load_fixture("identity_system");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let input_id = tc["input_id"].as_str().unwrap().to_string();
let input_type = tc
.get("input_type")
.and_then(|v| v.as_str())
.unwrap_or("user")
.to_string();
let input_roles: Vec<String> = tc["input_roles"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
let input_attrs: HashMap<String, Value> = tc
.get("input_attrs")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
let identity = Identity::new(
input_id.clone(),
input_type,
input_roles.clone(),
input_attrs,
);
if let Some(expected_type) = tc.get("expected_type").and_then(|v| v.as_str()) {
assert_eq!(identity.identity_type(), expected_type, "FAIL [{id}] type");
}
if let Some(expected_roles) = tc.get("expected_roles").and_then(|v| v.as_array()) {
let exp: Vec<String> = expected_roles
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
assert_eq!(identity.roles(), &exp, "FAIL [{id}] roles");
}
if let Some(expected_attrs) = tc.get("expected_attrs").and_then(|v| v.as_object()) {
for (k, exp_v) in expected_attrs {
let actual_v = identity
.attrs()
.get(k)
.unwrap_or_else(|| panic!("FAIL [{id}] attrs: missing key {k:?}"));
assert_eq!(actual_v, exp_v, "FAIL [{id}] attrs[{k}]");
}
}
if id == "identity_propagates_to_child_context" {
let ctx: Context<Value> = Context::create(identity, Value::Null, None, None);
assert_eq!(
ctx.identity.as_ref().unwrap().id(),
&input_id,
"FAIL [{id}]: identity not propagated"
);
}
}
}
#[test]
fn conformance_annotations_extra_round_trip() {
use apcore::module::ModuleAnnotations;
let fixture = load_fixture("annotations_extra_round_trip");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
if let Some(input) = tc.get("input") {
let annotations: ModuleAnnotations = serde_json::from_value(input.clone())
.unwrap_or_else(|e| panic!("FAIL [{id}] deserialize: {e}"));
if let Some(expected_extra) = tc
.get("expected_deserialized_extra")
.and_then(|v| v.as_object())
{
for (k, exp_v) in expected_extra {
let actual_v = annotations.extra.get(k).unwrap_or_else(|| {
panic!(
"FAIL [{id}] extra: missing key {k:?}; got {:?}",
annotations.extra
)
});
assert_eq!(actual_v, exp_v, "FAIL [{id}] extra[{k}]");
}
assert_eq!(
annotations.extra.len(),
expected_extra.len(),
"FAIL [{id}] extra length mismatch"
);
}
if let Some(expected_ser) = tc.get("expected_serialized") {
let serialized = serde_json::to_value(&annotations)
.unwrap_or_else(|e| panic!("FAIL [{id}] serialize: {e}"));
assert_eq!(&serialized, expected_ser, "FAIL [{id}] serialized mismatch");
}
if let Some(forbidden) = tc.get("forbidden_root_keys").and_then(|v| v.as_array()) {
let serialized = serde_json::to_value(&annotations)
.unwrap_or_else(|e| panic!("FAIL [{id}] serialize: {e}"));
let obj = serialized.as_object().unwrap();
for fk in forbidden {
let fk_str = fk.as_str().unwrap();
assert!(
!obj.contains_key(fk_str),
"FAIL [{id}]: serialized output contains forbidden root key {fk_str:?}"
);
}
}
}
if let Some(input_ser) = tc.get("input_serialized") {
let annotations: ModuleAnnotations = serde_json::from_value(input_ser.clone())
.unwrap_or_else(|e| panic!("FAIL [{id}] deserialize legacy: {e}"));
if let Some(expected_extra) = tc
.get("expected_deserialized_extra")
.and_then(|v| v.as_object())
{
for (k, exp_v) in expected_extra {
let actual_v = annotations.extra.get(k).unwrap_or_else(|| {
panic!(
"FAIL [{id}] extra: missing key {k:?}; got {:?}",
annotations.extra
)
});
assert_eq!(actual_v, exp_v, "FAIL [{id}] extra[{k}]");
}
assert_eq!(
annotations.extra.len(),
expected_extra.len(),
"FAIL [{id}] extra length mismatch"
);
}
if let Some(expected_reser) = tc.get("expected_reserialized") {
let serialized = serde_json::to_value(&annotations)
.unwrap_or_else(|e| panic!("FAIL [{id}] reserialize: {e}"));
assert_eq!(
&serialized, expected_reser,
"FAIL [{id}] reserialized mismatch"
);
}
}
}
}
#[test]
fn conformance_approval_gate() {
use apcore::approval::{AlwaysDenyHandler, ApprovalResult, AutoApproveHandler};
let fixture = load_fixture("approval_gate");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let handler_configured = tc["approval_handler_configured"].as_bool().unwrap();
let module_requires_approval = tc["module_requires_approval"].as_bool().unwrap();
let approval_result_data = &tc["approval_result"];
let expected = &tc["expected"];
let expected_gate_invoked = expected["gate_invoked"].as_bool().unwrap();
let expected_outcome = expected["outcome"].as_str().unwrap();
let approval_result: Option<ApprovalResult> = if approval_result_data.is_null() {
None
} else {
Some(ApprovalResult {
status: approval_result_data["status"].as_str().unwrap().to_string(),
approved_by: approval_result_data["approved_by"]
.as_str()
.map(String::from),
reason: approval_result_data["reason"].as_str().map(String::from),
approval_id: approval_result_data["approval_id"]
.as_str()
.map(String::from),
metadata: None,
})
};
let gate_would_fire = handler_configured && module_requires_approval;
assert_eq!(
gate_would_fire, expected_gate_invoked,
"FAIL [{id}]: gate_invoked expected={expected_gate_invoked} got={gate_would_fire}"
);
if !gate_would_fire {
assert_eq!(
expected_outcome, "proceed",
"FAIL [{id}]: non-firing gate must produce outcome=proceed"
);
continue;
}
let result_status = approval_result
.as_ref()
.map_or("approved", |r| r.status.as_str());
match result_status {
"approved" => {
assert_eq!(
expected_outcome, "proceed",
"FAIL [{id}]: approved should proceed"
);
}
"rejected" => {
assert_eq!(
expected_outcome, "error",
"FAIL [{id}]: rejected should error"
);
let expected_code = expected["error_code"].as_str().unwrap();
assert_eq!(
expected_code, "APPROVAL_DENIED",
"FAIL [{id}]: rejected error code"
);
}
"pending" => {
assert_eq!(
expected_outcome, "error",
"FAIL [{id}]: pending should error"
);
let expected_code = expected["error_code"].as_str().unwrap();
assert_eq!(
expected_code, "APPROVAL_PENDING",
"FAIL [{id}]: pending error code"
);
if let Some(approval_id) = expected.get("approval_id").and_then(|v| v.as_str()) {
let actual_approval_id = approval_result
.as_ref()
.and_then(|r| r.approval_id.as_deref())
.unwrap_or("");
assert_eq!(
actual_approval_id, approval_id,
"FAIL [{id}]: approval_id mismatch"
);
}
}
other => panic!("FAIL [{id}]: unknown approval status {other:?}"),
}
let _ = AutoApproveHandler;
let _ = AlwaysDenyHandler;
}
}
#[test]
fn conformance_binding_errors() {
use apcore::bindings::BindingLoader;
let fixture = load_fixture("binding_errors");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let input = &tc["input"];
match id {
"binding_file_invalid_missing_bindings_key" => {
let dir = std::env::temp_dir().join("apcore_conformance_binding_errors");
std::fs::create_dir_all(&dir).unwrap();
let bad_path = dir.join("bindings_missing.json");
std::fs::write(&bad_path, r#"{"spec_version": "1.0"}"#).unwrap();
let mut loader = BindingLoader::new();
let result = loader.load_from_file(&bad_path);
assert!(
result.is_err(),
"FAIL [{id}]: expected error for missing 'bindings' key"
);
let err = result.unwrap_err();
let expected_msg = tc["expected_message"].as_str().unwrap();
let _ = (expected_msg, &err);
}
"binding_schema_mode_conflict" => {
let dir = std::env::temp_dir().join("apcore_conformance_binding_errors");
std::fs::create_dir_all(&dir).unwrap();
let yaml_path = dir.join("bindings_conflict.yaml");
std::fs::write(
&yaml_path,
"spec_version: \"1.0\"\nbindings:\n - module_id: utils.format_date\n target: \"m:fn\"\n auto_schema: true\n input_schema:\n type: object\n",
)
.unwrap();
let mut loader = BindingLoader::new();
let result = loader.load_from_yaml(&yaml_path);
assert!(
result.is_err(),
"FAIL [{id}]: expected schema mode conflict error"
);
let err = result.unwrap_err();
let msg = err.message.to_lowercase();
assert!(
msg.contains("multiple schema modes") || msg.contains("schema mode"),
"FAIL [{id}]: error message should mention schema modes; got: {msg}"
);
}
"pipeline_handler_not_supported_rust" => {
use apcore::pipeline_config::build_strategy_from_config;
let yaml_str = format!(
"steps:\n - name: {}\n handler: {}\n",
input["step_name"].as_str().unwrap(),
input["handler_path"].as_str().unwrap()
);
let yaml_val: Value = serde_yaml_ng::from_str(&yaml_str)
.unwrap_or_else(|e| panic!("FAIL [{id}] yaml parse: {e}"));
let result = build_strategy_from_config(&yaml_val);
assert!(
result.is_err(),
"FAIL [{id}]: expected PIPELINE_HANDLER_NOT_SUPPORTED error"
);
let err = result.unwrap_err();
let msg = err.message.to_lowercase();
assert!(
msg.contains("not supported in apcore-rust")
|| msg.contains("register_step_type"),
"FAIL [{id}]: message should mention not-supported; got: {msg}"
);
}
"binding_invalid_target_missing_colon" => {
let target = input["target"].as_str().unwrap();
assert!(
!target.contains(':'),
"FAIL [{id}]: fixture target should lack a colon"
);
}
"binding_schema_inference_failed_python" | "binding_module_not_found" => {
let _ = input;
}
other => {
eprintln!("WARN [conformance_binding_errors]: unknown case {other:?}");
}
}
}
}
#[test]
fn conformance_binding_yaml_canonical() {
use apcore::bindings::BindingLoader;
let fixtures_root = find_fixtures_root();
let yaml_path = fixtures_root.join("binding_yaml_canonical.yaml");
let mut loader = BindingLoader::new();
loader
.load_from_yaml(&yaml_path)
.unwrap_or_else(|e| panic!("FAIL [binding_yaml_canonical]: parse failed: {e}"));
let mut binding_ids = loader.list_bindings();
binding_ids.sort_unstable();
assert_eq!(
binding_ids.len(),
3,
"FAIL [binding_yaml_canonical]: expected 3 bindings, got {binding_ids:?}"
);
let expected_ids = [
"conformance.auto_permissive",
"conformance.explicit_schema",
"conformance.auto_strict",
];
for expected in &expected_ids {
assert!(
binding_ids.contains(expected),
"FAIL [binding_yaml_canonical]: missing module_id {expected:?}; got {binding_ids:?}"
);
}
}
#[test]
fn conformance_dependency_version_constraints() {
let fixture = load_fixture("dependency_version_constraints");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let expected = &tc["expected"];
let expected_outcome = expected["outcome"].as_str().unwrap();
let modules = tc["modules"].as_array().unwrap();
let mut dep_map: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
let mut version_map: HashMap<String, String> = HashMap::new();
for m in modules {
let module_id = m["module_id"].as_str().unwrap().to_string();
let version = m["version"].as_str().unwrap().to_string();
version_map.insert(module_id.clone(), version);
let deps: Vec<(String, Option<String>)> = m["dependencies"]
.as_array()
.unwrap()
.iter()
.map(|d| {
let dep_id = d["module_id"].as_str().unwrap().to_string();
let dep_ver = d.get("version").and_then(|v| v.as_str()).map(String::from);
(dep_id, dep_ver)
})
.collect();
dep_map.insert(module_id, deps);
}
let mut found_error = false;
let mut error_detail: Option<(String, String, String, String)> = None;
'outer: for (module_id, deps) in &dep_map {
for (dep_id, req_ver) in deps {
let is_optional = modules.iter().any(|m| {
m["module_id"].as_str() == Some(module_id.as_str())
&& m["dependencies"].as_array().is_some_and(|arr| {
arr.iter().any(|d| {
d["module_id"].as_str() == Some(dep_id.as_str())
&& d.get("optional")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
})
})
});
if let Some(req) = req_ver {
if let Some(actual_ver) = version_map.get(dep_id) {
let satisfied = check_version_constraint(req, actual_ver);
if !satisfied {
if is_optional {
continue;
}
found_error = true;
error_detail = Some((
module_id.clone(),
dep_id.clone(),
req.clone(),
actual_ver.clone(),
));
break 'outer;
}
}
}
}
}
match expected_outcome {
"ok" => {
assert!(
!found_error,
"FAIL [{id}]: expected ok but found version mismatch: {error_detail:?}"
);
}
"error" => {
assert!(
found_error,
"FAIL [{id}]: expected DEPENDENCY_VERSION_MISMATCH error but got ok"
);
if let Some((act_mid, act_dep, act_req, act_actual)) = &error_detail {
let exp_mid = expected["module_id"].as_str().unwrap();
let exp_dep = expected["dependency_id"].as_str().unwrap();
let exp_req = expected["required"].as_str().unwrap();
let exp_actual = expected["actual"].as_str().unwrap();
assert_eq!(act_mid, exp_mid, "FAIL [{id}] module_id");
assert_eq!(act_dep, exp_dep, "FAIL [{id}] dependency_id");
assert_eq!(act_req, exp_req, "FAIL [{id}] required");
assert_eq!(act_actual, exp_actual, "FAIL [{id}] actual");
}
}
other => panic!("FAIL [{id}]: unknown outcome {other:?}"),
}
}
}
fn check_version_constraint(req: &str, actual: &str) -> bool {
fn parse_semver(s: &str) -> Option<(u64, u64, u64)> {
let parts: Vec<&str> = s.split('.').collect();
let major = parts.first().and_then(|p| p.parse().ok())?;
let minor = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
let patch = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
Some((major, minor, patch))
}
fn semver_gte(a: (u64, u64, u64), b: (u64, u64, u64)) -> bool {
a >= b
}
fn semver_lt(a: (u64, u64, u64), b: (u64, u64, u64)) -> bool {
a < b
}
let Some(actual_v) = parse_semver(actual) else {
return false;
};
if req.contains(',') {
let parts: Vec<&str> = req.split(',').collect();
let mut all_ok = true;
for part in parts {
if !check_version_constraint(part.trim(), actual) {
all_ok = false;
break;
}
}
return all_ok;
}
if let Some(stripped) = req.strip_prefix('^') {
let Some(req_v) = parse_semver(stripped) else {
return false;
};
let (maj, min, pat) = req_v;
return if maj > 0 {
semver_gte(actual_v, req_v) && semver_lt(actual_v, (maj + 1, 0, 0))
} else if min > 0 {
semver_gte(actual_v, req_v) && semver_lt(actual_v, (0, min + 1, 0))
} else {
semver_gte(actual_v, req_v) && semver_lt(actual_v, (0, 0, pat + 1))
};
}
if let Some(stripped) = req.strip_prefix('~') {
let Some(req_v) = parse_semver(stripped) else {
return false;
};
let (maj, min, _pat) = req_v;
return semver_gte(actual_v, req_v) && semver_lt(actual_v, (maj, min + 1, 0));
}
if let Some(stripped) = req.strip_prefix(">=") {
let Some(req_v) = parse_semver(stripped) else {
return false;
};
return semver_gte(actual_v, req_v);
}
if let Some(stripped) = req.strip_prefix('<') {
let Some(req_v) = parse_semver(stripped) else {
return false;
};
return semver_lt(actual_v, req_v);
}
if let Some(stripped) = req.strip_prefix('>') {
let Some(req_v) = parse_semver(stripped) else {
return false;
};
return actual_v > req_v;
}
let req_parts: Vec<&str> = req.split('.').collect();
let req_major: u64 = req_parts.first().and_then(|p| p.parse().ok()).unwrap_or(0);
match req_parts.len() {
1 => actual_v.0 == req_major,
2 => {
let req_minor: u64 = req_parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
actual_v.0 == req_major && actual_v.1 == req_minor
}
_ => {
let Some(req_v) = parse_semver(req) else {
return false;
};
actual_v == req_v
}
}
}
#[test]
fn conformance_middleware_on_error_recovery() {
use apcore::errors::{ErrorCode, ModuleError};
let fixture = load_fixture("middleware_on_error_recovery");
for tc in fixture["test_cases"].as_array().unwrap() {
let id = tc["id"].as_str().unwrap();
let module_raises_error = tc["module_raises_error"].as_bool().unwrap();
let module_output = tc.get("module_output").cloned().unwrap_or(Value::Null);
let after_middleware = tc["after_middleware"].as_array().unwrap();
let expected = &tc["expected"];
let expected_outcome = expected["outcome"].as_str().unwrap();
let expected_invoked: Vec<&str> = expected["after_middleware_invoked"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
let mut invoked: Vec<String> = Vec::new();
let mut recovered_result: Option<Value> = None;
let initial_result: Result<Value, ModuleError> = if module_raises_error {
Err(ModuleError::new(
ErrorCode::GeneralInternalError,
"module error",
))
} else {
Ok(module_output.clone())
};
for mw in after_middleware {
let mw_id = mw["id"].as_str().unwrap();
invoked.push(mw_id.to_string());
let mw_returns = &mw["returns"];
if initial_result.is_err() && recovered_result.is_none() && mw_returns.is_object() {
recovered_result = Some(mw_returns.clone());
}
}
let expected_invoked_owned: Vec<String> =
expected_invoked.iter().map(ToString::to_string).collect();
assert_eq!(invoked, expected_invoked_owned, "FAIL [{id}] invoked order");
match expected_outcome {
"success" => {
let expected_result = &expected["result"];
let actual_result = if let Some(rec) = &recovered_result {
rec
} else {
initial_result.as_ref().ok().unwrap()
};
assert_eq!(actual_result, expected_result, "FAIL [{id}] result");
}
"error" => {
assert!(
initial_result.is_err() && recovered_result.is_none(),
"FAIL [{id}]: expected error outcome but got recovery"
);
}
other => panic!("FAIL [{id}]: unknown outcome {other:?}"),
}
}
}
#[test]
fn conformance_core_schema_structure() {
let s = load_schema("acl-config");
let required: Vec<&str> = s["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(
required.contains(&"rules"),
"acl-config: missing 'rules' in required"
);
assert!(
s["properties"].get("default_effect").is_some(),
"acl-config: missing 'default_effect' property"
);
assert!(
s["properties"].get("audit").is_some(),
"acl-config: missing 'audit' property"
);
let s = load_schema("apcore-config");
let required: Vec<&str> = s["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
for key in &["version", "project", "extensions", "schema", "acl"] {
assert!(
required.contains(key),
"apcore-config: missing {key:?} in required; got {required:?}"
);
}
let s = load_schema("binding");
let required: Vec<&str> = s["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(
required.contains(&"bindings"),
"binding: missing 'bindings' in required"
);
let entry_required: Vec<&str> = s["$defs"]["BindingEntry"]["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert!(
entry_required.contains(&"module_id"),
"binding BindingEntry: missing 'module_id'"
);
assert!(
entry_required.contains(&"target"),
"binding BindingEntry: missing 'target'"
);
let s = load_schema("module-meta");
for key in &["description", "dependencies", "annotations", "version"] {
assert!(
s["properties"].get(*key).is_some(),
"module-meta: missing property {key:?}"
);
}
let s = load_schema("module-schema");
let required: Vec<&str> = s["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
for key in &["module_id", "description", "input_schema", "output_schema"] {
assert!(
required.contains(key),
"module-schema: missing {key:?} in required; got {required:?}"
);
}
}
#[test]
fn conformance_defaults_schema_completeness() {
let schema = load_schema("defaults");
let expected_namespaces = ["extensions", "schema", "acl", "executor", "observability"];
for ns in &expected_namespaces {
assert!(
schema["properties"].get(*ns).is_some(),
"defaults schema: missing namespace {ns:?}"
);
}
let config = Config::default();
let schema_max_depth = schema["properties"]["executor"]["properties"]["max_call_depth"]
.get("default")
.and_then(serde_json::Value::as_u64);
if let Some(schema_val) = schema_max_depth {
let config_val = config
.get("executor.max_call_depth")
.and_then(|v| v.as_u64())
.unwrap_or(0);
assert_eq!(
schema_val, config_val,
"defaults schema executor.max_call_depth mismatch"
);
}
let schema_timeout = schema["properties"]["executor"]["properties"]["default_timeout"]
.get("default")
.and_then(serde_json::Value::as_u64);
if let Some(schema_val) = schema_timeout {
let config_val = config
.get("executor.default_timeout")
.and_then(|v| v.as_u64())
.unwrap_or(0);
assert_eq!(
schema_val, config_val,
"defaults schema executor.default_timeout mismatch"
);
}
}
#[test]
fn conformance_sys_module_output_schemas() {
let cases = vec![
(
"sys-control-update-config",
vec!["success", "key", "old_value", "new_value"],
),
("sys-control-reload-module", vec!["success", "module_id"]),
(
"sys-control-toggle-feature",
vec!["success", "module_id", "enabled"],
),
("sys-health-summary", vec!["project", "summary", "modules"]),
(
"sys-health-module",
vec![
"module_id",
"status",
"total_calls",
"error_count",
"error_rate",
],
),
("sys-manifest-module", vec!["module_id", "description"]),
(
"sys-manifest-full",
vec!["project_name", "module_count", "modules"],
),
];
for (schema_name, expected_required) in cases {
let s = load_schema(schema_name);
let actual_required: Vec<&str> = s["required"]
.as_array()
.unwrap_or_else(|| {
panic!("schema {schema_name}: 'required' array missing or not an array")
})
.iter()
.map(|v| v.as_str().unwrap())
.collect();
for key in &expected_required {
assert!(
actual_required.contains(key),
"schema {schema_name}: missing required key {key:?}; got {actual_required:?}"
);
}
}
}