stemedb/crates/stemedb-query/tests/battery/battery9_alias_store.rs
jordan 137a588ed0 feat: Concept hierarchy (Phase 5D) - ConceptPath, source schemes, AliasStore
Implements hierarchical subject identifiers with scheme-based source tier inference:

- ConceptPath type with parse/wire_format, leaf/parent, prefix matching
- SourceScheme registry mapping schemes to default SourceClass tiers:
  - rfc://, fda://, ietf:// → Regulatory (Tier 0)
  - peer://, pubmed:// → PeerReviewed (Tier 1)
  - code://, wiki:// → Expert (Tier 3)
  - blog://, anon:// → Anecdotal (Tier 5)
- AliasStore for cross-scheme entity resolution (bidirectional indexing)
- API endpoints for concept operations
- Battery tests 8, 9 & 10 for concepts, aliases, and advanced signatures
- Go SDK updates for concept types and signing

Completes Phase 5, advancing to Phase 6 (Distributed Writes).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 17:44:54 -07:00

290 lines
11 KiB
Rust

//! Battery 9: AliasStore Resolution and Cross-Scheme Queries.
//!
//! Tests alias storage, resolution, and transitive expansion for cross-scheme queries.
//!
//! # Test Coverage
//!
//! | Test | Feature | Validates |
//! |------|---------|-----------|
//! | `test_alias_direct_resolution` | Basic alias | Query alias, get canonical |
//! | `test_alias_transitive_resolution` | A → B → C chain | Transitive resolution |
//! | `test_alias_cycle_detection` | A → B → A | Safe termination |
//! | `test_alias_bidirectional` | Reverse lookup | get_aliases for canonical |
//! | `test_alias_delete` | Delete alias | Clean removal |
//! | `test_alias_suggest` | Suggest aliases | Similarity-based suggestions |
#![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages
use std::sync::Arc;
use stemedb_core::types::{AliasOrigin, ConceptAlias, ConceptPath};
use stemedb_storage::{AliasStore, GenericAliasStore, HybridStore};
/// 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 9.1: Direct alias resolution.
///
/// Store alias: code://rust/auth/jwt/aud → rfc://7519/jwt/audience
/// Query: get_canonical("code://rust/auth/jwt/aud")
/// Expect: rfc://7519/jwt/audience
#[tokio::test]
async fn test_alias_direct_resolution() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Store alias
let alias = create_alias("code://rust/auth/jwt/aud", "rfc://7519/jwt/audience");
alias_store.set_alias(&alias).await.expect("set alias");
// Resolve alias
let canonical =
alias_store.get_canonical("code://rust/auth/jwt/aud").await.expect("get canonical");
assert!(canonical.is_some(), "should find canonical path");
let canonical_path = canonical.unwrap();
assert_eq!(canonical_path.scheme, "rfc");
assert_eq!(canonical_path.segments, vec!["7519", "jwt", "audience"]);
}
/// Test 9.2: Transitive alias resolution.
///
/// Store chain: internal://jwt → code://rust/auth/jwt → rfc://7519/jwt
/// Query: resolve_all("internal://jwt")
/// Expect: all three paths returned
#[tokio::test]
async fn test_alias_transitive_resolution() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Store alias chain: internal → code → rfc
let alias1 = create_alias("internal://jwt/impl", "code://rust/auth/jwt");
let alias2 = create_alias("code://rust/auth/jwt", "rfc://7519/jwt");
alias_store.set_alias(&alias1).await.expect("set alias1");
alias_store.set_alias(&alias2).await.expect("set alias2");
// Resolve all from internal (start of chain)
let all_paths = alias_store.resolve_all("internal://jwt/impl").await.expect("resolve all");
assert!(all_paths.contains(&"internal://jwt/impl".to_string()), "should include starting path");
assert!(
all_paths.contains(&"code://rust/auth/jwt".to_string()),
"should include intermediate alias"
);
assert!(all_paths.contains(&"rfc://7519/jwt".to_string()), "should include final canonical");
// Should have exactly 3 paths
assert_eq!(all_paths.len(), 3, "should resolve to 3 paths in chain");
}
/// Test 9.3: Cycle detection.
///
/// Store cycle: A → B → A
/// Query: resolve_all("A")
/// Expect: Safe termination, returns [A, B] without infinite loop
#[tokio::test]
async fn test_alias_cycle_detection() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Create a cycle: code → rfc → code
let alias1 = create_alias("code://cycle/a", "rfc://cycle/b");
let alias2 = create_alias("rfc://cycle/b", "code://cycle/a");
alias_store.set_alias(&alias1).await.expect("set alias1");
alias_store.set_alias(&alias2).await.expect("set alias2");
// Resolve all - should not hang
let all_paths = alias_store.resolve_all("code://cycle/a").await.expect("resolve all (cycle)");
// Should have exactly 2 paths
assert_eq!(all_paths.len(), 2, "cycle should resolve to 2 paths");
assert!(all_paths.contains(&"code://cycle/a".to_string()), "should include A");
assert!(all_paths.contains(&"rfc://cycle/b".to_string()), "should include B");
}
/// Test 9.4: Bidirectional lookup (reverse index).
///
/// Store alias: code://rust/auth/jwt → rfc://7519/jwt
/// Query: get_aliases("rfc://7519/jwt")
/// Expect: [code://rust/auth/jwt]
#[tokio::test]
async fn test_alias_bidirectional() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Store multiple aliases pointing to same canonical
let alias1 = create_alias("code://rust/auth/jwt", "rfc://7519/jwt");
let alias2 = create_alias("internal://jwt/impl", "rfc://7519/jwt");
alias_store.set_alias(&alias1).await.expect("set alias1");
alias_store.set_alias(&alias2).await.expect("set alias2");
// Reverse lookup: get all aliases for canonical
let aliases = alias_store.get_aliases("rfc://7519/jwt").await.expect("get aliases");
assert_eq!(aliases.len(), 2, "should have 2 aliases for canonical");
let alias_strings: Vec<String> = aliases.iter().map(|p| p.to_wire_format()).collect();
assert!(
alias_strings.contains(&"code://rust/auth/jwt".to_string()),
"should include code alias"
);
assert!(
alias_strings.contains(&"internal://jwt/impl".to_string()),
"should include internal alias"
);
}
/// Test 9.5: Delete alias.
///
/// Store alias, verify exists, delete, verify gone.
/// Also verify reverse index is updated.
#[tokio::test]
async fn test_alias_delete() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Store alias
let alias = create_alias("code://rust/auth/jwt", "rfc://7519/jwt");
alias_store.set_alias(&alias).await.expect("set alias");
// Verify it exists
let canonical = alias_store
.get_canonical("code://rust/auth/jwt")
.await
.expect("get canonical before delete");
assert!(canonical.is_some(), "alias should exist before delete");
// Delete
let deleted = alias_store.delete_alias("code://rust/auth/jwt").await.expect("delete alias");
assert!(deleted, "delete should return true");
// Verify forward lookup is gone
let canonical_after = alias_store
.get_canonical("code://rust/auth/jwt")
.await
.expect("get canonical after delete");
assert!(canonical_after.is_none(), "alias should not exist after delete");
// Verify reverse lookup is updated
let aliases =
alias_store.get_aliases("rfc://7519/jwt").await.expect("get aliases after delete");
assert!(aliases.is_empty(), "reverse index should be empty after delete");
}
/// Test 9.6: Delete non-existent alias returns false.
#[tokio::test]
async fn test_alias_delete_nonexistent() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Try to delete non-existent alias
let deleted = alias_store.delete_alias("nonexistent://path").await.expect("delete nonexistent");
assert!(!deleted, "delete should return false for non-existent alias");
}
/// Test 9.7: Alias suggestions based on leaf similarity.
///
/// Given existing subjects with similar leaf names across DIFFERENT schemes,
/// suggest potential aliases.
///
/// Note: Suggestions only work across different schemes (cross-scheme aliasing).
/// Same-scheme paths are not suggested as aliases.
#[tokio::test]
async fn test_alias_suggest() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Existing subjects in the system
let existing_subjects = vec![
"rfc://7519/jwt/audience".to_string(),
"internal://jwt/aud".to_string(), // different scheme, similar leaf
"owasp://top10/injection".to_string(),
"code://rust/citadeldb/net/tls".to_string(),
];
// Ask for suggestions for a new code path with "audience" leaf
let suggestions = alias_store
.suggest_aliases("code://new/jwt/audience", &existing_subjects)
.await
.expect("suggest aliases");
// Should suggest the RFC path with same leaf name (different scheme)
let suggested_paths: Vec<&str> = suggestions.iter().map(|(p, _)| p.as_str()).collect();
assert!(
suggested_paths.contains(&"rfc://7519/jwt/audience"),
"should suggest rfc://7519/jwt/audience (same leaf 'audience', different scheme)"
);
// Should suggest the internal path with similar leaf 'aud' (different scheme)
// ('aud' is substring of 'audience')
assert!(
suggested_paths.contains(&"internal://jwt/aud"),
"should suggest internal://jwt/aud (similar leaf 'aud', different scheme)"
);
// Should NOT suggest unrelated paths
assert!(
!suggested_paths.contains(&"owasp://top10/injection"),
"should NOT suggest unrelated path"
);
// Should NOT suggest same-scheme paths even with similar leaf
assert!(
!suggested_paths.contains(&"code://rust/citadeldb/net/tls"),
"should NOT suggest same-scheme path"
);
}
/// Test 9.8: List all aliases.
///
/// Store multiple aliases, list all, verify count and content.
#[tokio::test]
async fn test_alias_list_all() {
let store = Arc::new(HybridStore::open_temp().expect("create store"));
let alias_store = GenericAliasStore::new(store);
// Store multiple aliases
let alias1 = create_alias("code://a", "rfc://1");
let alias2 = create_alias("code://b", "rfc://2");
let alias3 = create_alias("internal://c", "rfc://3");
alias_store.set_alias(&alias1).await.expect("set alias1");
alias_store.set_alias(&alias2).await.expect("set alias2");
alias_store.set_alias(&alias3).await.expect("set alias3");
// List all
let all_aliases = alias_store.list_all_aliases().await.expect("list all");
assert_eq!(all_aliases.len(), 3, "should have 3 aliases");
// Verify content
let alias_map: std::collections::HashMap<String, String> = all_aliases.into_iter().collect();
assert_eq!(
alias_map.get("code://a"),
Some(&"rfc://1".to_string()),
"code://a should map to rfc://1"
);
assert_eq!(
alias_map.get("code://b"),
Some(&"rfc://2".to_string()),
"code://b should map to rfc://2"
);
assert_eq!(
alias_map.get("internal://c"),
Some(&"rfc://3".to_string()),
"internal://c should map to rfc://3"
);
}