Skip to main content

zeph_common/
types.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Strongly-typed identifiers and shared tool types across `zeph-*` crates.
5//!
6//! This module defines `ToolName`, `ProviderName`, `SkillName`, `SessionId`, and
7//! `ToolDefinition` — types shared by multiple crates without creating cross-crate
8//! dependencies.
9//!
10//! `ToolName`, `ProviderName`, `SkillName`, and `SessionId` use `#[serde(transparent)]`
11//! for zero-cost serialization compatibility: the JSON wire format is unchanged relative
12//! to plain `String` fields.
13
14use std::borrow::Borrow;
15use std::fmt;
16use std::str::FromStr;
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21/// Generates an `Arc<str>`-backed newtype with the shared trait surface used by
22/// `ToolName`, `ProviderName`, and `SkillName`: `Default`, `Display`, `AsRef<str>`,
23/// `Borrow<str>`, `From<&str>`, `From<String>`, `FromStr`, and both directions of
24/// `PartialEq` against `str`/`&str`/`String`, plus `new`/`as_str` constructors.
25///
26/// `Borrow<str>` and `derive(Hash)` are kept consistent so instances can be used as
27/// `HashMap` keys and looked up by `&str` without allocating.
28macro_rules! arc_str_newtype {
29    (
30        $(#[$struct_doc:meta])*
31        struct $name:ident;
32        new_doc: $(#[$new_doc:meta])*
33        as_str_doc: $(#[$as_str_doc:meta])*
34        default_doc: $(#[$default_doc:meta])*
35    ) => {
36        $(#[$struct_doc])*
37        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
38        #[serde(transparent)]
39        pub struct $name(Arc<str>);
40
41        impl $name {
42            $(#[$new_doc])*
43            #[must_use]
44            pub fn new(s: impl Into<Arc<str>>) -> Self {
45                Self(s.into())
46            }
47
48            $(#[$as_str_doc])*
49            #[must_use]
50            pub fn as_str(&self) -> &str {
51                &self.0
52            }
53        }
54
55        impl Default for $name {
56            $(#[$default_doc])*
57            fn default() -> Self {
58                Self(Arc::from(""))
59            }
60        }
61
62        impl fmt::Display for $name {
63            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64                f.write_str(&self.0)
65            }
66        }
67
68        impl AsRef<str> for $name {
69            fn as_ref(&self) -> &str {
70                &self.0
71            }
72        }
73
74        impl Borrow<str> for $name {
75            fn borrow(&self) -> &str {
76                &self.0
77            }
78        }
79
80        impl From<&str> for $name {
81            fn from(s: &str) -> Self {
82                Self(Arc::from(s))
83            }
84        }
85
86        impl From<String> for $name {
87            fn from(s: String) -> Self {
88                Self(Arc::from(s.as_str()))
89            }
90        }
91
92        impl FromStr for $name {
93            type Err = std::convert::Infallible;
94
95            fn from_str(s: &str) -> Result<Self, Self::Err> {
96                Ok(Self::from(s))
97            }
98        }
99
100        impl PartialEq<str> for $name {
101            fn eq(&self, other: &str) -> bool {
102                self.0.as_ref() == other
103            }
104        }
105
106        impl PartialEq<&str> for $name {
107            fn eq(&self, other: &&str) -> bool {
108                self.0.as_ref() == *other
109            }
110        }
111
112        impl PartialEq<String> for $name {
113            fn eq(&self, other: &String) -> bool {
114                self.0.as_ref() == other.as_str()
115            }
116        }
117
118        impl PartialEq<$name> for str {
119            fn eq(&self, other: &$name) -> bool {
120                self == other.0.as_ref()
121            }
122        }
123
124        impl PartialEq<$name> for String {
125            fn eq(&self, other: &$name) -> bool {
126                self.as_str() == other.0.as_ref()
127            }
128        }
129    };
130}
131
132arc_str_newtype!(
133    /// Strongly-typed tool name label.
134    ///
135    /// `ToolName` identifies a tool by its canonical name (e.g., `"shell"`, `"web_scrape"`).
136    /// It is produced by the LLM in JSON tool-use responses and matched against the registered
137    /// tool registry at dispatch time.
138    ///
139    /// # Label semantics (not a validated reference)
140    ///
141    /// `ToolName` is an unvalidated label from untrusted input (LLM JSON). It does **not**
142    /// guarantee that a tool with this name is registered. Validation happens downstream at
143    /// tool dispatch, not at construction.
144    ///
145    /// # Inner type: `Arc<str>`
146    ///
147    /// The inner type is `Arc<str>`, not `String`. Tool names are cloned into multiple contexts
148    /// (event channels, tracing spans, tool output structs) during a single tool execution.
149    /// `Arc<str>` makes all clones O(1) vs O(n) for `String`. Use `.clone()` to duplicate
150    /// a `ToolName` — it is cheap.
151    ///
152    /// # No `Deref<Target=str>`
153    ///
154    /// `ToolName` does **not** implement `Deref<Target=str>`. This prevents the `.to_owned()`
155    /// footgun where muscle memory returns `String` instead of `ToolName`. Use `.as_str()` for
156    /// explicit string conversion and `.clone()` to duplicate the `ToolName`.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use zeph_common::ToolName;
162    ///
163    /// let name = ToolName::new("shell");
164    /// assert_eq!(name.as_str(), "shell");
165    /// assert_eq!(name, "shell");
166    ///
167    /// // Clone is O(1) — Arc reference count increment only.
168    /// let name2 = name.clone();
169    /// assert_eq!(name, name2);
170    /// ```
171    struct ToolName;
172    new_doc:
173    /// Construct a `ToolName` from any value convertible to `Arc<str>`.
174    ///
175    /// This is the primary constructor. The name is accepted without validation — it is a
176    /// label from the LLM wire or tool registry, not a proof of registration.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use zeph_common::ToolName;
182    ///
183    /// let name = ToolName::new("shell");
184    /// assert_eq!(name.as_str(), "shell");
185    /// ```
186    as_str_doc:
187    /// Return the inner string slice.
188    ///
189    /// Prefer this over `Deref` (which is intentionally not implemented) when an `&str`
190    /// reference is needed.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use zeph_common::ToolName;
196    ///
197    /// let name = ToolName::new("web_scrape");
198    /// assert_eq!(name.as_str(), "web_scrape");
199    /// ```
200    default_doc:
201    /// Returns an empty `ToolName`.
202    ///
203    /// This implementation exists solely for `#[serde(default)]` on optional fields.
204    /// Do not construct a `ToolName` with an empty string in application code.
205);
206
207// ── ProviderName ─────────────────────────────────────────────────────────────
208
209arc_str_newtype!(
210    /// Strongly-typed LLM provider name.
211    ///
212    /// `ProviderName` identifies a configured provider by its name field (e.g., `"fast"`,
213    /// `"quality"`, `"ollama-local"`). Names come from `[[llm.providers]] name = "…"` in the
214    /// TOML config; subsystems reference providers by this name via `*_provider` fields.
215    ///
216    /// # Inner type: `Arc<str>`
217    ///
218    /// The inner type is `Arc<str>`. Provider names are cloned widely across subsystem config
219    /// structs, metric labels, and log spans. `Arc<str>` makes all clones O(1).
220    ///
221    /// # No `Deref<Target=str>`
222    ///
223    /// `ProviderName` does **not** implement `Deref<Target=str>`. Use `.as_str()` for explicit
224    /// string conversion and `.clone()` to duplicate.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use zeph_common::ProviderName;
230    ///
231    /// let name = ProviderName::new("fast");
232    /// assert_eq!(name.as_str(), "fast");
233    /// assert_eq!(name, "fast");
234    ///
235    /// // Clone is O(1) — Arc reference count increment only.
236    /// let name2 = name.clone();
237    /// assert_eq!(name, name2);
238    /// ```
239    struct ProviderName;
240    new_doc:
241    /// Construct a `ProviderName` from any value convertible to `Arc<str>`.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use zeph_common::ProviderName;
247    ///
248    /// let name = ProviderName::new("quality");
249    /// assert_eq!(name.as_str(), "quality");
250    /// ```
251    as_str_doc:
252    /// Return the inner string slice.
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use zeph_common::ProviderName;
258    ///
259    /// let name = ProviderName::new("ollama-local");
260    /// assert_eq!(name.as_str(), "ollama-local");
261    /// ```
262    default_doc:
263    /// Returns an empty `ProviderName`.
264    ///
265    /// Exists solely for `#[serde(default)]` on optional fields. Do not use in
266    /// application code — an empty name will fail provider lookup.
267);
268
269impl ProviderName {
270    /// Return `true` when this is the empty sentinel (use the primary provider).
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use zeph_common::ProviderName;
276    ///
277    /// assert!(ProviderName::default().is_empty());
278    /// assert!(!ProviderName::new("fast").is_empty());
279    /// ```
280    #[must_use]
281    pub fn is_empty(&self) -> bool {
282        self.0.is_empty()
283    }
284
285    /// Return `Some(&str)` when non-empty, `None` for the empty sentinel.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use zeph_common::ProviderName;
291    ///
292    /// assert_eq!(ProviderName::default().as_non_empty(), None);
293    /// assert_eq!(ProviderName::new("fast").as_non_empty(), Some("fast"));
294    /// ```
295    #[must_use]
296    pub fn as_non_empty(&self) -> Option<&str> {
297        if self.0.is_empty() {
298            None
299        } else {
300            Some(&self.0)
301        }
302    }
303}
304
305// ── SkillName ────────────────────────────────────────────────────────────────
306
307arc_str_newtype!(
308    /// Strongly-typed skill name identifier.
309    ///
310    /// `SkillName` identifies a skill by its canonical name (e.g., `"rust-agents"`,
311    /// `"readme-generator"`). Names come from `SKILL.md` frontmatter `name:` fields and
312    /// are used at match time, invocation routing, and telemetry.
313    ///
314    /// # Inner type: `Arc<str>`
315    ///
316    /// The inner type is `Arc<str>`. Skill names are referenced from multiple subsystems
317    /// (registry, matcher, invoker, TUI) during a single agent turn. `Arc<str>` makes all
318    /// clones O(1).
319    ///
320    /// # No `Deref<Target=str>`
321    ///
322    /// `SkillName` does **not** implement `Deref<Target=str>`. Use `.as_str()` for explicit
323    /// string conversion and `.clone()` to duplicate.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use zeph_common::SkillName;
329    ///
330    /// let name = SkillName::new("rust-agents");
331    /// assert_eq!(name.as_str(), "rust-agents");
332    /// assert_eq!(name, "rust-agents");
333    ///
334    /// // Clone is O(1) — Arc reference count increment only.
335    /// let name2 = name.clone();
336    /// assert_eq!(name, name2);
337    /// ```
338    struct SkillName;
339    new_doc:
340    /// Construct a `SkillName` from any value convertible to `Arc<str>`.
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use zeph_common::SkillName;
346    ///
347    /// let name = SkillName::new("readme-generator");
348    /// assert_eq!(name.as_str(), "readme-generator");
349    /// ```
350    as_str_doc:
351    /// Return the inner string slice.
352    ///
353    /// # Examples
354    ///
355    /// ```
356    /// use zeph_common::SkillName;
357    ///
358    /// let name = SkillName::new("rust-agents");
359    /// assert_eq!(name.as_str(), "rust-agents");
360    /// ```
361    default_doc:
362    /// Returns an empty `SkillName`.
363    ///
364    /// Exists solely for `#[serde(default)]` on optional fields. Do not use in
365    /// application code — an empty name will fail skill lookup.
366);
367
368// ── SessionId ────────────────────────────────────────────────────────────────
369
370/// Identifies a single agent session (one binary invocation or one ACP connection).
371///
372/// Uses `String` internally to support both UUID-based IDs (production) and
373/// arbitrary string IDs (tests, experiments). UUID validation is enforced only at
374/// [`SessionId::generate`] time; [`SessionId::new`] accepts any non-empty string for
375/// flexibility in test fixtures.
376///
377/// # Serialization
378///
379/// `SessionId` uses `#[serde(transparent)]` — it serializes as a plain JSON string
380/// identical to the raw `String` fields it replaces. No wire format change, no DB
381/// schema migration required.
382///
383/// # ACP Note
384///
385/// `acp::SessionId` from the external `agent_client_protocol` crate is distinct.
386/// This type is for **our own** session tracking only.
387///
388/// # Examples
389///
390/// ```
391/// use zeph_common::SessionId;
392///
393/// // Production: generate a fresh UUID session
394/// let id = SessionId::generate();
395/// assert!(!id.as_str().is_empty());
396///
397/// // Tests: use a readable fixture string
398/// let test_id = SessionId::new("test-session");
399/// assert_eq!(test_id.as_str(), "test-session");
400/// ```
401#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
402#[serde(transparent)]
403pub struct SessionId(String);
404
405impl SessionId {
406    /// Create a `SessionId` from any non-empty string.
407    ///
408    /// Accepts UUID strings (production), readable names (tests), or any other
409    /// non-empty value. In debug builds, an empty string triggers a `debug_assert!`
410    /// to catch accidental construction early.
411    ///
412    /// # Panics
413    ///
414    /// Panics in **debug builds only** if `s` is empty.
415    ///
416    /// # Examples
417    ///
418    /// ```
419    /// use zeph_common::SessionId;
420    ///
421    /// let id = SessionId::new("test-session");
422    /// assert_eq!(id.as_str(), "test-session");
423    /// ```
424    pub fn new(s: impl Into<String>) -> Self {
425        let s = s.into();
426        debug_assert!(!s.is_empty(), "SessionId must not be empty");
427        Self(s)
428    }
429
430    /// Generate a new session ID backed by a random UUID v4.
431    ///
432    /// # Examples
433    ///
434    /// ```
435    /// use zeph_common::SessionId;
436    ///
437    /// let id = SessionId::generate();
438    /// assert!(!id.as_str().is_empty());
439    /// // UUIDs are 36 chars: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
440    /// assert_eq!(id.as_str().len(), 36);
441    /// ```
442    #[must_use]
443    pub fn generate() -> Self {
444        Self(uuid::Uuid::new_v4().to_string())
445    }
446
447    /// Return the inner string slice.
448    ///
449    /// # Examples
450    ///
451    /// ```
452    /// use zeph_common::SessionId;
453    ///
454    /// let id = SessionId::new("s1");
455    /// assert_eq!(id.as_str(), "s1");
456    /// ```
457    #[must_use]
458    pub fn as_str(&self) -> &str {
459        &self.0
460    }
461}
462
463impl Default for SessionId {
464    /// Generate a new UUID-backed session ID.
465    fn default() -> Self {
466        Self::generate()
467    }
468}
469
470impl fmt::Display for SessionId {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        f.write_str(&self.0)
473    }
474}
475
476impl AsRef<str> for SessionId {
477    fn as_ref(&self) -> &str {
478        &self.0
479    }
480}
481
482impl std::ops::Deref for SessionId {
483    type Target = str;
484
485    fn deref(&self) -> &str {
486        &self.0
487    }
488}
489
490impl From<String> for SessionId {
491    fn from(s: String) -> Self {
492        Self::new(s)
493    }
494}
495
496impl From<&str> for SessionId {
497    fn from(s: &str) -> Self {
498        Self::new(s)
499    }
500}
501
502impl From<uuid::Uuid> for SessionId {
503    fn from(u: uuid::Uuid) -> Self {
504        Self(u.to_string())
505    }
506}
507
508impl FromStr for SessionId {
509    type Err = std::convert::Infallible;
510
511    fn from_str(s: &str) -> Result<Self, Self::Err> {
512        Ok(Self::new(s))
513    }
514}
515
516impl PartialEq<str> for SessionId {
517    fn eq(&self, other: &str) -> bool {
518        self.0 == other
519    }
520}
521
522impl PartialEq<&str> for SessionId {
523    fn eq(&self, other: &&str) -> bool {
524        self.0 == *other
525    }
526}
527
528impl PartialEq<String> for SessionId {
529    fn eq(&self, other: &String) -> bool {
530        self.0 == *other
531    }
532}
533
534impl PartialEq<SessionId> for str {
535    fn eq(&self, other: &SessionId) -> bool {
536        self == other.0
537    }
538}
539
540impl PartialEq<SessionId> for String {
541    fn eq(&self, other: &SessionId) -> bool {
542        *self == other.0
543    }
544}
545
546// ── ToolDefinition ───────────────────────────────────────────────────────────
547
548/// Minimal tool definition passed to LLM providers.
549///
550/// Decoupled from `zeph-tools::ToolDef` to avoid cross-crate dependencies.
551/// Providers translate this into their native tool/function format before sending to the API.
552///
553/// # Examples
554///
555/// ```
556/// use zeph_common::types::ToolDefinition;
557/// use zeph_common::ToolName;
558///
559/// let tool = ToolDefinition {
560///     name: ToolName::new("get_weather"),
561///     description: "Return current weather for a city.".into(),
562///     parameters: serde_json::json!({
563///         "type": "object",
564///         "properties": {
565///             "city": { "type": "string" }
566///         },
567///         "required": ["city"]
568///     }),
569///     output_schema: None,
570/// };
571/// assert_eq!(tool.name, "get_weather");
572/// ```
573#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
574pub struct ToolDefinition {
575    /// Tool name — must match the name used in the response `ToolUseRequest`.
576    pub name: ToolName,
577    /// Human-readable description guiding the model on when to call this tool.
578    pub description: String,
579    /// JSON Schema object describing parameters.
580    pub parameters: serde_json::Value,
581    /// Raw output schema advertised by the MCP server, if present.
582    ///
583    /// When `mcp.forward_output_schema = true`, LLM provider assemblers append a compact JSON
584    /// hint to the tool description rather than adding a new top-level field (unsupported by
585    /// the Anthropic and `OpenAI` APIs).
586    ///
587    /// DO NOT convert to `schemars::Schema` — lossy; see #2931 critique P0-1.
588    #[serde(default, skip_serializing_if = "Option::is_none")]
589    pub output_schema: Option<serde_json::Value>,
590}
591
592/// Reason why the agent turn ended early.
593///
594/// Emitted by the agent loop when a non-default terminal condition is detected.
595/// Consumers (e.g. the ACP layer) map this to the protocol-level `StopReason`.
596///
597/// # Examples
598///
599/// ```
600/// use zeph_common::StopHint;
601///
602/// let hint = StopHint::MaxTokens;
603/// assert!(matches!(hint, StopHint::MaxTokens));
604/// ```
605#[non_exhaustive]
606#[derive(Debug, Clone, Copy, PartialEq, Eq)]
607pub enum StopHint {
608    /// The LLM response was cut off by the token limit.
609    MaxTokens,
610    /// The turn loop exhausted `max_turns` without a final text response.
611    MaxTurnRequests,
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    #[test]
619    fn tool_name_construction_and_equality() {
620        let name = ToolName::new("shell");
621        assert_eq!(name.as_str(), "shell");
622        assert_eq!(name, "shell");
623        assert_eq!(name, "shell".to_owned());
624        // Reverse direction: PartialEq<ToolName> for str/String
625        assert_eq!(*"shell", name);
626        assert_eq!("shell".to_owned(), name);
627    }
628
629    #[test]
630    fn tool_name_default_is_empty() {
631        let name = ToolName::default();
632        assert_eq!(name.as_str(), "");
633    }
634
635    #[test]
636    fn tool_name_clone_is_cheap() {
637        let name = ToolName::new("web_scrape");
638        let name2 = name.clone();
639        assert_eq!(name, name2);
640        // Both Arc<str> point to same allocation
641        assert!(Arc::ptr_eq(&name.0, &name2.0));
642    }
643
644    #[test]
645    fn tool_name_from_impls() {
646        let from_str: ToolName = ToolName::from("bash");
647        let from_string: ToolName = ToolName::from("bash".to_owned());
648        let parsed: ToolName = "bash".parse().unwrap();
649        assert_eq!(from_str, from_string);
650        assert_eq!(from_str, parsed);
651    }
652
653    #[test]
654    fn tool_name_as_hashmap_key() {
655        use std::collections::HashMap;
656        let mut map: HashMap<ToolName, u32> = HashMap::new();
657        map.insert(ToolName::new("shell"), 1);
658        // Borrow<str> enables lookup by &str
659        assert_eq!(map.get("shell"), Some(&1));
660    }
661
662    #[test]
663    fn tool_name_display() {
664        let name = ToolName::new("my_tool");
665        assert_eq!(format!("{name}"), "my_tool");
666    }
667
668    #[test]
669    fn tool_name_serde_transparent() {
670        let name = ToolName::new("shell");
671        let json = serde_json::to_string(&name).unwrap();
672        assert_eq!(json, r#""shell""#);
673        let back: ToolName = serde_json::from_str(&json).unwrap();
674        assert_eq!(back, name);
675    }
676
677    #[test]
678    fn session_id_new_roundtrip() {
679        let id = SessionId::new("test-session");
680        assert_eq!(id.as_str(), "test-session");
681        assert_eq!(id.to_string(), "test-session");
682    }
683
684    #[test]
685    fn session_id_generate_is_uuid() {
686        let id = SessionId::generate();
687        assert_eq!(id.as_str().len(), 36);
688        assert!(uuid::Uuid::parse_str(id.as_str()).is_ok());
689    }
690
691    #[test]
692    fn session_id_default_is_generated() {
693        let id = SessionId::default();
694        assert!(!id.as_str().is_empty());
695        assert_eq!(id.as_str().len(), 36);
696    }
697
698    #[test]
699    fn session_id_from_uuid() {
700        let u = uuid::Uuid::new_v4();
701        let id = SessionId::from(u);
702        assert_eq!(id.as_str(), u.to_string());
703    }
704
705    #[test]
706    fn session_id_deref_slicing() {
707        let id = SessionId::new("abcdefgh");
708        // Deref<Target=str> enables string slicing
709        assert_eq!(&id[..4], "abcd");
710    }
711
712    #[test]
713    fn session_id_serde_transparent() {
714        let id = SessionId::new("sess-abc");
715        let json = serde_json::to_string(&id).unwrap();
716        assert_eq!(json, r#""sess-abc""#);
717        let back: SessionId = serde_json::from_str(&json).unwrap();
718        assert_eq!(back, id);
719    }
720
721    #[test]
722    fn session_id_from_str_parses() {
723        let id: SessionId = "my-session".parse().unwrap();
724        assert_eq!(id.as_str(), "my-session");
725    }
726
727    #[test]
728    fn provider_name_construction_and_equality() {
729        let name = ProviderName::new("fast");
730        assert_eq!(name.as_str(), "fast");
731        assert_eq!(name, "fast");
732        assert_eq!(name, "fast".to_owned());
733        // Reverse direction: PartialEq<ProviderName> for str/String
734        assert_eq!(*"fast", name);
735        assert_eq!("fast".to_owned(), name);
736    }
737
738    #[test]
739    fn provider_name_clone_is_cheap() {
740        let name = ProviderName::new("quality");
741        let name2 = name.clone();
742        assert_eq!(name, name2);
743        assert!(Arc::ptr_eq(&name.0, &name2.0));
744    }
745
746    #[test]
747    fn provider_name_from_impls() {
748        let from_str: ProviderName = ProviderName::from("fast");
749        let from_string: ProviderName = ProviderName::from("fast".to_owned());
750        let parsed: ProviderName = "fast".parse().unwrap();
751        assert_eq!(from_str, from_string);
752        assert_eq!(from_str, parsed);
753    }
754
755    #[test]
756    fn provider_name_as_hashmap_key() {
757        use std::collections::HashMap;
758        let mut map: HashMap<ProviderName, u32> = HashMap::new();
759        map.insert(ProviderName::new("fast"), 1);
760        assert_eq!(map.get("fast"), Some(&1));
761    }
762
763    #[test]
764    fn provider_name_display() {
765        let name = ProviderName::new("ollama-local");
766        assert_eq!(format!("{name}"), "ollama-local");
767    }
768
769    #[test]
770    fn provider_name_serde_transparent() {
771        let name = ProviderName::new("quality");
772        let json = serde_json::to_string(&name).unwrap();
773        assert_eq!(json, r#""quality""#);
774        let back: ProviderName = serde_json::from_str(&json).unwrap();
775        assert_eq!(back, name);
776    }
777
778    #[test]
779    fn skill_name_construction_and_equality() {
780        let name = SkillName::new("rust-agents");
781        assert_eq!(name.as_str(), "rust-agents");
782        assert_eq!(name, "rust-agents");
783        assert_eq!(name, "rust-agents".to_owned());
784        // Reverse direction: PartialEq<SkillName> for str/String
785        assert_eq!(*"rust-agents", name);
786        assert_eq!("rust-agents".to_owned(), name);
787    }
788
789    #[test]
790    fn skill_name_default_is_empty() {
791        let name = SkillName::default();
792        assert_eq!(name.as_str(), "");
793    }
794
795    #[test]
796    fn skill_name_clone_is_cheap() {
797        let name = SkillName::new("readme-generator");
798        let name2 = name.clone();
799        assert_eq!(name, name2);
800        assert!(Arc::ptr_eq(&name.0, &name2.0));
801    }
802
803    #[test]
804    fn skill_name_from_impls() {
805        let from_str: SkillName = SkillName::from("rust-agents");
806        let from_string: SkillName = SkillName::from("rust-agents".to_owned());
807        let parsed: SkillName = "rust-agents".parse().unwrap();
808        assert_eq!(from_str, from_string);
809        assert_eq!(from_str, parsed);
810    }
811
812    #[test]
813    fn skill_name_as_hashmap_key() {
814        use std::collections::HashMap;
815        let mut map: HashMap<SkillName, u32> = HashMap::new();
816        map.insert(SkillName::new("rust-agents"), 1);
817        assert_eq!(map.get("rust-agents"), Some(&1));
818    }
819
820    #[test]
821    fn skill_name_display() {
822        let name = SkillName::new("readme-generator");
823        assert_eq!(format!("{name}"), "readme-generator");
824    }
825
826    #[test]
827    fn skill_name_serde_transparent() {
828        let name = SkillName::new("rust-agents");
829        let json = serde_json::to_string(&name).unwrap();
830        assert_eq!(json, r#""rust-agents""#);
831        let back: SkillName = serde_json::from_str(&json).unwrap();
832        assert_eq!(back, name);
833    }
834}