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