Skip to main content

hyperactor_mesh/
mesh_id.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Mesh identity types.
10//!
11//! [`ResourceId`] is the common control-plane identifier for mesh resources.
12//! The mesh-specific newtypes [`ActorMeshId`], [`ProcMeshId`], and
13//! [`HostMeshId`] provide type safety at mesh struct boundaries while
14//! converting freely to [`ResourceId`] for resource message plumbing.
15//!
16//! # Where resource ids are used
17//!
18//! `ResourceId` is the stable key used by resource messages in `resource.rs`
19//! (`GetRankStatus`, `WaitRankStatus`, `CreateOrUpdate`, `Stop`, `GetState`,
20//! `StreamState`, and `List`) and by the internal state maps in `proc_agent.rs`
21//! and `host_mesh/host_agent.rs`.
22//!
23//! The same logical resource may be rendered into other name spaces:
24//!
25//! - The control-plane resource name is `ResourceId::to_string()`.
26//! - The runtime actor id is carried as `ActorMeshId::uid()` by
27//!   `ProcRef::actor_id()` in `proc_mesh.rs` and by `ProcAgent` when it calls
28//!   `remote.gspawn(...)`.
29//! - The runtime proc name is `ResourceId::to_string()`, which is consumed by
30//!   `host_mesh.rs` and by `HostAgent` when it spawns a proc on a host.
31//! - Telemetry uses `display_label()` for human-facing `given_name`, while
32//!   `to_string()` is emitted as the stable `full_name`.
33//!
34//! # String formats
35//!
36//! `ResourceId` has two externally visible string forms:
37//!
38//! - Singleton: `label`
39//! - Labeled instance: `label-uid58`
40//!
41//! Here `uid58` is the base58 instance component produced by [`Uid::Instance`],
42//! without angle brackets. Instances always render with a label. When an
43//! instance has no explicit label metadata, the formatter uses the id type's
44//! default label, such as `proc`, `actor`, or `resource`.
45//!
46//! Identity is uid-only: labels are descriptive metadata and do not
47//! participate in `Eq`, `Hash`, or `Ord`.
48
49use std::cmp::Ordering;
50use std::fmt;
51use std::hash::Hash;
52use std::hash::Hasher;
53use std::str::FromStr;
54
55use hyperactor::ActorAddr;
56use hyperactor::ActorId;
57use hyperactor::Location;
58use hyperactor::ProcAddr;
59use hyperactor::ProcId;
60use hyperactor::id::Label;
61use hyperactor::id::LabelError;
62use hyperactor::id::Uid;
63use hyperactor::id::UidParseError;
64use serde::Deserialize;
65use serde::Serialize;
66use typeuri::Named;
67
68const RESOURCE_ID_DEFAULT_LABEL: &str = "resource";
69const HOST_MESH_ID_DEFAULT_LABEL: &str = "host";
70const PROC_MESH_ID_DEFAULT_LABEL: &str = "proc";
71const ACTOR_MESH_ID_DEFAULT_LABEL: &str = "actor";
72
73/// Errors that can occur when parsing a [`ResourceId`] from a string.
74#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
75pub enum ResourceIdParseError {
76    /// Error parsing the uid component.
77    #[error("invalid uid: {0}")]
78    InvalidUid(#[from] UidParseError),
79    /// Error parsing the label component.
80    #[error("invalid label: {0}")]
81    InvalidLabel(#[from] LabelError),
82}
83
84/// Identifies a resource in the mesh system.
85///
86/// Identity (Eq, Hash, Ord) is determined by the underlying [`Uid`].
87#[derive(Clone, Serialize, Deserialize, Named)]
88pub struct ResourceId(Uid);
89wirevalue::register_type!(ResourceId);
90
91impl ResourceId {
92    /// Create a [`ResourceId`] with explicit uid and label.
93    pub fn new(uid: Uid, label: Option<Label>) -> Self {
94        Self(uid.with_label(label))
95    }
96
97    /// Create a singleton [`ResourceId`] identified by label.
98    /// The label becomes the uid; no separate label metadata is stored.
99    pub fn singleton(label: Label) -> Self {
100        Self(Uid::Singleton(label))
101    }
102
103    /// Create an instance [`ResourceId`] with a random uid and the given label.
104    pub fn instance(label: Label) -> Self {
105        Self(Uid::instance(label))
106    }
107
108    /// Create a resource id from a resource-name string.
109    ///
110    /// This accepts the mesh resource-id grammar, falling back to a stripped
111    /// singleton label for legacy call sites that pass arbitrary names.
112    pub fn from_name(name: impl AsRef<str>) -> Self {
113        name.as_ref()
114            .parse()
115            .unwrap_or_else(|_| Self::singleton(Label::strip(name.as_ref())))
116    }
117
118    /// Returns the uid.
119    pub fn uid(&self) -> &Uid {
120        &self.0
121    }
122
123    /// Returns the explicit label metadata, if any.
124    pub fn label(&self) -> Option<&Label> {
125        match &self.0 {
126            Uid::Singleton(_) => None,
127            Uid::Instance(_, label) => label.as_ref(),
128        }
129    }
130
131    /// Returns the human-facing label for this resource id.
132    ///
133    /// This is the explicit label metadata for instances, or the singleton
134    /// label embedded in the uid. Telemetry uses this for `given_name`.
135    pub fn display_label(&self) -> Option<&Label> {
136        self.0.label()
137    }
138
139    /// Converts this resource id into a hyperactor proc id.
140    pub fn proc_id(&self) -> ProcId {
141        ProcId::new(self.0.clone(), None)
142    }
143
144    /// Converts this resource id into a hyperactor proc addr at `location`.
145    pub fn proc_addr(&self, location: impl Into<Location>) -> ProcAddr {
146        ProcAddr::new(self.proc_id(), location.into())
147    }
148
149    /// Creates a hyperactor proc addr from a mesh resource-name string.
150    pub fn proc_addr_from_name(location: impl Into<Location>, name: impl AsRef<str>) -> ProcAddr {
151        Self::from_name(name).proc_addr(location)
152    }
153}
154
155impl From<ResourceId> for Uid {
156    fn from(id: ResourceId) -> Self {
157        id.0
158    }
159}
160
161impl From<&ResourceId> for Uid {
162    fn from(id: &ResourceId) -> Self {
163        id.0.clone()
164    }
165}
166
167impl From<ResourceId> for ProcId {
168    fn from(id: ResourceId) -> Self {
169        Self::new(id.0, None)
170    }
171}
172
173impl From<&ResourceId> for ProcId {
174    fn from(id: &ResourceId) -> Self {
175        id.proc_id()
176    }
177}
178
179impl From<ProcId> for ResourceId {
180    fn from(id: ProcId) -> Self {
181        Self(id.uid().clone())
182    }
183}
184
185impl From<&ProcId> for ResourceId {
186    fn from(id: &ProcId) -> Self {
187        Self(id.uid().clone())
188    }
189}
190
191impl From<&ProcAddr> for ResourceId {
192    fn from(addr: &ProcAddr) -> Self {
193        Self::from(addr.id())
194    }
195}
196
197impl PartialEq for ResourceId {
198    fn eq(&self, other: &Self) -> bool {
199        self.0 == other.0
200    }
201}
202
203impl Eq for ResourceId {}
204
205impl Hash for ResourceId {
206    fn hash<H: Hasher>(&self, state: &mut H) {
207        self.0.hash(state);
208    }
209}
210
211impl PartialOrd for ResourceId {
212    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
213        Some(self.cmp(other))
214    }
215}
216
217impl Ord for ResourceId {
218    fn cmp(&self, other: &Self) -> Ordering {
219        self.0.cmp(&other.0)
220    }
221}
222
223fn fmt_instance_uid(uid: u64) -> String {
224    Uid::Instance(uid, None)
225        .instance_uid_base58()
226        .expect("instance uid should have base58 representation")
227}
228
229fn parse_instance_uid(s: &str) -> Result<u64, UidParseError> {
230    Uid::parse_instance_uid_base58(s)
231}
232
233fn fmt_id_component(
234    f: &mut fmt::Formatter<'_>,
235    uid: &Uid,
236    label: Option<&Label>,
237    default_instance_label: &str,
238) -> fmt::Result {
239    match uid {
240        Uid::Singleton(singleton) => write!(f, "{singleton}"),
241        Uid::Instance(uid, _) => match label {
242            Some(label) => write!(f, "{label}-{}", fmt_instance_uid(*uid)),
243            None => write!(f, "{}-{}", default_instance_label, fmt_instance_uid(*uid)),
244        },
245    }
246}
247
248impl fmt::Display for ResourceId {
249    /// Formats the canonical control-plane string form of this resource id.
250    ///
251    /// This string is used for resource message keys, proc names on hosts,
252    /// and telemetry `full_name`.
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        fmt_id_component(f, &self.0, self.label(), RESOURCE_ID_DEFAULT_LABEL)
255    }
256}
257
258impl fmt::Debug for ResourceId {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        match (&self.0, self.label()) {
261            (Uid::Singleton(label), _) => write!(f, "<{label}>"),
262            (Uid::Instance(uid, _), Some(label)) => {
263                write!(f, "<'{label}' {}>", fmt_instance_uid(*uid))
264            }
265            (Uid::Instance(uid, _), None) => write!(f, "<{}>", fmt_instance_uid(*uid)),
266        }
267    }
268}
269
270fn parse_id_component(s: &str, default_instance_label: &str) -> Result<Uid, ResourceIdParseError> {
271    if let Some(split) = s.rfind('-') {
272        let label_part = &s[..split];
273        let uid_part = &s[split + 1..];
274        if uid_part.len() >= 8
275            && let (Ok(label), Ok(uid)) = (Label::new(label_part), parse_instance_uid(uid_part))
276        {
277            if label.as_str() == default_instance_label {
278                return Ok(Uid::Instance(uid, None));
279            }
280            return Ok(Uid::Instance(uid, Some(label)));
281        }
282    }
283
284    let label = Label::new(s)?;
285    Ok(Uid::Singleton(label))
286}
287
288impl FromStr for ResourceId {
289    type Err = ResourceIdParseError;
290
291    /// Parses the canonical resource-id string forms accepted by the mesh
292    /// control plane.
293    ///
294    /// Accepted inputs are:
295    /// - `label` for singletons
296    /// - `label-uid58` for labeled instances
297    ///
298    /// `resource-uid58` parses as an instance without explicit label metadata.
299    fn from_str(s: &str) -> Result<Self, Self::Err> {
300        Ok(Self(parse_id_component(s, RESOURCE_ID_DEFAULT_LABEL)?))
301    }
302}
303
304macro_rules! define_mesh_id {
305    ($(#[$meta:meta])* $name:ident, $default_label:expr) => {
306        $(#[$meta])*
307        #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Named)]
308        #[serde(transparent)]
309        pub struct $name(ResourceId);
310        wirevalue::register_type!($name);
311
312        impl $name {
313            /// Create a mesh id with explicit uid and label.
314            pub fn new(uid: Uid, label: Option<Label>) -> Self {
315                Self(ResourceId::new(uid, label))
316            }
317
318            /// Create a singleton mesh id identified by label.
319            pub fn singleton(label: Label) -> Self {
320                Self(ResourceId::singleton(label))
321            }
322
323            /// Create an instance mesh id with a random uid and the given label.
324            pub fn instance(label: Label) -> Self {
325                Self(ResourceId::instance(label))
326            }
327
328            /// Returns the uid.
329            pub fn uid(&self) -> &Uid {
330                self.0.uid()
331            }
332
333            /// Returns the explicit label metadata, if any.
334            pub fn label(&self) -> Option<&Label> {
335                self.0.label()
336            }
337
338            /// Returns the human-facing label for this mesh id.
339            ///
340            /// Telemetry uses this for `given_name`.
341            pub fn display_label(&self) -> Option<&Label> {
342                self.0.display_label()
343            }
344
345            /// Returns the inner [`ResourceId`].
346            pub fn resource_id(&self) -> &ResourceId {
347                &self.0
348            }
349
350            /// Returns the default instance label for this mesh id type.
351            pub fn default_instance_label() -> &'static str {
352                $default_label
353            }
354        }
355
356        impl From<$name> for ResourceId {
357            fn from(id: $name) -> Self {
358                id.0
359            }
360        }
361
362        impl From<ResourceId> for $name {
363            fn from(id: ResourceId) -> Self {
364                Self(id)
365            }
366        }
367
368
369        impl fmt::Display for $name {
370            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371                fmt_id_component(f, self.uid(), self.label(), Self::default_instance_label())
372            }
373        }
374
375        impl fmt::Debug for $name {
376            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377                fmt::Debug::fmt(&self.0, f)
378            }
379        }
380
381        impl FromStr for $name {
382            type Err = ResourceIdParseError;
383
384            fn from_str(s: &str) -> Result<Self, Self::Err> {
385                Ok(Self(ResourceId(parse_id_component(
386                    s,
387                    Self::default_instance_label(),
388                )?)))
389            }
390        }
391    };
392}
393
394define_mesh_id!(
395    /// Identifies a host mesh.
396    HostMeshId,
397    HOST_MESH_ID_DEFAULT_LABEL
398);
399
400define_mesh_id!(
401    /// Identifies a proc mesh.
402    ProcMeshId,
403    PROC_MESH_ID_DEFAULT_LABEL
404);
405
406define_mesh_id!(
407    /// Identifies an actor mesh.
408    ActorMeshId,
409    ACTOR_MESH_ID_DEFAULT_LABEL
410);
411
412impl ProcMeshId {
413    /// Converts this mesh id into a hyperactor proc id.
414    pub fn proc_id(&self) -> ProcId {
415        self.resource_id().proc_id()
416    }
417
418    /// Converts this mesh id into a hyperactor proc addr at `location`.
419    pub fn proc_addr(&self, location: impl Into<Location>) -> ProcAddr {
420        self.resource_id().proc_addr(location)
421    }
422}
423
424impl ActorMeshId {
425    /// Converts this mesh id into a hyperactor actor id within `proc_id`.
426    pub fn actor_id(&self, proc_id: ProcId) -> ActorId {
427        ActorId::new(self.uid().clone(), proc_id, None)
428    }
429
430    /// Converts this mesh id into a hyperactor actor addr within `proc_addr`.
431    pub fn actor_addr(&self, proc_addr: ProcAddr) -> ActorAddr {
432        ActorAddr::new_from_uid(proc_addr, self.uid().clone())
433    }
434}
435
436impl hyperactor_config::AttrValue for ActorMeshId {
437    fn display(&self) -> String {
438        self.to_string()
439    }
440
441    fn parse(value: &str) -> Result<Self, anyhow::Error> {
442        Ok(value.parse()?)
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use std::collections::hash_map::DefaultHasher;
449
450    use super::*;
451
452    #[test]
453    fn test_resource_id_singleton() {
454        let id = ResourceId::singleton(Label::new("local").unwrap());
455        assert_eq!(*id.uid(), Uid::Singleton(Label::new("local").unwrap()));
456        assert_eq!(id.label(), None);
457        assert_eq!(id.to_string(), "local");
458    }
459
460    #[test]
461    fn test_resource_id_instance() {
462        let id = ResourceId::instance(Label::new("workers").unwrap());
463        assert!(id.uid().is_instance());
464        assert_eq!(id.label().map(|l| l.as_str()), Some("workers"));
465    }
466
467    #[test]
468    fn test_resource_id_unlabeled() {
469        let id = ResourceId::new(Uid::Instance(0xabcdef, None), None);
470        assert_eq!(
471            id.to_string(),
472            format!("resource-{}", fmt_instance_uid(0xabcdef))
473        );
474        assert_eq!(id.label(), None);
475    }
476
477    #[test]
478    fn test_resource_id_eq_by_uid_only() {
479        let uid = Uid::Instance(0x42, None);
480        let a = ResourceId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
481        let b = ResourceId::new(uid, Some(Label::new("beta").unwrap()));
482        assert_eq!(a, b);
483    }
484
485    #[test]
486    fn test_resource_id_neq_different_uid() {
487        let a = ResourceId::new(Uid::Instance(1, None), Some(Label::new("same").unwrap()));
488        let b = ResourceId::new(Uid::Instance(2, None), Some(Label::new("same").unwrap()));
489        assert_ne!(a, b);
490    }
491
492    #[test]
493    fn test_resource_id_hash_by_uid_only() {
494        let uid = Uid::Instance(0x42, None);
495        let a = ResourceId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
496        let b = ResourceId::new(uid, Some(Label::new("beta").unwrap()));
497
498        let hash = |id: &ResourceId| {
499            let mut h = DefaultHasher::new();
500            id.hash(&mut h);
501            h.finish()
502        };
503        assert_eq!(hash(&a), hash(&b));
504    }
505
506    #[test]
507    fn test_resource_id_ord_by_uid_only() {
508        let a = ResourceId::new(Uid::Instance(1, None), Some(Label::new("zzz").unwrap()));
509        let b = ResourceId::new(Uid::Instance(2, None), Some(Label::new("aaa").unwrap()));
510        assert!(a < b);
511    }
512
513    #[test]
514    fn test_resource_id_display_singleton() {
515        let id = ResourceId::singleton(Label::new("local").unwrap());
516        assert_eq!(id.to_string(), "local");
517    }
518
519    #[test]
520    fn test_resource_id_display_labeled_instance() {
521        let id = ResourceId::new(
522            Uid::Instance(0xd5d54d7201103869, None),
523            Some(Label::new("workers").unwrap()),
524        );
525        assert_eq!(
526            id.to_string(),
527            format!("workers-{}", fmt_instance_uid(0xd5d54d7201103869))
528        );
529    }
530
531    #[test]
532    fn test_resource_id_display_unlabeled_instance() {
533        let id = ResourceId::new(Uid::Instance(0xd5d54d7201103869, None), None);
534        assert_eq!(
535            id.to_string(),
536            format!("resource-{}", fmt_instance_uid(0xd5d54d7201103869))
537        );
538    }
539
540    #[test]
541    fn test_resource_id_debug() {
542        let singleton = ResourceId::singleton(Label::new("local").unwrap());
543        assert_eq!(format!("{:?}", singleton), "<local>");
544
545        let labeled = ResourceId::new(
546            Uid::Instance(0xd5d54d7201103869, None),
547            Some(Label::new("workers").unwrap()),
548        );
549        assert_eq!(
550            format!("{:?}", labeled),
551            format!("<'workers' {}>", fmt_instance_uid(0xd5d54d7201103869))
552        );
553
554        let unlabeled = ResourceId::new(Uid::Instance(0xd5d54d7201103869, None), None);
555        assert_eq!(
556            format!("{:?}", unlabeled),
557            format!("<{}>", fmt_instance_uid(0xd5d54d7201103869))
558        );
559    }
560
561    #[test]
562    fn test_resource_id_fromstr_singleton() {
563        let parsed: ResourceId = "local".parse().unwrap();
564        assert_eq!(*parsed.uid(), Uid::Singleton(Label::new("local").unwrap()));
565        assert_eq!(parsed.label(), None);
566    }
567
568    #[test]
569    fn test_resource_id_fromstr_base58_like_singleton() {
570        let parsed: ResourceId = "service".parse().unwrap();
571        assert_eq!(
572            *parsed.uid(),
573            Uid::Singleton(Label::new("service").unwrap())
574        );
575        assert_eq!(parsed.label(), None);
576    }
577
578    #[test]
579    fn test_resource_id_fromstr_short_suffix_singleton() {
580        let parsed: ResourceId = "env-vars".parse().unwrap();
581        assert_eq!(
582            *parsed.uid(),
583            Uid::Singleton(Label::new("env-vars").unwrap())
584        );
585        assert_eq!(parsed.label(), None);
586    }
587
588    #[test]
589    fn test_resource_id_fromstr_labeled_instance() {
590        let parsed: ResourceId = format!("workers-{}", fmt_instance_uid(0xd5d54d7201103869))
591            .parse()
592            .unwrap();
593        assert_eq!(
594            *parsed.uid(),
595            Uid::Instance(0xd5d54d7201103869, Some(Label::new("workers").unwrap()))
596        );
597        assert_eq!(parsed.label().map(|l| l.as_str()), Some("workers"));
598    }
599
600    #[test]
601    fn test_resource_id_fromstr_default_labeled_instance() {
602        let parsed: ResourceId = format!("resource-{}", fmt_instance_uid(0xd5d54d7201103869))
603            .parse()
604            .unwrap();
605        assert_eq!(*parsed.uid(), Uid::Instance(0xd5d54d7201103869, None));
606        assert_eq!(parsed.label(), None);
607    }
608
609    #[test]
610    fn test_resource_id_fromstr_rejects_unlabeled_instance() {
611        let result: Result<ResourceId, _> =
612            format!("<{}>", fmt_instance_uid(0xd5d54d7201103869)).parse();
613        assert!(result.is_err());
614    }
615
616    #[test]
617    fn test_resource_id_fromstr_labeled_with_hyphens() {
618        let parsed: ResourceId = format!("my-service-{}", fmt_instance_uid(0xd5d54d7201103869))
619            .parse()
620            .unwrap();
621        assert_eq!(
622            *parsed.uid(),
623            Uid::Instance(0xd5d54d7201103869, Some(Label::new("my-service").unwrap()))
624        );
625        assert_eq!(parsed.label().map(|l| l.as_str()), Some("my-service"));
626    }
627
628    #[test]
629    fn test_resource_id_display_fromstr_roundtrip() {
630        let cases = vec![
631            ResourceId::singleton(Label::new("local").unwrap()),
632            ResourceId::new(
633                Uid::Instance(0xd5d54d7201103869, None),
634                Some(Label::new("workers").unwrap()),
635            ),
636            ResourceId::new(Uid::Instance(0xd5d54d7201103869, None), None),
637            ResourceId::new(
638                Uid::Instance(0xd5d54d7201103869, None),
639                Some(Label::new("my-service").unwrap()),
640            ),
641            ResourceId::new(
642                Uid::Instance(0xd5d54d7201103869, None),
643                Some(Label::new("a").unwrap()),
644            ),
645        ];
646        for id in cases {
647            let s = id.to_string();
648            let parsed: ResourceId = s.parse().unwrap();
649            assert_eq!(id, parsed, "round-trip failed for {s}");
650        }
651    }
652
653    #[test]
654    fn test_resource_id_serde_roundtrip() {
655        let cases = vec![
656            ResourceId::singleton(Label::new("local").unwrap()),
657            ResourceId::new(
658                Uid::Instance(0xabcdef, None),
659                Some(Label::new("workers").unwrap()),
660            ),
661            ResourceId::new(Uid::Instance(0xabcdef, None), None),
662        ];
663        for id in cases {
664            let json = serde_json::to_string(&id).unwrap();
665            let parsed: ResourceId = serde_json::from_str(&json).unwrap();
666            assert_eq!(id, parsed);
667            // Verify label is preserved through serde.
668            assert_eq!(
669                id.label().map(|l| l.as_str()),
670                parsed.label().map(|l| l.as_str())
671            );
672        }
673    }
674
675    #[test]
676    fn test_mesh_id_construction() {
677        let host = HostMeshId::singleton(Label::new("local").unwrap());
678        assert_eq!(host.to_string(), "local");
679        assert_eq!(*host.uid(), Uid::Singleton(Label::new("local").unwrap()));
680
681        let proc_ = ProcMeshId::instance(Label::new("workers").unwrap());
682        assert!(proc_.uid().is_instance());
683        assert_eq!(proc_.label().map(|l| l.as_str()), Some("workers"));
684
685        let actor = ActorMeshId::instance(Label::new("trainers").unwrap());
686        assert!(actor.uid().is_instance());
687        assert_eq!(actor.label().map(|l| l.as_str()), Some("trainers"));
688    }
689
690    #[test]
691    fn test_mesh_id_eq_by_uid_only() {
692        let uid = Uid::Instance(0x42, None);
693        let a = HostMeshId::new(uid.clone(), Some(Label::new("alpha").unwrap()));
694        let b = HostMeshId::new(uid, Some(Label::new("beta").unwrap()));
695        assert_eq!(a, b);
696    }
697
698    #[test]
699    fn test_mesh_id_display_fromstr_roundtrip() {
700        let ids: Vec<HostMeshId> = vec![
701            HostMeshId::singleton(Label::new("local").unwrap()),
702            HostMeshId::new(
703                Uid::Instance(0xd5d54d7201103869, None),
704                Some(Label::new("workers").unwrap()),
705            ),
706            HostMeshId::new(Uid::Instance(0xd5d54d7201103869, None), None),
707        ];
708        for id in ids {
709            let s = id.to_string();
710            let parsed: HostMeshId = s.parse().unwrap();
711            assert_eq!(id, parsed, "round-trip failed for {s}");
712        }
713    }
714
715    #[test]
716    fn test_typed_mesh_id_display_uses_type_default_label() {
717        let uid = Uid::Instance(0xd5d54d7201103869, None);
718        assert_eq!(
719            HostMeshId::new(uid.clone(), None).to_string(),
720            format!("host-{}", fmt_instance_uid(0xd5d54d7201103869))
721        );
722        assert_eq!(
723            ProcMeshId::new(uid.clone(), None).to_string(),
724            format!("proc-{}", fmt_instance_uid(0xd5d54d7201103869))
725        );
726        assert_eq!(
727            ActorMeshId::new(uid, None).to_string(),
728            format!("actor-{}", fmt_instance_uid(0xd5d54d7201103869))
729        );
730    }
731
732    #[test]
733    fn test_typed_mesh_id_parse_omits_type_default_label() {
734        let proc: ProcMeshId = format!("proc-{}", fmt_instance_uid(0xd5d54d7201103869))
735            .parse()
736            .unwrap();
737        assert_eq!(*proc.uid(), Uid::Instance(0xd5d54d7201103869, None));
738        assert_eq!(proc.label(), None);
739
740        let actor: ActorMeshId = format!("actor-{}", fmt_instance_uid(0xd5d54d7201103869))
741            .parse()
742            .unwrap();
743        assert_eq!(*actor.uid(), Uid::Instance(0xd5d54d7201103869, None));
744        assert_eq!(actor.label(), None);
745    }
746
747    #[test]
748    fn test_typed_mesh_id_parse_preserves_non_default_label() {
749        let proc: ProcMeshId = format!("worker-{}", fmt_instance_uid(0xd5d54d7201103869))
750            .parse()
751            .unwrap();
752        assert_eq!(
753            *proc.uid(),
754            Uid::Instance(0xd5d54d7201103869, Some(Label::new("worker").unwrap()))
755        );
756        assert_eq!(proc.label().map(|l| l.as_str()), Some("worker"));
757    }
758
759    #[test]
760    fn test_mesh_id_resource_id_conversion() {
761        let host = HostMeshId::instance(Label::new("test").unwrap());
762        let resource_id: ResourceId = host.clone().into();
763        assert_eq!(host.uid(), resource_id.uid());
764        assert_eq!(
765            host.label().map(|l| l.as_str()),
766            resource_id.label().map(|l| l.as_str())
767        );
768
769        let back: HostMeshId = resource_id.into();
770        assert_eq!(host, back);
771    }
772
773    #[test]
774    fn test_mesh_id_serde_transparent() {
775        let host = HostMeshId::new(
776            Uid::Instance(0xabcdef, None),
777            Some(Label::new("test").unwrap()),
778        );
779        let resource = ResourceId::new(
780            Uid::Instance(0xabcdef, None),
781            Some(Label::new("test").unwrap()),
782        );
783
784        let host_json = serde_json::to_string(&host).unwrap();
785        let resource_json = serde_json::to_string(&resource).unwrap();
786        assert_eq!(host_json, resource_json);
787    }
788
789    #[test]
790    fn test_instance_ids_differ() {
791        let a = ResourceId::instance(Label::new("test").unwrap());
792        let b = ResourceId::instance(Label::new("test").unwrap());
793        assert_ne!(a, b);
794    }
795
796    #[test]
797    fn test_singleton_ids_match() {
798        let a = ResourceId::singleton(Label::new("local").unwrap());
799        let b = ResourceId::singleton(Label::new("local").unwrap());
800        assert_eq!(a, b);
801    }
802}