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>
290 lines
11 KiB
Rust
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"
|
|
);
|
|
}
|