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())
.unwrap_or(false);
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 {
}
}
}