#![cfg(feature = "graph-rag")]
use std::sync::Arc;
use klieo::embed_common::FakeEmbedder;
use klieo::{App, GraphRagConfig};
use klieo_core::memory::{Fact, Scope};
const TEST_EMBEDDER_ID: &str = "test-fake";
const TEST_EMBEDDER_DIM: usize = 384;
fn test_embedder() -> Arc<FakeEmbedder> {
Arc::new(FakeEmbedder::new(TEST_EMBEDDER_DIM))
}
fn test_cfg() -> GraphRagConfig {
GraphRagConfig::in_memory().embedder(test_embedder(), TEST_EMBEDDER_ID)
}
#[tokio::test]
async fn shutdown_drains_projector() {
let app = App::local()
.model("dummy-model")
.graph_rag_with(test_cfg())
.build()
.await
.expect("graph-rag App should build with an in-memory backend");
app.shutdown().await;
app.shutdown().await;
}
#[tokio::test]
async fn shutdown_does_not_cancel_shared_parent() {
use tokio_util::sync::CancellationToken;
let parent = CancellationToken::new();
let app = App::local()
.model("dummy-model")
.cancel_token(parent.clone())
.graph_rag_with(test_cfg())
.build()
.await
.expect("graph-rag App should build with an in-memory backend");
app.shutdown().await;
assert!(
!parent.is_cancelled(),
"shutting down the projector must not cancel the App's parent token"
);
}
#[tokio::test]
async fn in_memory_graph_rag_builds_and_recalls() {
let cfg = GraphRagConfig::in_memory().embedder(test_embedder(), TEST_EMBEDDER_ID);
let app = App::local()
.model("dummy-model")
.graph_rag_with(cfg)
.build()
.await
.expect("graph-rag App should build with an in-memory backend");
let long_term = app.memory().long_term.clone();
let scope = Scope::Workspace("facade-test".to_string());
let target = "klieo wires GraphRAG into App::build";
long_term
.remember(scope.clone(), Fact::new(target))
.await
.expect("remember through the graph-aware long-term port should succeed");
long_term
.remember(
scope.clone(),
Fact::new("an entirely unrelated note about gardening"),
)
.await
.expect("remember of a distractor fact should succeed");
let hits = long_term
.recall(scope, "klieo wires GraphRAG into App::build", 5)
.await
.expect("recall through the graph-aware long-term port should succeed");
assert_eq!(
hits.first().map(|f| f.text.as_str()),
Some(target),
"the fact matching the query must rank first under real cosine ranking; got {hits:?}"
);
}
#[tokio::test]
async fn graph_rag_without_provenance_still_recalls() {
let cfg = GraphRagConfig::in_memory()
.embedder(test_embedder(), TEST_EMBEDDER_ID)
.without_provenance();
let app = App::local()
.model("dummy-model")
.graph_rag_with(cfg)
.build()
.await
.expect("graph-rag App should build without the provenance wrapper");
let long_term = app.memory().long_term.clone();
let scope = Scope::Workspace("facade-test-no-prov".to_string());
long_term
.remember(scope.clone(), Fact::new("provenance can be disabled"))
.await
.expect("remember should succeed without provenance");
let hits = long_term
.recall(scope, "provenance can be disabled", 5)
.await
.expect("recall should succeed without provenance");
assert!(
hits.iter().any(|f| f.text.contains("provenance")),
"fact must round-trip even with the provenance wrapper disabled; got {hits:?}"
);
}
#[tokio::test]
async fn graph_rag_exposes_metrics_when_built() {
let cfg = GraphRagConfig::in_memory().embedder(test_embedder(), TEST_EMBEDDER_ID);
let app = App::local()
.model("dummy-model")
.graph_rag_with(cfg)
.build()
.await
.expect("graph-rag App should build with an in-memory backend");
assert!(
app.graph_rag_metrics().is_some(),
"an App built with graph_rag must expose a recall-metrics snapshot"
);
}
#[tokio::test]
async fn graph_rag_without_projector_still_exposes_metrics() {
let cfg = GraphRagConfig::in_memory()
.embedder(test_embedder(), TEST_EMBEDDER_ID)
.without_projector();
let app = App::local()
.model("dummy-model")
.graph_rag_with(cfg)
.build()
.await
.expect("graph-rag App should build with the projector disabled");
assert!(
app.graph_rag_metrics().is_some(),
"metrics are exposed even when the episode-to-graph projector is off"
);
}