//! Tests for alias resolution in QueryEngine. //! //! Tests the `resolve_aliases` query flag and `alias_store` integration. use std::sync::Arc; use stemedb_core::testing::AssertionBuilder; use stemedb_core::types::{AliasOrigin, ConceptAlias, ConceptPath, LifecycleStage}; use stemedb_storage::{AliasStore, GenericAliasStore, HybridStore}; use super::{store_assertion, QueryEngine}; use crate::query::Query; /// Helper to create a test ConceptAlias. fn create_alias(alias: &str, canonical: &str) -> ConceptAlias { ConceptAlias::new( ConceptPath::parse(alias).expect("valid alias path"), ConceptPath::parse(canonical).expect("valid canonical path"), [1u8; 32], // agent_id 1000, // timestamp AliasOrigin::Manual, ) } /// Test that resolve_aliases: true expands subject to aliased paths. #[tokio::test] async fn test_resolve_aliases_expands_subjects() { let store = Arc::new(HybridStore::open_temp().expect("store")); // Create assertions for two different subjects (aliased paths) let code_assertion = AssertionBuilder::new() .subject("code://rust/myapp/tls") .predicate("cert_verification") .object_text("enabled") .confidence(0.8) .lifecycle(LifecycleStage::Approved) .build(); let rfc_assertion = AssertionBuilder::new() .subject("rfc://5246/tls") .predicate("cert_verification") .object_text("required") .confidence(0.95) .lifecycle(LifecycleStage::Approved) .build(); store_assertion(&store, &code_assertion).await; store_assertion(&store, &rfc_assertion).await; // Set up alias store with alias: code://rust/myapp/tls -> rfc://5246/tls let alias_store = GenericAliasStore::new(store.clone()); let alias = create_alias("code://rust/myapp/tls", "rfc://5246/tls"); alias_store.set_alias(&alias).await.expect("set alias"); // Create query engine with alias store let engine = QueryEngine::new(Arc::new(store.clone())) .with_alias_store(Arc::new(alias_store) as Arc); // Query with resolve_aliases: true should find BOTH assertions let query = Query::builder() .subject("code://rust/myapp/tls") .predicate("cert_verification") .resolve_aliases(true) .build(); let result = engine.execute(&query).await.expect("execute"); assert_eq!(result.assertions.len(), 2, "Should find assertions from both aliased subjects"); let subjects: Vec<&str> = result.assertions.iter().map(|a| a.subject.as_str()).collect(); assert!(subjects.contains(&"code://rust/myapp/tls"), "Should include code assertion"); assert!(subjects.contains(&"rfc://5246/tls"), "Should include rfc assertion"); } /// Test that resolve_aliases: false queries exact subject only. #[tokio::test] async fn test_resolve_aliases_false_is_exact() { let store = Arc::new(HybridStore::open_temp().expect("store")); // Create assertions for two different subjects let code_assertion = AssertionBuilder::new() .subject("code://rust/myapp/tls") .predicate("cert_verification") .object_text("enabled") .confidence(0.8) .lifecycle(LifecycleStage::Approved) .build(); let rfc_assertion = AssertionBuilder::new() .subject("rfc://5246/tls") .predicate("cert_verification") .object_text("required") .confidence(0.95) .lifecycle(LifecycleStage::Approved) .build(); store_assertion(&store, &code_assertion).await; store_assertion(&store, &rfc_assertion).await; // Set up alias store with alias let alias_store = GenericAliasStore::new(store.clone()); let alias = create_alias("code://rust/myapp/tls", "rfc://5246/tls"); alias_store.set_alias(&alias).await.expect("set alias"); let engine = QueryEngine::new(Arc::new(store.clone())) .with_alias_store(Arc::new(alias_store) as Arc); // Query with resolve_aliases: false (default) should find only the exact subject let query = Query::builder() .subject("code://rust/myapp/tls") .predicate("cert_verification") .resolve_aliases(false) .build(); let result = engine.execute(&query).await.expect("execute"); assert_eq!(result.assertions.len(), 1, "Should find only exact subject assertion"); assert_eq!(result.assertions[0].subject, "code://rust/myapp/tls"); } /// Test that resolve_aliases: true without alias_store gracefully returns exact subject. #[tokio::test] async fn test_no_alias_store_graceful() { let store = Arc::new(HybridStore::open_temp().expect("store")); let assertion = AssertionBuilder::new() .subject("code://rust/myapp/tls") .predicate("cert_verification") .object_text("enabled") .confidence(0.8) .lifecycle(LifecycleStage::Approved) .build(); store_assertion(&store, &assertion).await; // Query engine WITHOUT alias store let engine = QueryEngine::new(Arc::new(store)); // Query with resolve_aliases: true should still work (graceful degradation) let query = Query::builder() .subject("code://rust/myapp/tls") .predicate("cert_verification") .resolve_aliases(true) .build(); let result = engine.execute(&query).await.expect("execute"); assert_eq!(result.assertions.len(), 1, "Should find exact subject assertion"); assert_eq!(result.assertions[0].subject, "code://rust/myapp/tls"); } /// Test that alias resolution deduplicates by assertion hash. #[tokio::test] async fn test_resolve_aliases_deduplicates() { let store = Arc::new(HybridStore::open_temp().expect("store")); // Create a single assertion let assertion = AssertionBuilder::new() .subject("code://rust/myapp/tls") .predicate("cert_verification") .object_text("enabled") .confidence(0.8) .lifecycle(LifecycleStage::Approved) .build(); store_assertion(&store, &assertion).await; // Set up alias store where both paths lead to the same subject // (In real use, this wouldn't happen, but tests the dedup logic) let alias_store = GenericAliasStore::new(store.clone()); // No alias set - both code:// and the query subject are the same let engine = QueryEngine::new(Arc::new(store.clone())) .with_alias_store(Arc::new(alias_store) as Arc); // Query with resolve_aliases: true let query = Query::builder().subject("code://rust/myapp/tls").resolve_aliases(true).build(); let result = engine.execute(&query).await.expect("execute"); // Should have exactly 1 result (no duplicates) assert_eq!(result.assertions.len(), 1); } /// Test subject-only query with alias resolution. #[tokio::test] async fn test_resolve_aliases_subject_only() { let store = Arc::new(HybridStore::open_temp().expect("store")); // Create assertions for two aliased subjects with different predicates let code_assertion1 = AssertionBuilder::new() .subject("code://rust/myapp/tls") .predicate("cert_verification") .object_text("enabled") .confidence(0.8) .lifecycle(LifecycleStage::Approved) .build(); let code_assertion2 = AssertionBuilder::new() .subject("code://rust/myapp/tls") .predicate("timeout") .object_text("30s") .confidence(0.9) .lifecycle(LifecycleStage::Approved) .build(); let rfc_assertion = AssertionBuilder::new() .subject("rfc://5246/tls") .predicate("cert_verification") .object_text("required") .confidence(0.95) .lifecycle(LifecycleStage::Approved) .build(); store_assertion(&store, &code_assertion1).await; store_assertion(&store, &code_assertion2).await; store_assertion(&store, &rfc_assertion).await; // Set up alias store let alias_store = GenericAliasStore::new(store.clone()); let alias = create_alias("code://rust/myapp/tls", "rfc://5246/tls"); alias_store.set_alias(&alias).await.expect("set alias"); let engine = QueryEngine::new(Arc::new(store.clone())) .with_alias_store(Arc::new(alias_store) as Arc); // Query by subject only (no predicate filter) with resolve_aliases: true let query = Query::builder().subject("code://rust/myapp/tls").resolve_aliases(true).build(); let result = engine.execute(&query).await.expect("execute"); // Should find all 3 assertions (2 from code://, 1 from rfc://) assert_eq!(result.assertions.len(), 3, "Should find all assertions from aliased subjects"); } /// Test default query (resolve_aliases not set) behaves like exact match. #[tokio::test] async fn test_resolve_aliases_default_is_false() { let query = Query::builder().subject("test").predicate("pred").build(); assert!(!query.resolve_aliases, "Default should be false"); } /// Test query builder sets resolve_aliases correctly. #[tokio::test] async fn test_query_builder_resolve_aliases() { let query_true = Query::builder().subject("test").predicate("pred").resolve_aliases(true).build(); let query_false = Query::builder().subject("test").predicate("pred").resolve_aliases(false).build(); assert!(query_true.resolve_aliases, "Should be true when set to true"); assert!(!query_false.resolve_aliases, "Should be false when set to false"); }