mod common;
use common::{build_client, cleanup, fresh_tenant, require_backend};
use reasoninglayer::api_spec::{
BulkCreateSortsRequest, BulkSortDefinition, ComputeGlbRequest, ComputeLubRequest,
CreateSortRequest, FeatureDescriptorDto,
};
use reasoninglayer::{
constrained, guard, psi, var, AddFactRequest, AddRuleRequest, BackwardChainRequest,
CreateTermRequest, FeatureInputValueDto, FindBySortRequest, GuardOp, Value,
};
use std::collections::BTreeMap;
use uuid::Uuid;
#[tokio::test]
#[ignore = "requires a running backend"]
async fn scenario_sort_lifecycle() {
let tenant = fresh_tenant();
let client = build_client(&tenant);
require_backend(&client).await;
let sort = client
.sorts()
.create_sort(CreateSortRequest::with_name("scenario_animal"), None)
.await
.expect("create_sort succeeds");
let sort_id = sort.id.to_string();
let fetched = client
.sorts()
.get_sort(&sort_id, None)
.await
.expect("get_sort succeeds");
assert_eq!(fetched.id, sort.id);
assert_eq!(fetched.name, "scenario_animal");
let listed = client
.sorts()
.list_sorts(None)
.await
.expect("list_sorts succeeds");
assert_eq!(listed.len(), 1, "expected exactly one sort, got {}", listed.len());
client
.sorts()
.delete_sort(&sort_id, None)
.await
.expect("delete_sort succeeds");
let after = client
.sorts()
.list_sorts(None)
.await
.expect("list_sorts after delete succeeds");
assert!(
after.is_empty(),
"expected empty sort list after delete, got {} sorts",
after.len()
);
cleanup(&client, &tenant).await;
}
#[tokio::test]
#[ignore = "requires a running backend"]
async fn scenario_sort_hierarchy_with_glb_lub() {
let tenant = fresh_tenant();
let client = build_client(&tenant);
require_backend(&client).await;
let animal = client
.sorts()
.create_sort(CreateSortRequest::with_name("animal"), None)
.await
.expect("create animal sort");
let dog_req = CreateSortRequest {
name: Some("dog".to_string()),
parents: vec![animal.id],
..Default::default()
};
let dog = client
.sorts()
.create_sort(dog_req, None)
.await
.expect("create dog sort");
let cat_req = CreateSortRequest {
name: Some("cat".to_string()),
parents: vec![animal.id],
..Default::default()
};
let cat = client
.sorts()
.create_sort(cat_req, None)
.await
.expect("create cat sort");
let is_sub = client
.sorts()
.is_subtype(&dog.id.to_string(), &animal.id.to_string(), None)
.await
.expect("is_subtype succeeds");
assert!(is_sub, "expected dog ⊆ animal");
let glb = client
.sorts()
.compute_glb(
ComputeGlbRequest {
sort1_id: dog.id,
sort2_id: cat.id,
},
None,
)
.await
.expect("compute_glb succeeds");
let _ = glb.glb;
let lub = client
.sorts()
.compute_lub(
ComputeLubRequest {
sort1_id: dog.id,
sort2_id: cat.id,
},
None,
)
.await
.expect("compute_lub succeeds");
assert_eq!(
lub.lub,
Some(animal.id),
"expected LUB(dog, cat) = animal"
);
cleanup(&client, &tenant).await;
}
#[tokio::test]
#[ignore = "requires a running backend"]
async fn scenario_term_lifecycle() {
let tenant = fresh_tenant();
let client = build_client(&tenant);
require_backend(&client).await;
let sort = client
.sorts()
.create_sort(CreateSortRequest::with_name("person"), None)
.await
.expect("create person sort");
let mut features = BTreeMap::new();
features.insert("name".into(), Value::string("Alice"));
features.insert("age".into(), Value::integer(30));
let term = client
.terms()
.create_term(
CreateTermRequest {
sort_id: sort.id.to_string(),
owner_id: tenant.clone(),
features,
},
None,
)
.await
.expect("create_term succeeds")
.term;
let found = client
.query()
.find_by_sort(
FindBySortRequest {
sort_name: Some("person".into()),
..Default::default()
},
None,
)
.await
.expect("find_by_sort succeeds");
assert_eq!(found.len(), 1);
assert_eq!(found[0].id, term.id);
assert_eq!(found[0].display_name.as_deref(), Some("Alice"));
cleanup(&client, &tenant).await;
}
#[tokio::test]
#[ignore = "requires a running backend"]
async fn scenario_backward_chain_finds_fact() {
let tenant = fresh_tenant();
let client = build_client(&tenant);
require_backend(&client).await;
client
.sorts()
.create_sort(CreateSortRequest::with_name("employee"), None)
.await
.expect("create employee");
client
.sorts()
.create_sort(CreateSortRequest::with_name("well_paid"), None)
.await
.expect("create well_paid");
client
.inference()
.add_fact(
AddFactRequest {
term: psi(
"employee",
[
("name", FeatureInputValueDto::string("Alice")),
("salary", FeatureInputValueDto::Integer(95_000)),
],
),
},
None,
)
.await
.expect("add_fact succeeds");
client
.inference()
.add_rule(
AddRuleRequest {
term: psi("well_paid", [("person", var("?X"))]),
antecedents: vec![psi(
"employee",
[
("name", var("?X")),
(
"salary",
constrained("?S", guard(GuardOp::Gt, 80_000_i64)),
),
],
)],
certainty: None,
},
None,
)
.await
.expect("add_rule succeeds");
let resp = client
.inference()
.backward_chain(
BackwardChainRequest {
goal: Some(psi("well_paid", [("person", var("?Who"))])),
max_solutions: Some(10),
..Default::default()
},
None,
)
.await
.expect("backward_chain succeeds");
assert!(
!resp.solutions.is_empty(),
"expected at least one solution binding ?Who, got {} solutions",
resp.solutions.len()
);
cleanup(&client, &tenant).await;
}
#[tokio::test]
#[ignore = "requires a running backend"]
async fn scenario_bulk_create_sorts_by_name() {
let tenant = fresh_tenant();
let client = build_client(&tenant);
require_backend(&client).await;
let req = BulkCreateSortsRequest {
sorts: vec![
BulkSortDefinition {
name: "vehicle".into(),
parents: vec![],
features: vec![],
alt_labels: vec![],
description: None,
},
BulkSortDefinition {
name: "car".into(),
parents: vec!["vehicle".into()],
features: vec![FeatureDescriptorDto {
name: "wheels".into(),
required: true,
annotations: Default::default(),
constraint: None,
expected_sort: None,
expected_type_hint: Some("Integer".into()),
}],
alt_labels: vec![],
description: None,
},
BulkSortDefinition {
name: "truck".into(),
parents: vec!["vehicle".into()],
features: vec![],
alt_labels: vec![],
description: None,
},
],
};
let resp = client
.sorts()
.bulk_create_sorts(req, None)
.await
.expect("bulk_create_sorts succeeds");
assert_eq!(resp.created_count, 3);
assert!(resp.errors.is_empty(), "unexpected errors: {:?}", resp.errors);
assert_eq!(resp.sort_ids.len(), 3);
let car_id = resp
.sort_ids
.get("car")
.expect("car sort_id present")
.to_string();
let vehicle_id = resp
.sort_ids
.get("vehicle")
.expect("vehicle sort_id present")
.to_string();
let is_sub = client
.sorts()
.is_subtype(&car_id, &vehicle_id, None)
.await
.expect("is_subtype succeeds");
assert!(is_sub, "expected car ⊆ vehicle after bulk create");
cleanup(&client, &tenant).await;
}
#[tokio::test]
#[ignore = "requires a running backend"]
async fn scenario_tenant_isolation() {
let tenant_a = fresh_tenant();
let tenant_b = fresh_tenant();
let client_a = build_client(&tenant_a);
let client_b = build_client(&tenant_b);
require_backend(&client_a).await;
client_a
.sorts()
.create_sort(CreateSortRequest::with_name("only_in_a"), None)
.await
.expect("create_sort in tenant A");
let in_b = client_b
.sorts()
.list_sorts(None)
.await
.expect("list_sorts in tenant B");
assert!(
in_b.iter().all(|s| s.name != "only_in_a"),
"tenant B sees tenant A's sort — isolation broken"
);
cleanup(&client_a, &tenant_a).await;
cleanup(&client_b, &tenant_b).await;
}
#[tokio::test]
#[ignore = "requires a running backend"]
async fn scenario_constrained_variable_round_trips_through_backend() {
let tenant = fresh_tenant();
let client = build_client(&tenant);
require_backend(&client).await;
client
.sorts()
.create_sort(CreateSortRequest::with_name("number_holder"), None)
.await
.expect("create sort");
client
.inference()
.add_fact(
AddFactRequest {
term: psi(
"number_holder",
[("value", FeatureInputValueDto::Integer(42))],
),
},
None,
)
.await
.expect("add fact");
let goal = psi(
"number_holder",
[(
"value",
constrained("?V", guard(GuardOp::Lt, 100_i64)),
)],
);
let resp = client
.inference()
.backward_chain(
BackwardChainRequest {
goal: Some(goal),
max_solutions: Some(1),
..Default::default()
},
None,
)
.await
.expect("backward_chain with constrained var succeeds");
assert!(
!resp.solutions.is_empty(),
"expected at least one solution for value < 100"
);
cleanup(&client, &tenant).await;
}
fn _silence_unused_imports() {
let _: Option<Uuid> = None;
}