//! Battery 8: ConceptPath Parsing and Source Class Inference. //! //! Tests ConceptPath parsing, roundtrip, and scheme-based source class inference. //! //! # Test Coverage //! //! | Test | Feature | Validates | //! |------|---------|-----------| //! | `test_concept_path_parse_full` | Full wire format | scheme://seg1/seg2/seg3 | //! | `test_concept_path_backward_compat` | Bare strings | Maps to custom:// scheme | //! | `test_concept_path_roundtrip` | parse → to_wire_format → parse | Identity | //! | `test_concept_path_prefix_matching` | is_prefix_of | Hierarchical matching | //! | `test_source_scheme_inference` | scheme → SourceClass | Tier mapping | #![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages use stemedb_core::types::{ConceptPath, SourceClass, SourceScheme}; /// Test 8.1: Parse ConceptPath from full wire format. /// /// Wire format: `scheme://segment1/segment2/segment3` /// /// Verify: /// - scheme is extracted correctly /// - segments are split on "/" /// - leaf() returns last segment /// - parent() returns path without last segment #[test] fn test_concept_path_parse_full() { // Full hierarchical path let path = ConceptPath::parse("code://rust/citadeldb/auth/jwt/aud_validation") .expect("parse full path"); assert_eq!(path.scheme, "code"); assert_eq!(path.segments, vec!["rust", "citadeldb", "auth", "jwt", "aud_validation"]); assert_eq!(path.leaf(), "aud_validation"); // Test parent let parent = path.parent().expect("has parent"); assert_eq!(parent.scheme, "code"); assert_eq!(parent.segments, vec!["rust", "citadeldb", "auth", "jwt"]); assert_eq!(parent.leaf(), "jwt"); // Test grandparent let grandparent = parent.parent().expect("has grandparent"); assert_eq!(grandparent.leaf(), "auth"); } /// Test 8.2: Parse bare string (backward compatibility). /// /// Bare strings without `://` should map to `custom://` scheme. /// /// This ensures backward compatibility with pre-ConceptPath subjects. #[test] fn test_concept_path_backward_compat() { // Bare string without scheme let path = ConceptPath::parse("Semaglutide").expect("parse bare string"); assert_eq!(path.scheme, "custom"); assert_eq!(path.segments, vec!["Semaglutide"]); assert_eq!(path.leaf(), "Semaglutide"); // No parent for single segment assert!(path.parent().is_none()); } /// Test 8.3: Roundtrip: parse → to_wire_format → parse. /// /// Verify identity property: parsing the wire format of a parsed path /// should yield an equivalent path. #[test] fn test_concept_path_roundtrip() { let test_cases = vec![ "code://rust/citadeldb/auth/jwt/aud_validation", "rfc://7519/jwt/audience_validation", "fda://drug/12345/indication", "custom://Semaglutide", ]; for original in test_cases { let parsed = ConceptPath::parse(original).expect("parse original"); let wire = parsed.to_wire_format(); let reparsed = ConceptPath::parse(&wire).expect("reparse wire format"); assert_eq!(parsed.scheme, reparsed.scheme, "scheme mismatch for {}", original); assert_eq!(parsed.segments, reparsed.segments, "segments mismatch for {}", original); } } /// Test 8.4: Prefix matching with is_prefix_of. /// /// Verify hierarchical prefix matching: /// - `code://rust/citadeldb/auth/` is prefix of `code://rust/citadeldb/auth/jwt/aud` /// - `code://rust/citadeldb/` is prefix of `code://rust/citadeldb/auth/jwt/aud` /// - Different schemes are NOT prefixes of each other #[test] fn test_concept_path_prefix_matching() { let jwt_path = ConceptPath::parse("code://rust/citadeldb/auth/jwt/aud").expect("parse jwt path"); let auth_prefix = ConceptPath::parse("code://rust/citadeldb/auth").expect("parse auth prefix"); let citadeldb_prefix = ConceptPath::parse("code://rust/citadeldb").expect("parse citadeldb prefix"); let rfc_path = ConceptPath::parse("rfc://7519/jwt/audience").expect("parse rfc path"); // auth is prefix of jwt assert!(auth_prefix.is_prefix_of(&jwt_path), "auth should be prefix of jwt"); // citadeldb is prefix of jwt assert!(citadeldb_prefix.is_prefix_of(&jwt_path), "citadeldb should be prefix of jwt"); // jwt is NOT prefix of auth (longer path) assert!(!jwt_path.is_prefix_of(&auth_prefix), "jwt should NOT be prefix of auth"); // different schemes are not prefixes assert!( !rfc_path.is_prefix_of(&jwt_path), "rfc should NOT be prefix of code (different scheme)" ); assert!( !jwt_path.is_prefix_of(&rfc_path), "code should NOT be prefix of rfc (different scheme)" ); } /// Test 8.5: Source scheme inference from scheme string. /// /// Verify tier mapping: /// - Tier 0 (Regulatory): rfc, nist, fda, sec /// - Tier 1 (Clinical): owasp, pubmed, doi /// - Tier 2 (Observational): vendor, cve /// - Tier 3 (Expert): internal, code, custom /// - Tier 4 (Community): community, wiki /// - Tier 5 (Anecdotal): blog, social #[test] fn test_source_scheme_inference() { // Tier 0: Regulatory assert_eq!(SourceScheme::parse("rfc").default_source_class(), SourceClass::Regulatory); assert_eq!(SourceScheme::parse("nist").default_source_class(), SourceClass::Regulatory); assert_eq!(SourceScheme::parse("fda").default_source_class(), SourceClass::Regulatory); assert_eq!(SourceScheme::parse("sec").default_source_class(), SourceClass::Regulatory); // Tier 1: Clinical assert_eq!(SourceScheme::parse("owasp").default_source_class(), SourceClass::Clinical); assert_eq!(SourceScheme::parse("pubmed").default_source_class(), SourceClass::Clinical); assert_eq!(SourceScheme::parse("doi").default_source_class(), SourceClass::Clinical); // Tier 2: Observational assert_eq!(SourceScheme::parse("vendor").default_source_class(), SourceClass::Observational); assert_eq!(SourceScheme::parse("cve").default_source_class(), SourceClass::Observational); // Tier 3: Expert assert_eq!(SourceScheme::parse("internal").default_source_class(), SourceClass::Expert); assert_eq!(SourceScheme::parse("code").default_source_class(), SourceClass::Expert); assert_eq!(SourceScheme::parse("custom").default_source_class(), SourceClass::Expert); // Tier 4: Community assert_eq!(SourceScheme::parse("community").default_source_class(), SourceClass::Community); assert_eq!(SourceScheme::parse("wiki").default_source_class(), SourceClass::Community); // Tier 5: Anecdotal assert_eq!(SourceScheme::parse("blog").default_source_class(), SourceClass::Anecdotal); assert_eq!(SourceScheme::parse("social").default_source_class(), SourceClass::Anecdotal); } /// Test 8.6: ConceptPath default_source_class from scheme. /// /// Verify ConceptPath.default_source_class() returns the correct tier /// based on the parsed scheme. #[test] fn test_concept_path_default_source_class() { let test_cases = vec![ ("rfc://7519/jwt/aud", SourceClass::Regulatory), ("fda://drug/12345", SourceClass::Regulatory), ("owasp://top10/a01", SourceClass::Clinical), ("pubmed://pmid/123456", SourceClass::Clinical), ("vendor://aws/s3/encryption", SourceClass::Observational), ("code://rust/citadeldb/auth", SourceClass::Expert), ("internal://policy/security", SourceClass::Expert), ("community://stackoverflow/q/123", SourceClass::Community), ("blog://medium/post/abc", SourceClass::Anecdotal), ("Semaglutide", SourceClass::Expert), // bare string → custom → Expert ]; for (path_str, expected_class) in test_cases { let path = ConceptPath::parse(path_str).expect("parse path"); assert_eq!( path.default_source_class(), expected_class, "path {} should have source class {:?}", path_str, expected_class ); } } /// Test 8.7: Edge cases for ConceptPath parsing. /// /// Verify handling of edge cases: /// - Empty path → error /// - Single segment with scheme → valid /// - Path with trailing slash → parsed without empty segment #[test] fn test_concept_path_edge_cases() { // Empty string should fail let empty_result = ConceptPath::parse(""); assert!(empty_result.is_err(), "empty string should fail parsing"); // Single segment with scheme let single = ConceptPath::parse("rfc://7519").expect("parse single segment"); assert_eq!(single.scheme, "rfc"); assert_eq!(single.segments, vec!["7519"]); assert_eq!(single.leaf(), "7519"); assert!(single.parent().is_none(), "single segment has no parent"); // Scheme only (no path) - maps to empty first segment let scheme_only = ConceptPath::parse("code://"); // This should either error or have an empty segments vec match scheme_only { Ok(path) => { // If it succeeds, segments should be empty or have one empty string assert!( path.segments.is_empty() || path.segments == vec![""], "scheme only should have no meaningful segments" ); } Err(_) => { // Error is also acceptable for scheme-only } } }