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