kmp-application 0.18.1

Application services behind KMP's ten memory moves and evidence-backed recall
Documentation
use std::sync::{Arc, Mutex};

use kmp_domain::{
    ContextPathNeighborhood, GraphNeighborhoodReader, NodeDetailProjection, NodeDetailReader,
    NodeNeighborhood, NodeProjection, PortError,
};

use super::QueryApplicationService;

#[derive(Default)]
struct Records {
    nodes: Vec<Option<NodeProjection>>,
    bodies: Vec<Option<NodeDetailProjection>>,
    requested_bodies: Mutex<Vec<String>>,
    body_error: bool,
}

impl GraphNeighborhoodReader for Records {
    async fn load_nodes_batch(
        &self,
        _: Vec<String>,
    ) -> Result<Vec<Option<NodeProjection>>, PortError> {
        Ok(self.nodes.clone())
    }
    async fn load_neighborhood(
        &self,
        _: &str,
        _: u32,
    ) -> Result<Option<NodeNeighborhood>, PortError> {
        panic!("selected batch must not read source neighborhoods")
    }
    async fn load_context_path(
        &self,
        _: &str,
        _: &str,
        _: u32,
    ) -> Result<Option<ContextPathNeighborhood>, PortError> {
        panic!("selected batch must not discover a path")
    }
}
impl NodeDetailReader for Records {
    async fn load_node_detail(&self, _: &str) -> Result<Option<NodeDetailProjection>, PortError> {
        panic!("selected batch must reuse the body batch")
    }
    async fn load_node_details_batch(
        &self,
        ids: Vec<String>,
    ) -> Result<Vec<Option<NodeDetailProjection>>, PortError> {
        *self.requested_bodies.lock().expect("fixture lock") = ids;
        if self.body_error {
            return Err(PortError::Unavailable("body read failed".into()));
        }
        Ok(self.bodies.clone())
    }
}

fn node(id: &str) -> NodeProjection {
    NodeProjection {
        node_id: id.into(),
        node_kind: "evidence".into(),
        title: "Signed source".into(),
        summary: "Stored summary".into(),
        status: "ACTIVE".into(),
        labels: vec!["source".into()],
        properties: [("source".into(), "signed:document".into())].into(),
        provenance: None,
    }
}
fn body(id: &str) -> NodeDetailProjection {
    NodeDetailProjection {
        node_id: id.into(),
        detail: "Canonical source — 17 != 18".into(),
        content_hash: "opaque-stored-hash".into(),
        revision: 7,
    }
}
fn query(records: &Arc<Records>) -> QueryApplicationService<Records, Records, ()> {
    QueryApplicationService::new(records.clone(), records.clone(), Arc::new(()), "batch-test")
}

#[tokio::test]
async fn selected_nodes_keep_order_duplicates_properties_and_missing_body_states() {
    let records = Arc::new(Records {
        nodes: vec![Some(node("a")), None, Some(node("b")), Some(node("a"))],
        bodies: vec![Some(body("a")), None, Some(body("a"))],
        ..Default::default()
    });
    let result = query(&records)
        .get_node_details(vec![" a ".into(), "orphan".into(), "b".into(), "a".into()])
        .await
        .expect("typed batch");
    assert_eq!(
        *records.requested_bodies.lock().expect("fixture lock"),
        ["a", "b", "a"]
    );
    assert!(
        result[1].is_none(),
        "an absent graph node is not an orphaned body"
    );
    assert!(result[2].as_ref().expect("present node").detail.is_none());
    assert!(
        result[0] == result[3],
        "duplicate input retains its complete slot"
    );
    let a = result[0].as_ref().expect("present a");
    assert_eq!(a.node.properties["source"], "signed:document");
    assert_eq!(a.node.labels, ["source"]);
    assert_eq!(a.node.summary, "Stored summary");
    let a_body = a.detail.as_ref().expect("source body");
    assert_eq!(a_body.detail, "Canonical source — 17 != 18");
    assert_eq!(a_body.content_hash, "opaque-stored-hash");
    assert_eq!(a_body.revision, 7);
}

#[tokio::test]
async fn malformed_batch_slots_are_refused_instead_of_attaching_the_wrong_body() {
    for records in [
        Records {
            nodes: vec![],
            ..Default::default()
        },
        Records {
            nodes: vec![Some(node("other"))],
            ..Default::default()
        },
        Records {
            nodes: vec![Some(node("a"))],
            bodies: vec![],
            ..Default::default()
        },
        Records {
            nodes: vec![Some(node("a"))],
            bodies: vec![Some(body("other"))],
            ..Default::default()
        },
    ] {
        let result = query(&Arc::new(records))
            .get_node_details(vec!["a".into()])
            .await;
        assert!(matches!(
            result,
            Err(crate::ApplicationError::Ports(PortError::InvalidState(_)))
        ));
    }
}

#[tokio::test]
async fn empty_and_absent_selections_skip_bodies_but_real_read_errors_propagate() {
    let records = Arc::new(Records {
        nodes: vec![None],
        body_error: true,
        ..Default::default()
    });
    assert!(
        query(&records)
            .get_node_details(vec![])
            .await
            .expect("empty")
            .is_empty()
    );
    assert!(
        query(&records)
            .get_node_details(vec!["orphan".into()])
            .await
            .expect("missing")[0]
            .is_none()
    );
    assert!(
        records
            .requested_bodies
            .lock()
            .expect("fixture lock")
            .is_empty()
    );
    assert!(matches!(
        query(&records).get_node_details(vec![" ".into()]).await,
        Err(crate::ApplicationError::Validation(_))
    ));
    let broken = Arc::new(Records {
        nodes: vec![Some(node("a"))],
        body_error: true,
        ..Default::default()
    });
    assert!(matches!(
        query(&broken).get_node_details(vec!["a".into()]).await,
        Err(crate::ApplicationError::Ports(PortError::Unavailable(_)))
    ));
}