Files
Hare 4544bc0d8b feat: add scoped reference formatters for the TUI
Follow Text View reference notation with component quoting and complete JSON/control/bidi safety escapes. Keep protocol reference pairs and operation names separate from display labels; screen integration remains T-23.

Validated: cargo fmt --all --check; cargo check -p wip-tui --all-targets; cargo test -p wip-tui -p wip-protocol -p wip-client (99 tests incl. doctests); nix develop --command cargo clippy -p wip-tui --all-targets -- -D warnings; RUSTDOCFLAGS=-Dwarnings cargo doc -p wip-tui --no-deps; git diff --check.
2026-10-08 02:56:26 +09:00

212 lines
6.7 KiB
Rust

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")));
}