reasoninglayer 1.0.3

Rust client SDK for the Reasoning Layer API
Documentation
//! Diagnosis tests — one quick smoke test per major resource client.
//!
//! Goal: verify each [`crate::ReasoningLayerClient`] accessor talks to a real
//! backend and round-trips its primary DTO without deserialization errors.
//! These tests aren't trying to exercise full functionality (see
//! `tests/scenarios.rs` for that) — they're "is the wiring intact?" probes
//! that surface schema / field-rename drift fast.
//!
//! Gated with `#[ignore]`. Run with:
//!
//! ```sh
//! cargo test --test diagnosis -- --ignored --test-threads=1
//! ```
//!
//! `--test-threads=1` is recommended because the tests share the
//! `clear-tenant` admin endpoint and parallel runs against the same tenant
//! UUID would race. Each test creates its own UUID-v4 tenant, so different
//! test binaries are already isolated.

mod common;

use common::{build_client, cleanup, fresh_tenant, require_backend};
use reasoninglayer::{
    psi, AddFactRequest, BackwardChainRequest, CreateSortRequest, CreateTermRequest,
    FeatureInputValueDto, FindBySortRequest, Value,
};
use std::collections::BTreeMap;

#[tokio::test]
#[ignore = "requires a running backend"]
async fn diag_health_check_returns_status() {
    let tenant = fresh_tenant();
    let client = build_client(&tenant);

    let resp = client
        .health()
        .check(None)
        .await
        .expect("health check round-trips");

    assert_eq!(resp.status, "healthy", "backend should report healthy");
    assert!(
        !resp.components.is_empty(),
        "expected at least one component reported"
    );
}

#[tokio::test]
#[ignore = "requires a running backend"]
async fn diag_sorts_create_and_list_round_trips() {
    let tenant = fresh_tenant();
    let client = build_client(&tenant);
    require_backend(&client).await;

    let sort = client
        .sorts()
        .create_sort(CreateSortRequest::with_name("diag_sort"), None)
        .await
        .expect("create_sort succeeds");

    assert_eq!(sort.name, "diag_sort");
    assert_eq!(sort.tenant_id.to_string(), tenant);

    let listed = client
        .sorts()
        .list_sorts(None)
        .await
        .expect("list_sorts succeeds");

    assert!(
        listed.iter().any(|s| s.id == sort.id),
        "list_sorts should include just-created sort {}",
        sort.id
    );

    cleanup(&client, &tenant).await;
}

#[tokio::test]
#[ignore = "requires a running backend"]
async fn diag_terms_create_round_trips() {
    let tenant = fresh_tenant();
    let client = build_client(&tenant);
    require_backend(&client).await;

    let sort = client
        .sorts()
        .create_sort(CreateSortRequest::with_name("diag_term_sort"), None)
        .await
        .expect("create_sort succeeds");

    let mut features = BTreeMap::new();
    features.insert("label".into(), Value::string("hello"));
    let resp = client
        .terms()
        .create_term(
            CreateTermRequest {
                sort_id: sort.id.to_string(),
                owner_id: tenant.clone(),
                features,
            },
            None,
        )
        .await
        .expect("create_term succeeds");

    assert_eq!(resp.term.sort_id, sort.id.to_string());
    assert_eq!(resp.term.owner_id, tenant);

    cleanup(&client, &tenant).await;
}

#[tokio::test]
#[ignore = "requires a running backend"]
async fn diag_inference_add_fact_then_get_facts() {
    let tenant = fresh_tenant();
    let client = build_client(&tenant);
    require_backend(&client).await;

    client
        .sorts()
        .create_sort(CreateSortRequest::with_name("diag_fact_sort"), None)
        .await
        .expect("create_sort succeeds");

    let fact = psi(
        "diag_fact_sort",
        [("name", FeatureInputValueDto::string("alpha"))],
    );
    client
        .inference()
        .add_fact(
            AddFactRequest { term: fact },
            None,
        )
        .await
        .expect("add_fact succeeds");

    let facts = client
        .inference()
        .get_facts(None)
        .await
        .expect("get_facts succeeds");

    assert!(
        facts.facts.iter().any(|t| t.sort_name == "diag_fact_sort"),
        "expected the just-added fact to appear in get_facts"
    );

    cleanup(&client, &tenant).await;
}

#[tokio::test]
#[ignore = "requires a running backend"]
async fn diag_query_find_by_sort_returns_terms() {
    let tenant = fresh_tenant();
    let client = build_client(&tenant);
    require_backend(&client).await;

    let sort_name = "diag_query_sort";
    let sort = client
        .sorts()
        .create_sort(CreateSortRequest::with_name(sort_name), None)
        .await
        .expect("create_sort succeeds");

    let mut features = BTreeMap::new();
    features.insert("name".into(), Value::string("alpha"));
    client
        .terms()
        .create_term(
            CreateTermRequest {
                sort_id: sort.id.to_string(),
                owner_id: tenant.clone(),
                features,
            },
            None,
        )
        .await
        .expect("create_term succeeds");

    let terms = client
        .query()
        .find_by_sort(
            FindBySortRequest {
                sort_name: Some(sort_name.into()),
                ..Default::default()
            },
            None,
        )
        .await
        .expect("find_by_sort succeeds");

    assert!(
        !terms.is_empty(),
        "expected at least one term for sort {sort_name}, got 0"
    );

    cleanup(&client, &tenant).await;
}

#[tokio::test]
#[ignore = "requires a running backend"]
async fn diag_admin_list_tenants_returns_us() {
    let tenant = fresh_tenant();
    let client = build_client(&tenant);
    require_backend(&client).await;

    // Smoke-test that the typed response deserializes. Whether *our* tenant
    // shows up in the list depends on backend semantics (some implementations
    // only list tenants with terms, not sorts) — that's a separate scenario.
    let listed = client
        .admin()
        .list_tenants(None)
        .await
        .expect("list_tenants succeeds");

    // Field shape sanity check: every entry must round-trip cleanly.
    for t in &listed.tenants {
        assert!(
            uuid::Uuid::parse_str(&t.tenant_id).is_ok(),
            "list_tenants returned non-UUID tenant_id: {:?}",
            t.tenant_id
        );
    }

    cleanup(&client, &tenant).await;
}

#[tokio::test]
#[ignore = "requires a running backend"]
async fn diag_backward_chain_round_trips_empty_solution() {
    // Smoke-test inference path even when no rules apply — the response must
    // still deserialize cleanly from the backend.
    let tenant = fresh_tenant();
    let client = build_client(&tenant);
    require_backend(&client).await;

    client
        .sorts()
        .create_sort(CreateSortRequest::with_name("diag_chain_sort"), None)
        .await
        .expect("create_sort succeeds");

    let resp = client
        .inference()
        .backward_chain(
            BackwardChainRequest {
                goal: Some(psi(
                    "diag_chain_sort",
                    [("name", FeatureInputValueDto::string("missing"))],
                )),
                max_solutions: Some(1),
                ..Default::default()
            },
            None,
        )
        .await
        .expect("backward_chain round-trips even on empty result");

    // No rules → expect zero solutions, but response must parse.
    assert!(
        resp.solutions.is_empty(),
        "expected empty solutions but got {}",
        resp.solutions.len()
    );

    cleanup(&client, &tenant).await;
}