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}