1use std::cmp::Ordering;
16use std::fmt;
17use std::str::FromStr;
18
19use crate::agent_id::AgentId;
20use crate::capability_path::CapabilityPath;
21use crate::constants::{MAX_URI_LENGTH, SCHEME};
22use crate::error::{ParseError, ParseErrorKind};
23use crate::fragment::Fragment;
24use crate::query::QueryParams;
25use crate::trust_root::TrustRoot;
26
27#[derive(Debug, Clone)]
118pub struct AgentUri {
119 trust_root: TrustRoot,
120 capability_path: CapabilityPath,
121 agent_id: AgentId,
122 query: QueryParams,
123 fragment: Option<Fragment>,
124 normalized: String,
126}
127
128impl PartialEq for AgentUri {
134 fn eq(&self, other: &Self) -> bool {
135 self.trust_root == other.trust_root
136 && self.capability_path == other.capability_path
137 && self.agent_id == other.agent_id
138 }
139}
140
141impl Eq for AgentUri {}
142
143impl std::hash::Hash for AgentUri {
147 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
148 self.trust_root.hash(state);
149 self.capability_path.hash(state);
150 self.agent_id.hash(state);
151 }
152}
153
154impl AgentUri {
155 pub fn parse(input: &str) -> Result<Self, ParseError> {
185 Self::parse_inner(input).map_err(|kind| ParseError::new(input, kind))
186 }
187
188 pub fn new(
194 trust_root: TrustRoot,
195 capability_path: CapabilityPath,
196 agent_id: AgentId,
197 query: QueryParams,
198 fragment: Option<Fragment>,
199 ) -> Result<Self, ParseError> {
200 let normalized = Self::normalize(
201 &trust_root,
202 &capability_path,
203 &agent_id,
204 &query,
205 fragment.as_ref(),
206 );
207 let len = normalized.len();
208
209 if len > MAX_URI_LENGTH {
210 return Err(ParseError::new(
211 &normalized,
212 ParseErrorKind::TooLong {
213 max: MAX_URI_LENGTH,
214 actual: len,
215 },
216 ));
217 }
218
219 Ok(Self {
220 trust_root,
221 capability_path,
222 agent_id,
223 query,
224 fragment,
225 normalized,
226 })
227 }
228
229 #[must_use]
231 pub const fn trust_root(&self) -> &TrustRoot {
232 &self.trust_root
233 }
234
235 #[must_use]
237 pub const fn capability_path(&self) -> &CapabilityPath {
238 &self.capability_path
239 }
240
241 #[must_use]
243 pub const fn agent_id(&self) -> &AgentId {
244 &self.agent_id
245 }
246
247 #[must_use]
249 pub const fn query(&self) -> &QueryParams {
250 &self.query
251 }
252
253 #[must_use]
255 pub const fn fragment(&self) -> Option<&Fragment> {
256 self.fragment.as_ref()
257 }
258
259 #[must_use]
261 pub fn as_str(&self) -> &str {
262 &self.normalized
263 }
264
265 #[must_use]
267 pub fn canonical(&self) -> String {
268 format!(
269 "{SCHEME}://{}/{}/{}",
270 self.trust_root, self.capability_path, self.agent_id
271 )
272 }
273
274 #[must_use]
276 pub fn is_localhost(&self) -> bool {
277 self.trust_root.is_localhost()
278 }
279
280 pub fn with_query(&self, query: QueryParams) -> Result<Self, ParseError> {
297 Self::new(
298 self.trust_root.clone(),
299 self.capability_path.clone(),
300 self.agent_id.clone(),
301 query,
302 self.fragment.clone(),
303 )
304 }
305
306 pub fn with_query_str(&self, s: &str) -> Result<Self, ParseError> {
324 let query = QueryParams::parse(s)
325 .map_err(|e| ParseError::new(s, ParseErrorKind::InvalidQuery(e)))?;
326 self.with_query(query)
327 }
328
329 pub fn without_query(&self) -> Result<Self, ParseError> {
346 self.with_query(QueryParams::new())
347 }
348
349 pub fn with_fragment(&self, fragment: Fragment) -> Result<Self, ParseError> {
366 Self::new(
367 self.trust_root.clone(),
368 self.capability_path.clone(),
369 self.agent_id.clone(),
370 self.query.clone(),
371 Some(fragment),
372 )
373 }
374
375 pub fn with_fragment_str(&self, s: &str) -> Result<Self, ParseError> {
392 let fragment = Fragment::parse(s)
393 .map_err(|e| ParseError::new(s, ParseErrorKind::InvalidFragment(e)))?;
394 self.with_fragment(fragment)
395 }
396
397 pub fn without_fragment(&self) -> Result<Self, ParseError> {
414 Self::new(
415 self.trust_root.clone(),
416 self.capability_path.clone(),
417 self.agent_id.clone(),
418 self.query.clone(),
419 None,
420 )
421 }
422
423 fn parse_inner(input: &str) -> Result<Self, ParseErrorKind> {
424 if input.is_empty() {
425 return Err(ParseErrorKind::Empty);
426 }
427
428 if input.len() > MAX_URI_LENGTH {
429 return Err(ParseErrorKind::TooLong {
430 max: MAX_URI_LENGTH,
431 actual: input.len(),
432 });
433 }
434
435 let scheme_prefix = format!("{SCHEME}://");
441 let scheme_matches = input
442 .get(..scheme_prefix.len())
443 .is_some_and(|found| found.eq_ignore_ascii_case(&scheme_prefix));
444 if !scheme_matches {
445 let found = input.split("://").next().map(str::to_string);
446 return Err(ParseErrorKind::InvalidScheme { found });
447 }
448 let rest = &input[scheme_prefix.len()..];
449
450 let (rest, fragment) = Self::split_fragment(rest)?;
452
453 let (rest, query) = Self::split_query(rest)?;
455
456 let (trust_root_str, path_with_id) = Self::split_trust_root(rest)?;
458
459 let trust_root =
461 TrustRoot::parse(trust_root_str).map_err(ParseErrorKind::InvalidTrustRoot)?;
462
463 let (cap_path_str, agent_id_str) = Self::split_path_and_id(path_with_id)?;
465
466 let capability_path =
468 CapabilityPath::parse(cap_path_str).map_err(ParseErrorKind::InvalidCapabilityPath)?;
469
470 let agent_id = AgentId::parse(agent_id_str).map_err(ParseErrorKind::InvalidAgentId)?;
472
473 let normalized = Self::normalize(
474 &trust_root,
475 &capability_path,
476 &agent_id,
477 &query,
478 fragment.as_ref(),
479 );
480
481 Ok(Self {
482 trust_root,
483 capability_path,
484 agent_id,
485 query,
486 fragment,
487 normalized,
488 })
489 }
490
491 fn split_fragment(input: &str) -> Result<(&str, Option<Fragment>), ParseErrorKind> {
492 if let Some(hash_idx) = input.find('#') {
493 let rest = &input[..hash_idx];
494 let frag_str = &input[hash_idx + 1..];
495 if frag_str.is_empty() {
496 Ok((rest, None))
498 } else {
499 let fragment =
500 Fragment::parse(frag_str).map_err(ParseErrorKind::InvalidFragment)?;
501 Ok((rest, Some(fragment)))
502 }
503 } else {
504 Ok((input, None))
505 }
506 }
507
508 fn split_query(input: &str) -> Result<(&str, QueryParams), ParseErrorKind> {
509 if let Some(q_idx) = input.find('?') {
510 let rest = &input[..q_idx];
511 let query_str = &input[q_idx + 1..];
512 if query_str.is_empty() {
513 Ok((rest, QueryParams::new())) } else {
515 let query = QueryParams::parse(query_str).map_err(ParseErrorKind::InvalidQuery)?;
516 Ok((rest, query))
517 }
518 } else {
519 Ok((input, QueryParams::new()))
520 }
521 }
522
523 fn split_trust_root(input: &str) -> Result<(&str, &str), ParseErrorKind> {
524 let slash_idx = input.find('/').ok_or(ParseErrorKind::MissingComponent {
526 component: "capability path",
527 })?;
528
529 let trust_root = &input[..slash_idx];
530 let path = &input[slash_idx + 1..];
531
532 if trust_root.is_empty() {
533 return Err(ParseErrorKind::MissingComponent {
534 component: "trust root",
535 });
536 }
537
538 if path.is_empty() {
539 return Err(ParseErrorKind::MissingComponent {
540 component: "capability path",
541 });
542 }
543
544 Ok((trust_root, path))
545 }
546
547 fn split_path_and_id(input: &str) -> Result<(&str, &str), ParseErrorKind> {
548 let last_slash_idx = input.rfind('/').ok_or(ParseErrorKind::MissingComponent {
550 component: "agent ID",
551 })?;
552
553 let path = &input[..last_slash_idx];
554 let agent_id = &input[last_slash_idx + 1..];
555
556 if path.is_empty() {
557 return Err(ParseErrorKind::MissingComponent {
558 component: "capability path",
559 });
560 }
561
562 if agent_id.is_empty() {
563 return Err(ParseErrorKind::MissingComponent {
564 component: "agent ID",
565 });
566 }
567
568 Ok((path, agent_id))
569 }
570
571 fn normalize(
572 trust_root: &TrustRoot,
573 capability_path: &CapabilityPath,
574 agent_id: &AgentId,
575 query: &QueryParams,
576 fragment: Option<&Fragment>,
577 ) -> String {
578 let mut result = format!("{SCHEME}://{trust_root}/{capability_path}/{agent_id}");
579
580 if !query.is_empty() {
581 result.push('?');
582 result.push_str(&query.to_string());
583 }
584
585 if let Some(frag) = fragment {
586 result.push('#');
587 result.push_str(frag.as_str());
588 }
589
590 result
591 }
592}
593
594impl fmt::Display for AgentUri {
595 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596 write!(f, "{}", self.normalized)
597 }
598}
599
600impl FromStr for AgentUri {
601 type Err = ParseError;
602
603 fn from_str(s: &str) -> Result<Self, Self::Err> {
604 Self::parse(s)
605 }
606}
607
608impl AsRef<str> for AgentUri {
609 fn as_ref(&self) -> &str {
610 &self.normalized
611 }
612}
613
614impl TryFrom<&str> for AgentUri {
615 type Error = ParseError;
616
617 fn try_from(s: &str) -> Result<Self, Self::Error> {
618 Self::parse(s)
619 }
620}
621
622impl PartialOrd for AgentUri {
623 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
624 Some(self.cmp(other))
625 }
626}
627
628impl Ord for AgentUri {
640 fn cmp(&self, other: &Self) -> Ordering {
641 self.trust_root
642 .cmp(&other.trust_root)
643 .then_with(|| self.capability_path.cmp(&other.capability_path))
644 .then_with(|| self.agent_id.cmp(&other.agent_id))
645 }
646}
647
648#[cfg(feature = "serde")]
649impl serde::Serialize for AgentUri {
650 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
651 where
652 S: serde::Serializer,
653 {
654 serializer.serialize_str(&self.normalized)
655 }
656}
657
658#[cfg(feature = "serde")]
659impl<'de> serde::Deserialize<'de> for AgentUri {
660 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
661 where
662 D: serde::Deserializer<'de>,
663 {
664 let s = String::deserialize(deserializer)?;
665 Self::parse(&s).map_err(serde::de::Error::custom)
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672 use crate::error::{FragmentError, TrustRootError};
673
674 fn uri(input: &str) -> AgentUri {
676 AgentUri::parse(input).unwrap_or_else(|e| panic!("{input} should parse: {e}"))
677 }
678
679 #[test]
680 fn parse_valid_uri() {
681 let input = "agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q";
682 let uri = AgentUri::parse(input).unwrap();
683
684 assert_eq!(uri.trust_root().host_str(), "anthropic.com");
685 assert_eq!(uri.capability_path().as_str(), "assistant/chat");
686 assert_eq!(uri.agent_id().prefix().as_str(), "llm_chat");
687 }
688
689 #[test]
690 fn parse_empty_returns_error() {
691 let result = AgentUri::parse("");
692 assert!(matches!(
693 result,
694 Err(ParseError {
695 kind: ParseErrorKind::Empty,
696 ..
697 })
698 ));
699 }
700
701 #[test]
702 fn parse_too_long_returns_error() {
703 let long_path = "a".repeat(500);
704 let input = format!("agent://x.com/{long_path}/llm_01h455vb4pex5vsknk084sn02q");
705 let result = AgentUri::parse(&input);
706 assert!(matches!(
707 result,
708 Err(ParseError {
709 kind: ParseErrorKind::TooLong { .. },
710 ..
711 })
712 ));
713 }
714
715 #[test]
716 fn scheme_case_is_folded() {
717 for input in [
719 "AGENT://example.com/chat/llm_01h455vb4pex5vsknk084sn02q",
720 "AgEnT://example.com/chat/llm_01h455vb4pex5vsknk084sn02q",
721 "Agent://example.com/chat/llm_01h455vb4pex5vsknk084sn02q",
722 ] {
723 let uri = AgentUri::parse(input).unwrap();
724 assert_eq!(
725 uri.as_str(),
726 "agent://example.com/chat/llm_01h455vb4pex5vsknk084sn02q"
727 );
728 }
729 }
730
731 #[test]
732 fn folding_the_scheme_does_not_fold_anything_after_it() {
733 let uri =
736 AgentUri::parse("AGENT://Anthropic.COM/chat/llm_01h455vb4pex5vsknk084sn02q").unwrap();
737 assert_eq!(
738 uri.as_str(),
739 "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q"
740 );
741
742 assert!(
743 AgentUri::parse("AGENT://example.com/Chat/llm_01h455vb4pex5vsknk084sn02q").is_err()
744 );
745 assert!(
746 AgentUri::parse("AGENT://example.com/chat/LLM_01H455VB4PEX5VSKNK084SN02Q").is_err()
747 );
748 }
749
750 #[test]
751 fn a_scheme_shorter_than_the_prefix_is_an_invalid_scheme() {
752 assert!(matches!(
755 AgentUri::parse("agent:/"),
756 Err(ParseError {
757 kind: ParseErrorKind::InvalidScheme { .. },
758 ..
759 })
760 ));
761 assert!(matches!(
762 AgentUri::parse("agent:/\u{00e9}x/y/llm_01h455vb4pex5vsknk084sn02q"),
763 Err(ParseError {
764 kind: ParseErrorKind::InvalidScheme { .. },
765 ..
766 })
767 ));
768 }
769
770 #[test]
771 fn parse_wrong_scheme_returns_error() {
772 let result = AgentUri::parse("http://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q");
773 assert!(matches!(
774 result,
775 Err(ParseError {
776 kind: ParseErrorKind::InvalidScheme { .. },
777 ..
778 })
779 ));
780 }
781
782 #[test]
783 fn parse_with_query_params() {
784 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0&ttl=300";
785 let uri = AgentUri::parse(input).unwrap();
786
787 assert_eq!(uri.query().version(), Some("2.0"));
788 assert_eq!(uri.query().ttl(), Some(300));
789 }
790
791 #[test]
792 fn parse_with_fragment() {
793 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#summarization";
794 let uri = AgentUri::parse(input).unwrap();
795
796 assert_eq!(
797 uri.fragment().map(crate::Fragment::as_str),
798 Some("summarization")
799 );
800 }
801
802 #[test]
803 fn empty_query_is_stripped() {
804 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?";
805 let uri = AgentUri::parse(input).unwrap();
806 assert!(uri.query().is_empty());
807 }
808
809 #[test]
810 fn empty_fragment_is_stripped() {
811 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#";
812 let uri = AgentUri::parse(input).unwrap();
813 assert!(uri.fragment().is_none());
814 }
815
816 #[test]
817 fn with_fragment_str_rejects_empty_fragment() {
818 let uri =
819 AgentUri::parse("agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q").unwrap();
820
821 let result = uri.with_fragment_str("");
822
823 assert!(matches!(
824 result,
825 Err(ParseError {
826 kind: ParseErrorKind::InvalidFragment(FragmentError::Empty),
827 ..
828 })
829 ));
830 }
831
832 #[test]
833 fn uri_with_fragment_round_trips_through_as_str() {
834 let uri = AgentUri::parse("agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q")
835 .unwrap()
836 .with_fragment_str("sub/task:v2")
837 .unwrap();
838
839 assert!(uri.as_str().ends_with("#sub/task:v2"));
840
841 let reparsed = AgentUri::parse(uri.as_str()).unwrap();
842 assert_eq!(
843 reparsed.fragment().map(Fragment::as_str),
844 Some("sub/task:v2")
845 );
846 assert_eq!(reparsed.as_str(), uri.as_str());
847 }
848
849 #[test]
850 fn parsed_uri_never_renders_a_bare_hash() {
851 let uri =
852 AgentUri::parse("agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q#").unwrap();
853
854 assert!(uri.fragment().is_none());
855 assert!(!uri.as_str().ends_with('#'));
856
857 let reparsed = AgentUri::parse(uri.as_str()).unwrap();
858 assert_eq!(reparsed.as_str(), uri.as_str());
859 }
860
861 #[test]
862 fn canonical_strips_query_and_fragment() {
863 let input = "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0#test";
864 let uri = AgentUri::parse(input).unwrap();
865 let canonical = uri.canonical();
866
867 assert!(!canonical.contains('?'));
868 assert!(!canonical.contains('#'));
869 }
870
871 #[test]
872 fn query_and_fragment_do_not_change_identity_equality() {
873 let base =
874 AgentUri::parse("agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q").unwrap();
875 let decorated = AgentUri::parse(
876 "agent://anthropic.com/chat/llm_01h455vb4pex5vsknk084sn02q?version=2.0#task",
877 )
878 .unwrap();
879
880 assert_eq!(base, decorated);
881 assert_eq!(base.cmp(&decorated), Ordering::Equal);
882 assert_eq!(hash_of(&base), hash_of(&decorated));
883 }
884
885 fn hash_of(uri: &AgentUri) -> u64 {
886 use std::hash::{Hash, Hasher};
887
888 let mut hasher = std::collections::hash_map::DefaultHasher::new();
889 uri.hash(&mut hasher);
890 hasher.finish()
891 }
892
893 #[test]
894 fn equal_uris_hash_equally_and_key_one_entry() {
895 use std::collections::HashSet;
896
897 let base =
898 AgentUri::parse("agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02q").unwrap();
899 let decorated =
900 AgentUri::parse("agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02q?ttl=300")
901 .unwrap();
902
903 let mut set = HashSet::new();
904 assert!(set.insert(base.clone()));
905 assert!(!set.insert(decorated));
906 assert!(set.contains(&base));
907 assert_eq!(set.len(), 1);
908 }
909
910 #[test]
911 fn different_agents_do_not_share_a_hash_bucket_entry() {
912 use std::collections::HashSet;
913
914 let one =
915 AgentUri::parse("agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02q").unwrap();
916 let other =
917 AgentUri::parse("agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02r").unwrap();
918 let elsewhere =
919 AgentUri::parse("agent://other.com/workflow/rule_01h455vb4pex5vsknk084sn02q").unwrap();
920
921 assert_ne!(one, other);
922 assert_ne!(one, elsewhere);
923
924 let set: HashSet<_> = [one, other, elsewhere].into_iter().collect();
925 assert_eq!(set.len(), 3);
926 }
927
928 #[test]
929 fn hashing_ignores_exactly_what_equality_ignores() {
930 let base =
933 AgentUri::parse("agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02q").unwrap();
934
935 for variant in [
936 "agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02q?ttl=300",
937 "agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02q#invoice",
938 "agent://acme.com/workflow/rule_01h455vb4pex5vsknk084sn02q?v=1&ttl=9#x",
939 "agent://ACME.COM/workflow/rule_01h455vb4pex5vsknk084sn02q",
940 ] {
941 let variant = AgentUri::parse(variant).unwrap();
942 assert_eq!(base, variant, "{variant} should be the same identity");
943 assert_eq!(
944 hash_of(&base),
945 hash_of(&variant),
946 "{variant} is equal, so it must hash equally"
947 );
948 }
949 }
950
951 #[test]
952 fn is_localhost() {
953 let uri =
954 AgentUri::parse("agent://localhost:8472/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
955 assert!(uri.is_localhost());
956
957 let uri = AgentUri::parse("agent://127.0.0.1/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
958 assert!(uri.is_localhost());
959
960 let uri =
961 AgentUri::parse("agent://anthropic.com/test/llm_01h455vb4pex5vsknk084sn02q").unwrap();
962 assert!(!uri.is_localhost());
963 }
964
965 #[test]
966 fn parse_missing_capability_path() {
967 let result = AgentUri::parse("agent://anthropic.com/llm_01h455vb4pex5vsknk084sn02q");
968 assert!(matches!(
969 result,
970 Err(ParseError {
971 kind: ParseErrorKind::MissingComponent {
972 component: "agent ID"
973 },
974 ..
975 })
976 ));
977 }
978
979 #[test]
980 fn display_roundtrip() {
981 let input = "agent://anthropic.com/assistant/chat/llm_chat_01h455vb4pex5vsknk084sn02q";
982 let uri = AgentUri::parse(input).unwrap();
983 assert_eq!(uri.to_string(), input);
984 }
985
986 #[test]
987 fn new_from_components() {
988 let trust_root = TrustRoot::parse("anthropic.com").unwrap();
989 let cap_path = CapabilityPath::parse("assistant/chat").unwrap();
990 let agent_id = AgentId::parse("llm_01h455vb4pex5vsknk084sn02q").unwrap();
991
992 let uri = AgentUri::new(trust_root, cap_path, agent_id, QueryParams::new(), None).unwrap();
993
994 assert_eq!(uri.trust_root().host_str(), "anthropic.com");
995 assert_eq!(uri.capability_path().as_str(), "assistant/chat");
996 }
997
998 #[test]
999 fn ipv4_shaped_trust_root_with_out_of_range_octet_is_rejected() {
1000 let result = AgentUri::parse("agent://256.1.1.1/test/llm_01h455vb4pex5vsknk084sn02q");
1001
1002 assert!(matches!(
1003 result,
1004 Err(ParseError {
1005 kind: ParseErrorKind::InvalidTrustRoot(TrustRootError::InvalidIpAddress { .. }),
1006 ..
1007 })
1008 ));
1009 }
1010
1011 #[test]
1012 fn ordering_agrees_with_identity_equality() {
1013 let plain = uri("agent://acme.com/chat/llm_01h455vb4pex5vsknk084sn02q");
1016 let hinted = uri("agent://acme.com/chat/llm_01h455vb4pex5vsknk084sn02q?ttl=300#part");
1017
1018 assert_eq!(plain.cmp(&hinted), Ordering::Equal);
1019 assert_eq!(plain, hinted);
1020 }
1021
1022 #[test]
1023 fn ordering_walks_the_components_in_the_order_a_uri_writes_them() {
1024 let by_root = uri("agent://b.com/chat/llm_01h455vb4pex5vsknk084sn02q");
1025 let by_path = uri("agent://a.com/zeta/llm_01h455vb4pex5vsknk084sn02q");
1026 let by_id = uri("agent://a.com/chat/rule_01h455vb4pex5vsknk084sn02q");
1027 let first = uri("agent://a.com/chat/llm_01h455vb4pex5vsknk084sn02q");
1028
1029 assert!(first < by_id);
1031 assert!(by_id < by_path);
1032 assert!(by_path < by_root);
1033 }
1034
1035 #[test]
1036 fn ordering_is_by_component_and_not_by_the_rendered_string() {
1037 let shorter = uri("agent://acme.com/chat/llm_01h455vb4pex5vsknk084sn02q");
1043 let longer = uri("agent://acme.com.br/chat/llm_01h455vb4pex5vsknk084sn02q");
1044
1045 assert!(shorter < longer);
1046 assert!(longer.canonical() < shorter.canonical());
1047 }
1048
1049 #[test]
1050 fn ordering_is_a_total_order() {
1051 let uris = [
1052 uri("agent://a.com/chat/llm_01h455vb4pex5vsknk084sn02q"),
1053 uri("agent://a.com.br/chat/llm_01h455vb4pex5vsknk084sn02q"),
1054 uri("agent://a.com/chat/rule_01h455vb4pex5vsknk084sn02q"),
1055 uri("agent://b.com/zeta/deep/human_01h455vb4pex5vsknk084sn02q"),
1056 ];
1057
1058 for left in &uris {
1059 for right in &uris {
1060 assert_eq!(
1061 left.cmp(right).reverse(),
1062 right.cmp(left),
1063 "{left} vs {right} is not antisymmetric"
1064 );
1065 assert_eq!(left.cmp(right) == Ordering::Equal, left == right);
1066 }
1067 }
1068 }
1069
1070 #[test]
1071 fn a_rejected_input_too_large_to_be_a_uri_is_not_copied_into_the_error() {
1072 let hostile = format!("agent://{}", "a".repeat(2 * 1024 * 1024));
1075
1076 let Err(error) = AgentUri::parse(&hostile) else {
1077 panic!("a two-megabyte input must not parse");
1078 };
1079
1080 assert!(error.input.len() < 1024, "the error kept the whole input");
1081 assert!(matches!(error.kind, ParseErrorKind::TooLong { .. }));
1082 }
1083
1084 #[test]
1085 fn a_rejected_input_short_enough_to_have_been_a_uri_is_kept_whole() {
1086 let input = "agent://acme.com/Chat/llm_01h455vb4pex5vsknk084sn02q";
1087
1088 let Err(error) = AgentUri::parse(input) else {
1089 panic!("an uppercase path segment must not parse");
1090 };
1091
1092 assert_eq!(error.input, input);
1093 }
1094}