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}