use wip_protocol::{InterfaceReference, InterfaceTarget}; use wip_tui::reference::{format_interface_reference, format_qualified_operation}; fn reference(scope: &str, name: &str) -> InterfaceReference { InterfaceReference { scope: scope.into(), name: name.into(), } } #[test] fn specification_reference_examples() { for (scope, name, operation, expected) in [ ("/github", "issue.read", "read", "/github::issue.read#read"), ("/", "issue.read", "read", "/::issue.read#read"), ( "/github::archive", "issue.read", "read", "\"/github::archive\"::issue.read#read", ), ( "/github", "issue#read", "read", "/github::\"issue#read\"#read", ), ( "/github", "issue.read", "read#raw", "/github::issue.read#\"read#raw\"", ), ( "/開発", "課題.read", "read", "\"/開発\"::\"課題.read\"#read", ), ] { let input = reference(scope, name); input.validate().unwrap(); let before = input.clone(); assert_eq!( format_qualified_operation(&input, operation).unwrap(), expected ); assert_eq!(input, before); } } #[test] fn root_self_and_same_name_different_scope_stay_distinct() { let root = reference("/", "read"); let parent = reference("/a", "read"); let self_scope = reference("/a/b", "read"); let labels: Vec<_> = [&root, &parent, &self_scope] .into_iter() .map(|input| { input.validate_for_path("/a/b").unwrap(); format_interface_reference(input).unwrap() }) .collect(); assert_eq!(labels, ["/::read", "/a::read", "/a/b::read"]); assert_eq!( format_qualified_operation(&self_scope, "read").unwrap(), "/a/b::read#read" ); assert_ne!(root, self_scope); } #[test] fn components_not_declaration_identifiers_or_signatures() { // Reference components follow their own ASCII rules, not identifier/keyword // backtick quoting or the outer JSON string quoting used by full signatures. let input = reference("/a-b/c_d.1", "1-name.entry"); assert_eq!( format_interface_reference(&input).unwrap(), "/a-b/c_d.1::1-name.entry" ); assert_eq!( format_qualified_operation(&input, "operation").unwrap(), "/a-b/c_d.1::1-name.entry#operation" ); assert_eq!( format_qualified_operation(&input, "a/b").unwrap(), "/a-b/c_d.1::1-name.entry#\"a/b\"" ); assert_eq!( format_qualified_operation(&input, "").unwrap(), "/a-b/c_d.1::1-name.entry#\"\"" ); assert_eq!( format_interface_reference(&reference("/", "name`tick")).unwrap(), "/::\"name`tick\"" ); } #[test] fn delimiters_quote_backslash_whitespace_and_newline_are_component_data() { let input = reference("/a::b#c\"\\\n", "x::y#z\"\\\n"); let before = input.clone(); assert_eq!( format_interface_reference(&input).unwrap(), "\"/a::b#c\\\"\\\\\\n\"::\"x::y#z\\\"\\\\\\n\"" ); assert_eq!( format_qualified_operation(&input, "read::raw# \"\\\n\t").unwrap(), "\"/a::b#c\\\"\\\\\\n\"::\"x::y#z\\\"\\\\\\n\"#\"read::raw# \\\"\\\\\\n\\t\"" ); assert_eq!(input, before); assert_eq!( format_interface_reference(&reference("/space here", " name ")).unwrap(), "\"/space here\"::\" name \"" ); } #[test] fn every_required_safety_escape_in_each_component() { let controls = (0..=0x1f) .chain(0x7f..=0x9f) .chain([0x061c, 0x200e, 0x200f, 0x2028, 0x2029]) .chain(0x202a..=0x202e) .chain(0x2066..=0x2069); for code in controls { let ch = char::from_u32(code).unwrap(); let escape = match code { 8 => "\\b".into(), 9 => "\\t".into(), 10 => "\\n".into(), 12 => "\\f".into(), 13 => "\\r".into(), _ => format!("\\u{code:04x}"), }; let input = reference(&format!("/a{ch}z"), &format!("n{ch}z")); let operation = format!("o{ch}z"); let before = (input.clone(), operation.clone()); let label = format_qualified_operation(&input, &operation).unwrap(); assert_eq!( label, format!("\"/a{escape}z\"::\"n{escape}z\"#\"o{escape}z\""), "U+{code:04X}" ); assert!(!label.contains(ch), "raw U+{code:04X} leaked"); assert_eq!((input, operation), before); } } #[test] fn unicode_remains_exact_no_normalization_case_folding_or_url_encoding() { for name in ["é", "e\u{301}", "課題", "λ😀", "%23"] { let input = reference(&format!("/{name}"), name); assert_eq!( format_qualified_operation(&input, name).unwrap(), format!("\"/{name}\"::\"{name}\"#\"{name}\"") ); } assert_ne!( format_interface_reference(&reference("/", "é")).unwrap(), format_interface_reference(&reference("/", "e\u{301}")).unwrap() ); assert_ne!( format_interface_reference(&reference("/", "Read")).unwrap(), format_interface_reference(&reference("/", "read")).unwrap() ); } #[test] fn rejects_invalid_shape_instead_of_inferring_scope() { for scope in [ "", "github", "../github", "/a/../b", "/a/./b", "/a//b", "/a/", ] { let input = reference(scope, "read"); assert_eq!( format_interface_reference(&input).unwrap_err(), input.validate().unwrap_err() ); assert!(format_qualified_operation(&input, "read").is_err()); } assert!(format_interface_reference(&reference("/", "")).is_err()); } #[test] fn runtime_metadata_and_call_identity_stay_separate_from_display() { let mut target = InterfaceTarget { reference: reference("/a", "read#raw"), scope_ref: Some("secret-scope-ref".into()), validator: Some(vec![27, 255]), }; let before = target.clone(); let label = format_qualified_operation(&target.reference, "run").unwrap(); assert_eq!(label, "/a::\"read#raw\"#run"); assert_eq!(target, before); target.scope_ref = None; target.validator = None; assert_eq!( format_qualified_operation(&target.reference, "run").unwrap(), label ); // Selection/call keys are still exact protocol pairs; labels are disposable. let mut selected = std::collections::BTreeMap::new(); selected.insert(target.reference.clone(), "run"); assert_eq!(selected.get(&before.reference), Some(&"run")); assert!(!selected.contains_key(&reference("/", "read#raw"))); }