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    /// Return `true` when this is the empty sentinel (use the primary provider).
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use zeph_common::ProviderName;
251    ///
252    /// assert!(ProviderName::default().is_empty());
253    /// assert!(!ProviderName::new("fast").is_empty());
254    /// ```
255    #[must_use]
256    pub fn is_empty(&self) -> bool {
257        self.0.is_empty()
258    }
259
260    /// Return `Some(&str)` when non-empty, `None` for the empty sentinel.
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use zeph_common::ProviderName;
266    ///
267    /// assert_eq!(ProviderName::default().as_non_empty(), None);
268    /// assert_eq!(ProviderName::new("fast").as_non_empty(), Some("fast"));
269    /// ```
270    #[must_use]
271    pub fn as_non_empty(&self) -> Option<&str> {
272        if self.0.is_empty() {
273            None
274        } else {
275            Some(&self.0)
276        }
277    }
278}
279
280impl Default for ProviderName {
281    /// Returns an empty `ProviderName`.
282    ///
283    /// Exists solely for `#[serde(default)]` on optional fields. Do not use in
284    /// application code — an empty name will fail provider lookup.
285    fn default() -> Self {
286        Self(Arc::from(""))
287    }
288}
289
290impl fmt::Display for ProviderName {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        f.write_str(&self.0)
293    }
294}
295
296impl AsRef<str> for ProviderName {
297    fn as_ref(&self) -> &str {
298        &self.0
299    }
300}
301
302impl Borrow<str> for ProviderName {
303    fn borrow(&self) -> &str {
304        &self.0
305    }
306}
307
308impl From<&str> for ProviderName {
309    fn from(s: &str) -> Self {
310        Self(Arc::from(s))
311    }
312}
313
314impl From<String> for ProviderName {
315    fn from(s: String) -> Self {
316        Self(Arc::from(s.as_str()))
317    }
318}
319
320impl FromStr for ProviderName {
321    type Err = std::convert::Infallible;
322
323    fn from_str(s: &str) -> Result<Self, Self::Err> {
324        Ok(Self::from(s))
325    }
326}
327
328impl PartialEq<str> for ProviderName {
329    fn eq(&self, other: &str) -> bool {
330        self.0.as_ref() == other
331    }
332}
333
334impl PartialEq<&str> for ProviderName {
335    fn eq(&self, other: &&str) -> bool {
336        self.0.as_ref() == *other
337    }
338}
339
340impl PartialEq<String> for ProviderName {
341    fn eq(&self, other: &String) -> bool {
342        self.0.as_ref() == other.as_str()
343    }
344}
345
346impl PartialEq<ProviderName> for str {
347    fn eq(&self, other: &ProviderName) -> bool {
348        self == other.0.as_ref()
349    }
350}
351
352impl PartialEq<ProviderName> for String {
353    fn eq(&self, other: &ProviderName) -> bool {
354        self.as_str() == other.0.as_ref()
355    }
356}
357
358// ── SkillName ────────────────────────────────────────────────────────────────
359
360/// Strongly-typed skill name identifier.
361///
362/// `SkillName` identifies a skill by its canonical name (e.g., `"rust-agents"`,
363/// `"readme-generator"`). Names come from `SKILL.md` frontmatter `name:` fields and
364/// are used at match time, invocation routing, and telemetry.
365///
366/// # Inner type: `Arc<str>`
367///
368/// The inner type is `Arc<str>`. Skill names are referenced from multiple subsystems
369/// (registry, matcher, invoker, TUI) during a single agent turn. `Arc<str>` makes all
370/// clones O(1).
371///
372/// # No `Deref<Target=str>`
373///
374/// `SkillName` does **not** implement `Deref<Target=str>`. Use `.as_str()` for explicit
375/// string conversion and `.clone()` to duplicate.
376///
377/// # Examples
378///
379/// ```
380/// use zeph_common::SkillName;
381///
382/// let name = SkillName::new("rust-agents");
383/// assert_eq!(name.as_str(), "rust-agents");
384/// assert_eq!(name, "rust-agents");
385///
386/// // Clone is O(1) — Arc reference count increment only.
387/// let name2 = name.clone();
388/// assert_eq!(name, name2);
389/// ```
390#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
391#[serde(transparent)]
392pub struct SkillName(Arc<str>);
393
394impl SkillName {
395    /// Construct a `SkillName` from any value convertible to `Arc<str>`.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use zeph_common::SkillName;
401    ///
402    /// let name = SkillName::new("readme-generator");
403    /// assert_eq!(name.as_str(), "readme-generator");
404    /// ```
405    #[must_use]
406    pub fn new(s: impl Into<Arc<str>>) -> Self {
407        Self(s.into())
408    }
409
410    /// Return the inner string slice.
411    ///
412    /// # Examples
413    ///
414    /// ```
415    /// use zeph_common::SkillName;
416    ///
417    /// let name = SkillName::new("rust-agents");
418    /// assert_eq!(name.as_str(), "rust-agents");
419    /// ```
420    #[must_use]
421    pub fn as_str(&self) -> &str {
422        &self.0
423    }
424}
425
426impl Default for SkillName {
427    /// Returns an empty `SkillName`.
428    ///
429    /// Exists solely for `#[serde(default)]` on optional fields. Do not use in
430    /// application code — an empty name will fail skill lookup.
431    fn default() -> Self {
432        Self(Arc::from(""))
433    }
434}
435
436impl fmt::Display for SkillName {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        f.write_str(&self.0)
439    }
440}
441
442impl AsRef<str> for SkillName {
443    fn as_ref(&self) -> &str {
444        &self.0
445    }
446}
447
448impl Borrow<str> for SkillName {
449    fn borrow(&self) -> &str {
450        &self.0
451    }
452}
453
454impl From<&str> for SkillName {
455    fn from(s: &str) -> Self {
456        Self(Arc::from(s))
457    }
458}
459
460impl From<String> for SkillName {
461    fn from(s: String) -> Self {
462        Self(Arc::from(s.as_str()))
463    }
464}
465
466impl FromStr for SkillName {
467    type Err = std::convert::Infallible;
468
469    fn from_str(s: &str) -> Result<Self, Self::Err> {
470        Ok(Self::from(s))
471    }
472}
473
474impl PartialEq<str> for SkillName {
475    fn eq(&self, other: &str) -> bool {
476        self.0.as_ref() == other
477    }
478}
479
480impl PartialEq<&str> for SkillName {
481    fn eq(&self, other: &&str) -> bool {
482        self.0.as_ref() == *other
483    }
484}
485
486impl PartialEq<String> for SkillName {
487    fn eq(&self, other: &String) -> bool {
488        self.0.as_ref() == other.as_str()
489    }
490}
491
492impl PartialEq<SkillName> for str {
493    fn eq(&self, other: &SkillName) -> bool {
494        self == other.0.as_ref()
495    }
496}
497
498impl PartialEq<SkillName> for String {
499    fn eq(&self, other: &SkillName) -> bool {
500        self.as_str() == other.0.as_ref()
501    }
502}
503
504// ── SessionId ────────────────────────────────────────────────────────────────
505
506/// Identifies a single agent session (one binary invocation or one ACP connection).
507///
508/// Uses `String` internally to support both UUID-based IDs (production) and
509/// arbitrary string IDs (tests, experiments). UUID validation is enforced only at
510/// [`SessionId::generate`] time; [`SessionId::new`] accepts any non-empty string for
511/// flexibility in test fixtures.
512///
513/// # Serialization
514///
515/// `SessionId` uses `#[serde(transparent)]` — it serializes as a plain JSON string
516/// identical to the raw `String` fields it replaces. No wire format change, no DB
517/// schema migration required.
518///
519/// # ACP Note
520///
521/// `acp::SessionId` from the external `agent_client_protocol` crate is distinct.
522/// This type is for **our own** session tracking only.
523///
524/// # Examples
525///
526/// ```
527/// use zeph_common::SessionId;
528///
529/// // Production: generate a fresh UUID session
530/// let id = SessionId::generate();
531/// assert!(!id.as_str().is_empty());
532///
533/// // Tests: use a readable fixture string
534/// let test_id = SessionId::new("test-session");
535/// assert_eq!(test_id.as_str(), "test-session");
536/// ```
537#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
538#[serde(transparent)]
539pub struct SessionId(String);
540
541impl SessionId {
542    /// Create a `SessionId` from any non-empty string.
543    ///
544    /// Accepts UUID strings (production), readable names (tests), or any other
545    /// non-empty value. In debug builds, an empty string triggers a `debug_assert!`
546    /// to catch accidental construction early.
547    ///
548    /// # Panics
549    ///
550    /// Panics in **debug builds only** if `s` is empty.
551    ///
552    /// # Examples
553    ///
554    /// ```
555    /// use zeph_common::SessionId;
556    ///
557    /// let id = SessionId::new("test-session");
558    /// assert_eq!(id.as_str(), "test-session");
559    /// ```
560    pub fn new(s: impl Into<String>) -> Self {
561        let s = s.into();
562        debug_assert!(!s.is_empty(), "SessionId must not be empty");
563        Self(s)
564    }
565
566    /// Generate a new session ID backed by a random UUID v4.
567    ///
568    /// # Examples
569    ///
570    /// ```
571    /// use zeph_common::SessionId;
572    ///
573    /// let id = SessionId::generate();
574    /// assert!(!id.as_str().is_empty());
575    /// // UUIDs are 36 chars: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
576    /// assert_eq!(id.as_str().len(), 36);
577    /// ```
578    #[must_use]
579    pub fn generate() -> Self {
580        Self(uuid::Uuid::new_v4().to_string())
581    }
582
583    /// Return the inner string slice.
584    ///
585    /// # Examples
586    ///
587    /// ```
588    /// use zeph_common::SessionId;
589    ///
590    /// let id = SessionId::new("s1");
591    /// assert_eq!(id.as_str(), "s1");
592    /// ```
593    #[must_use]
594    pub fn as_str(&self) -> &str {
595        &self.0
596    }
597}
598
599impl Default for SessionId {
600    /// Generate a new UUID-backed session ID.
601    fn default() -> Self {
602        Self::generate()
603    }
604}
605
606impl fmt::Display for SessionId {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        f.write_str(&self.0)
609    }
610}
611
612impl AsRef<str> for SessionId {
613    fn as_ref(&self) -> &str {
614        &self.0
615    }
616}
617
618impl std::ops::Deref for SessionId {
619    type Target = str;
620
621    fn deref(&self) -> &str {
622        &self.0
623    }
624}
625
626impl From<String> for SessionId {
627    fn from(s: String) -> Self {
628        Self::new(s)
629    }
630}
631
632impl From<&str> for SessionId {
633    fn from(s: &str) -> Self {
634        Self::new(s)
635    }
636}
637
638impl From<uuid::Uuid> for SessionId {
639    fn from(u: uuid::Uuid) -> Self {
640        Self(u.to_string())
641    }
642}
643
644impl FromStr for SessionId {
645    type Err = std::convert::Infallible;
646
647    fn from_str(s: &str) -> Result<Self, Self::Err> {
648        Ok(Self::new(s))
649    }
650}
651
652impl PartialEq<str> for SessionId {
653    fn eq(&self, other: &str) -> bool {
654        self.0 == other
655    }
656}
657
658impl PartialEq<&str> for SessionId {
659    fn eq(&self, other: &&str) -> bool {
660        self.0 == *other
661    }
662}
663
664impl PartialEq<String> for SessionId {
665    fn eq(&self, other: &String) -> bool {
666        self.0 == *other
667    }
668}
669
670impl PartialEq<SessionId> for str {
671    fn eq(&self, other: &SessionId) -> bool {
672        self == other.0
673    }
674}
675
676impl PartialEq<SessionId> for String {
677    fn eq(&self, other: &SessionId) -> bool {
678        *self == other.0
679    }
680}
681
682// ── ToolDefinition ───────────────────────────────────────────────────────────
683
684/// Minimal tool definition passed to LLM providers.
685///
686/// Decoupled from `zeph-tools::ToolDef` to avoid cross-crate dependencies.
687/// Providers translate this into their native tool/function format before sending to the API.
688///
689/// # Examples
690///
691/// ```
692/// use zeph_common::types::ToolDefinition;
693/// use zeph_common::ToolName;
694///
695/// let tool = ToolDefinition {
696///     name: ToolName::new("get_weather"),
697///     description: "Return current weather for a city.".into(),
698///     parameters: serde_json::json!({
699///         "type": "object",
700///         "properties": {
701///             "city": { "type": "string" }
702///         },
703///         "required": ["city"]
704///     }),
705///     output_schema: None,
706/// };
707/// assert_eq!(tool.name, "get_weather");
708/// ```
709#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
710pub struct ToolDefinition {
711    /// Tool name — must match the name used in the response `ToolUseRequest`.
712    pub name: ToolName,
713    /// Human-readable description guiding the model on when to call this tool.
714    pub description: String,
715    /// JSON Schema object describing parameters.
716    pub parameters: serde_json::Value,
717    /// Raw output schema advertised by the MCP server, if present.
718    ///
719    /// When `mcp.forward_output_schema = true`, LLM provider assemblers append a compact JSON
720    /// hint to the tool description rather than adding a new top-level field (unsupported by
721    /// the Anthropic and `OpenAI` APIs).
722    ///
723    /// DO NOT convert to `schemars::Schema` — lossy; see #2931 critique P0-1.
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    pub output_schema: Option<serde_json::Value>,
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731
732    #[test]
733    fn tool_name_construction_and_equality() {
734        let name = ToolName::new("shell");
735        assert_eq!(name.as_str(), "shell");
736        assert_eq!(name, "shell");
737        assert_eq!(name, "shell".to_owned());
738        assert_eq!(name, "shell"); // symmetric check via PartialEq<str>
739    }
740
741    #[test]
742    fn tool_name_clone_is_cheap() {
743        let name = ToolName::new("web_scrape");
744        let name2 = name.clone();
745        assert_eq!(name, name2);
746        // Both Arc<str> point to same allocation
747        assert!(Arc::ptr_eq(&name.0, &name2.0));
748    }
749
750    #[test]
751    fn tool_name_from_impls() {
752        let from_str: ToolName = ToolName::from("bash");
753        let from_string: ToolName = ToolName::from("bash".to_owned());
754        let parsed: ToolName = "bash".parse().unwrap();
755        assert_eq!(from_str, from_string);
756        assert_eq!(from_str, parsed);
757    }
758
759    #[test]
760    fn tool_name_as_hashmap_key() {
761        use std::collections::HashMap;
762        let mut map: HashMap<ToolName, u32> = HashMap::new();
763        map.insert(ToolName::new("shell"), 1);
764        // Borrow<str> enables lookup by &str
765        assert_eq!(map.get("shell"), Some(&1));
766    }
767
768    #[test]
769    fn tool_name_display() {
770        let name = ToolName::new("my_tool");
771        assert_eq!(format!("{name}"), "my_tool");
772    }
773
774    #[test]
775    fn tool_name_serde_transparent() {
776        let name = ToolName::new("shell");
777        let json = serde_json::to_string(&name).unwrap();
778        assert_eq!(json, r#""shell""#);
779        let back: ToolName = serde_json::from_str(&json).unwrap();
780        assert_eq!(back, name);
781    }
782
783    #[test]
784    fn session_id_new_roundtrip() {
785        let id = SessionId::new("test-session");
786        assert_eq!(id.as_str(), "test-session");
787        assert_eq!(id.to_string(), "test-session");
788    }
789
790    #[test]
791    fn session_id_generate_is_uuid() {
792        let id = SessionId::generate();
793        assert_eq!(id.as_str().len(), 36);
794        assert!(uuid::Uuid::parse_str(id.as_str()).is_ok());
795    }
796
797    #[test]
798    fn session_id_default_is_generated() {
799        let id = SessionId::default();
800        assert!(!id.as_str().is_empty());
801        assert_eq!(id.as_str().len(), 36);
802    }
803
804    #[test]
805    fn session_id_from_uuid() {
806        let u = uuid::Uuid::new_v4();
807        let id = SessionId::from(u);
808        assert_eq!(id.as_str(), u.to_string());
809    }
810
811    #[test]
812    fn session_id_deref_slicing() {
813        let id = SessionId::new("abcdefgh");
814        // Deref<Target=str> enables string slicing
815        assert_eq!(&id[..4], "abcd");
816    }
817
818    #[test]
819    fn session_id_serde_transparent() {
820        let id = SessionId::new("sess-abc");
821        let json = serde_json::to_string(&id).unwrap();
822        assert_eq!(json, r#""sess-abc""#);
823        let back: SessionId = serde_json::from_str(&json).unwrap();
824        assert_eq!(back, id);
825    }
826
827    #[test]
828    fn session_id_from_str_parses() {
829        let id: SessionId = "my-session".parse().unwrap();
830        assert_eq!(id.as_str(), "my-session");
831    }
832
833    #[test]
834    fn provider_name_construction_and_equality() {
835        let name = ProviderName::new("fast");
836        assert_eq!(name.as_str(), "fast");
837        assert_eq!(name, "fast");
838        assert_eq!(name, "fast".to_owned());
839    }
840
841    #[test]
842    fn provider_name_clone_is_cheap() {
843        let name = ProviderName::new("quality");
844        let name2 = name.clone();
845        assert_eq!(name, name2);
846        assert!(Arc::ptr_eq(&name.0, &name2.0));
847    }
848
849    #[test]
850    fn provider_name_from_impls() {
851        let from_str: ProviderName = ProviderName::from("fast");
852        let from_string: ProviderName = ProviderName::from("fast".to_owned());
853        let parsed: ProviderName = "fast".parse().unwrap();
854        assert_eq!(from_str, from_string);
855        assert_eq!(from_str, parsed);
856    }
857
858    #[test]
859    fn provider_name_as_hashmap_key() {
860        use std::collections::HashMap;
861        let mut map: HashMap<ProviderName, u32> = HashMap::new();
862        map.insert(ProviderName::new("fast"), 1);
863        assert_eq!(map.get("fast"), Some(&1));
864    }
865
866    #[test]
867    fn provider_name_display() {
868        let name = ProviderName::new("ollama-local");
869        assert_eq!(format!("{name}"), "ollama-local");
870    }
871
872    #[test]
873    fn provider_name_serde_transparent() {
874        let name = ProviderName::new("quality");
875        let json = serde_json::to_string(&name).unwrap();
876        assert_eq!(json, r#""quality""#);
877        let back: ProviderName = serde_json::from_str(&json).unwrap();
878        assert_eq!(back, name);
879    }
880
881    #[test]
882    fn skill_name_construction_and_equality() {
883        let name = SkillName::new("rust-agents");
884        assert_eq!(name.as_str(), "rust-agents");
885        assert_eq!(name, "rust-agents");
886        assert_eq!(name, "rust-agents".to_owned());
887    }
888
889    #[test]
890    fn skill_name_clone_is_cheap() {
891        let name = SkillName::new("readme-generator");
892        let name2 = name.clone();
893        assert_eq!(name, name2);
894        assert!(Arc::ptr_eq(&name.0, &name2.0));
895    }
896
897    #[test]
898    fn skill_name_from_impls() {
899        let from_str: SkillName = SkillName::from("rust-agents");
900        let from_string: SkillName = SkillName::from("rust-agents".to_owned());
901        let parsed: SkillName = "rust-agents".parse().unwrap();
902        assert_eq!(from_str, from_string);
903        assert_eq!(from_str, parsed);
904    }
905
906    #[test]
907    fn skill_name_as_hashmap_key() {
908        use std::collections::HashMap;
909        let mut map: HashMap<SkillName, u32> = HashMap::new();
910        map.insert(SkillName::new("rust-agents"), 1);
911        assert_eq!(map.get("rust-agents"), Some(&1));
912    }
913
914    #[test]
915    fn skill_name_display() {
916        let name = SkillName::new("readme-generator");
917        assert_eq!(format!("{name}"), "readme-generator");
918    }
919
920    #[test]
921    fn skill_name_serde_transparent() {
922        let name = SkillName::new("rust-agents");
923        let json = serde_json::to_string(&name).unwrap();
924        assert_eq!(json, r#""rust-agents""#);
925        let back: SkillName = serde_json::from_str(&json).unwrap();
926        assert_eq!(back, name);
927    }
928}