Skip to main content

ares_types/types/
mod.rs

1//! Core types used throughout the A.R.E.S server.
2//!
3//! This module contains all the common data structures used for:
4//! - API requests and responses
5//! - Agent configuration and context
6//! - Memory and user preferences
7//! - Tool definitions and calls
8//! - RAG (Retrieval Augmented Generation)
9//! - Authentication
10//! - Error handling
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15/// Default datetime for serde deserialization
16fn default_datetime() -> DateTime<Utc> {
17    Utc::now()
18}
19
20// ============= API Request/Response Types =============
21
22/// A multimodal content part on a chat request or conversation message.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum ContentPart {
26    /// UTF-8 text.
27    Text {
28        /// Text payload.
29        text: String,
30    },
31    /// Image referenced by URL.
32    ImageUrl {
33        /// Image URL.
34        url: String,
35    },
36    /// Image as a base64 payload.
37    ImageBase64 {
38        /// MIME type, e.g. `image/png`.
39        mime: String,
40        /// Base64-encoded bytes.
41        data: String,
42    },
43    /// File or PDF referenced by URL.
44    FileUrl {
45        /// File URL.
46        url: String,
47        /// Optional MIME type.
48        #[serde(default, skip_serializing_if = "Option::is_none")]
49        mime: Option<String>,
50    },
51    /// File or PDF as a base64 payload.
52    FileBase64 {
53        /// MIME type, e.g. `application/pdf`.
54        mime: String,
55        /// Base64-encoded bytes.
56        data: String,
57        /// Optional file name.
58        #[serde(default, skip_serializing_if = "Option::is_none")]
59        name: Option<String>,
60    },
61}
62
63/// Request payload for chat endpoints.
64#[derive(Debug, Serialize, Deserialize)]
65pub struct ChatRequest {
66    /// The user's message to send to the agent.
67    pub message: String,
68    /// Optional agent type to handle the request. Defaults to router.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub agent_type: Option<AgentType>,
71    /// Optional context ID for conversation continuity.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub context_id: Option<String>,
74    /// Optional Eruka workspace_id for per-user context isolation.
75    /// When set, the Eruka context middleware queries this workspace instead of the default.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub workspace_id: Option<String>,
78    /// Optional per-request model override (Cordis intercept payload).
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub model: Option<String>,
81    /// Optional multimodal parts. When set, they travel on the wire with `message`.
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub parts: Option<Vec<ContentPart>>,
84    /// OpenAI Responses stateful continuation id.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub previous_response_id: Option<String>,
87    /// When true, attach the LLM provider's built-in web search (not daedra).
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub web_search: Option<bool>,
90}
91
92/// Response from chat endpoints.
93#[derive(Debug, Serialize, Deserialize)]
94pub struct ChatResponse {
95    /// The agent's response text.
96    pub response: String,
97    /// The name of the agent that handled the request.
98    pub agent: String,
99    /// Context ID for continuing this conversation.
100    pub context_id: String,
101    /// Optional sources used to generate the response.
102    pub sources: Option<Vec<Source>>,
103}
104
105/// A source reference used in responses.
106#[derive(Debug, Serialize, Deserialize, Clone)]
107pub struct Source {
108    /// Title of the source document or webpage.
109    pub title: String,
110    /// URL of the source, if available.
111    pub url: Option<String>,
112    /// Relevance score (0.0 to 1.0) indicating how relevant this source is.
113    pub relevance_score: f32,
114}
115
116/// Request payload for deep research endpoints.
117#[derive(Debug, Serialize, Deserialize)]
118pub struct ResearchRequest {
119    /// The research query or question.
120    pub query: String,
121    /// Optional maximum depth for recursive research (default: 3).
122    pub depth: Option<u8>,
123    /// Optional maximum iterations across all agents (default: 10).
124    pub max_iterations: Option<u8>,
125}
126
127/// Response from deep research endpoints.
128#[derive(Debug, Serialize, Deserialize)]
129pub struct ResearchResponse {
130    /// The compiled research findings.
131    pub findings: String,
132    /// Sources discovered during research.
133    pub sources: Vec<Source>,
134    /// Time taken for the research in milliseconds.
135    pub duration_ms: u64,
136}
137
138// ============= RAG API Types =============
139
140/// Request to ingest a document into the RAG system.
141#[derive(Debug, Serialize, Deserialize)]
142pub struct RagIngestRequest {
143    /// Collection name to ingest into.
144    pub collection: String,
145    /// The text content to ingest.
146    pub content: String,
147    /// Optional document title.
148    pub title: Option<String>,
149    /// Optional source URL or path.
150    pub source: Option<String>,
151    /// Optional tags for categorization.
152    #[serde(default)]
153    pub tags: Vec<String>,
154    /// Chunking strategy to use.
155    #[serde(default)]
156    pub chunking_strategy: Option<String>,
157}
158
159/// Response from document ingestion.
160#[derive(Debug, Serialize, Deserialize)]
161pub struct RagIngestResponse {
162    /// Number of chunks created.
163    pub chunks_created: usize,
164    /// Document IDs created.
165    pub document_ids: Vec<String>,
166    /// Collection name.
167    pub collection: String,
168}
169
170/// Request to search the RAG system.
171#[derive(Debug, Serialize, Deserialize)]
172pub struct RagSearchRequest {
173    /// Collection to search.
174    pub collection: String,
175    /// The search query.
176    pub query: String,
177    /// Maximum results to return (default: 10).
178    #[serde(default = "default_search_limit")]
179    pub limit: usize,
180    /// Search strategy to use: semantic, bm25, fuzzy, hybrid.
181    #[serde(default)]
182    pub strategy: Option<String>,
183    /// Minimum similarity threshold (0.0 to 1.0).
184    #[serde(default = "default_search_threshold")]
185    pub threshold: f32,
186    /// Whether to enable reranking.
187    #[serde(default)]
188    pub rerank: bool,
189    /// Reranker model to use if reranking.
190    #[serde(default)]
191    pub reranker_model: Option<String>,
192}
193
194fn default_search_limit() -> usize {
195    10
196}
197
198fn default_search_threshold() -> f32 {
199    0.0
200}
201
202/// Single search result.
203#[derive(Debug, Serialize, Deserialize)]
204pub struct RagSearchResult {
205    /// Document ID.
206    pub id: String,
207    /// Matching text content.
208    pub content: String,
209    /// Relevance score.
210    pub score: f32,
211    /// Document metadata.
212    pub metadata: DocumentMetadata,
213}
214
215/// Response from RAG search.
216#[derive(Debug, Serialize, Deserialize)]
217pub struct RagSearchResponse {
218    /// Search results.
219    pub results: Vec<RagSearchResult>,
220    /// Total number of results before limit.
221    pub total: usize,
222    /// Search strategy used.
223    pub strategy: String,
224    /// Whether reranking was applied.
225    pub reranked: bool,
226    /// Query processing time in milliseconds.
227    pub duration_ms: u64,
228}
229
230/// Request to delete a collection.
231#[derive(Debug, Serialize, Deserialize)]
232pub struct RagDeleteCollectionRequest {
233    /// Collection name to delete.
234    pub collection: String,
235}
236
237/// Response from collection deletion.
238#[derive(Debug, Serialize, Deserialize)]
239pub struct RagDeleteCollectionResponse {
240    /// Whether deletion was successful.
241    pub success: bool,
242    /// Collection that was deleted.
243    pub collection: String,
244    /// Number of documents deleted.
245    pub documents_deleted: usize,
246}
247
248// ============= Workflow Types =============
249
250// ============= Semantic Search Types =============
251
252/// Request for semantic document search.
253/// Available only when `ares-vector` feature is enabled.
254#[derive(Debug, Serialize, Deserialize)]
255pub struct SemanticSearchRequest {
256    /// Collection to search (tenant-scoped).
257    pub collection: String,
258    /// The search query text to embed.
259    pub query: String,
260    /// Maximum results to return (default: 10, max: 100).
261    #[serde(default = "default_search_limit")]
262    pub limit: usize,
263    /// Minimum similarity threshold (0.0 to 1.0, default: 0.0).
264    #[serde(default = "default_search_threshold")]
265    pub threshold: f32,
266}
267
268/// Single semantic search result.
269#[derive(Debug, Serialize, Deserialize)]
270pub struct SemanticSearchResult {
271    /// Document ID.
272    pub id: String,
273    /// Matching text content.
274    pub content: String,
275    /// Similarity score (0.0 to 1.0, higher is better).
276    pub similarity: f32,
277    /// Document metadata.
278    pub metadata: DocumentMetadata,
279}
280
281/// Response from semantic search.
282#[derive(Debug, Serialize, Deserialize)]
283pub struct SemanticSearchResponse {
284    /// Search results.
285    pub results: Vec<SemanticSearchResult>,
286    /// Total number of results found.
287    pub total: usize,
288    /// Query processing time in milliseconds.
289    pub duration_ms: u64,
290}
291
292/// Request payload for workflow execution endpoints.
293#[derive(Debug, Serialize, Deserialize)]
294pub struct WorkflowRequest {
295    /// The query to process through the workflow.
296    pub query: String,
297    /// Additional context data as key-value pairs.
298    #[serde(default)]
299    pub context: std::collections::HashMap<String, serde_json::Value>,
300}
301
302// ============= Agent Types =============
303
304/// Available agent types in the system.
305///
306/// This enum supports both built-in agent types and custom user-defined agents.
307/// The `Custom` variant allows for extensibility without modifying this enum.
308#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
309#[serde(rename_all = "lowercase")]
310#[non_exhaustive]
311pub enum AgentType {
312    /// Routes requests to appropriate specialized agents.
313    Router,
314    /// Orchestrates complex multi-step tasks.
315    Orchestrator,
316    /// Handles product-related queries.
317    Product,
318    /// Handles invoice and billing queries.
319    Invoice,
320    /// Handles sales-related queries.
321    Sales,
322    /// Handles financial queries and analysis.
323    Finance,
324    /// Handles HR and employee-related queries.
325    #[serde(rename = "hr")]
326    HR,
327    /// Custom user-defined agent type.
328    /// The string contains the agent's unique identifier/name.
329    #[serde(untagged)]
330    Custom(String),
331}
332
333impl AgentType {
334    /// Returns the agent type name as a string slice.
335    pub fn as_str(&self) -> &str {
336        match self {
337            AgentType::Router => "router",
338            AgentType::Orchestrator => "orchestrator",
339            AgentType::Product => "product",
340            AgentType::Invoice => "invoice",
341            AgentType::Sales => "sales",
342            AgentType::Finance => "finance",
343            AgentType::HR => "hr",
344            AgentType::Custom(name) => name,
345        }
346    }
347
348    /// Creates an AgentType from a string, using built-in types when possible.
349    pub fn from_string(s: &str) -> Self {
350        match s.to_lowercase().as_str() {
351            "router" => AgentType::Router,
352            "orchestrator" => AgentType::Orchestrator,
353            "product" => AgentType::Product,
354            "invoice" => AgentType::Invoice,
355            "sales" => AgentType::Sales,
356            "finance" => AgentType::Finance,
357            "hr" => AgentType::HR,
358            _ => AgentType::Custom(s.to_string()),
359        }
360    }
361
362    /// Returns true if this is a built-in agent type.
363    pub fn is_builtin(&self) -> bool {
364        !matches!(self, AgentType::Custom(_))
365    }
366}
367
368impl std::fmt::Display for AgentType {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        write!(f, "{}", self.as_str())
371    }
372}
373
374/// Context passed to agents during request processing.
375#[derive(Debug, Clone)]
376pub struct AgentContext {
377    /// Unique identifier for the user making the request.
378    pub user_id: String,
379    /// Session identifier for conversation tracking.
380    pub session_id: String,
381    /// Previous messages in the conversation.
382    pub conversation_history: Vec<Message>,
383    /// User's stored memory and preferences.
384    pub user_memory: Option<UserMemory>,
385}
386
387/// A single message in a conversation.
388#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct Message {
390    /// The role of the message sender.
391    pub role: MessageRole,
392    /// The message content.
393    pub content: String,
394    /// When the message was sent.
395    pub timestamp: DateTime<Utc>,
396    /// Multimodal parts. Empty means text-only. serde default `[]`.
397    #[serde(default, skip_serializing_if = "Vec::is_empty")]
398    pub parts: Vec<ContentPart>,
399}
400
401/// Role of a message sender in a conversation.
402#[derive(Debug, Clone, Serialize, Deserialize)]
403#[serde(rename_all = "lowercase")]
404pub enum MessageRole {
405    /// System instructions to the model.
406    System,
407    /// Message from the user.
408    User,
409    /// Response from the assistant/agent.
410    Assistant,
411}
412
413// ============= Memory Types =============
414
415/// User memory containing preferences and learned facts.
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct UserMemory {
418    /// The user's unique identifier.
419    pub user_id: String,
420    /// List of user preferences.
421    pub preferences: Vec<Preference>,
422    /// List of facts learned about the user.
423    pub facts: Vec<MemoryFact>,
424}
425
426/// A user preference entry.
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct Preference {
429    /// Category of the preference (e.g., "communication", "output").
430    pub category: String,
431    /// Key identifying the specific preference.
432    pub key: String,
433    /// The preference value.
434    pub value: String,
435    /// Confidence score (0.0 to 1.0) for this preference.
436    pub confidence: f32,
437}
438
439/// A fact learned about a user.
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct MemoryFact {
442    /// Unique identifier for this fact.
443    pub id: String,
444    /// The user this fact belongs to.
445    pub user_id: String,
446    /// Category of the fact (e.g., "personal", "work").
447    pub category: String,
448    /// Key identifying the specific fact.
449    pub fact_key: String,
450    /// The fact value.
451    pub fact_value: String,
452    /// Confidence score (0.0 to 1.0) for this fact.
453    pub confidence: f32,
454    /// When this fact was first recorded.
455    pub created_at: DateTime<Utc>,
456    /// When this fact was last updated.
457    pub updated_at: DateTime<Utc>,
458}
459
460// ============= Tool Types =============
461
462/// Definition of a tool that can be called by an LLM.
463#[derive(Debug, Serialize, Deserialize, Clone)]
464pub struct ToolDefinition {
465    /// Unique name of the tool.
466    pub name: String,
467    /// Human-readable description of what the tool does.
468    pub description: String,
469    /// JSON Schema defining the tool's parameters.
470    pub parameters: serde_json::Value,
471}
472
473/// A request to call a tool.
474#[derive(Debug, Serialize, Deserialize, Clone)]
475pub struct ToolCall {
476    /// Unique identifier for this tool call.
477    pub id: String,
478    /// Name of the tool to call.
479    pub name: String,
480    /// Arguments to pass to the tool.
481    pub arguments: serde_json::Value,
482}
483
484/// Result from executing a tool.
485#[derive(Debug, Serialize, Deserialize)]
486pub struct ToolResult {
487    /// ID of the tool call this result corresponds to.
488    pub tool_call_id: String,
489    /// The result data from the tool execution.
490    pub result: serde_json::Value,
491}
492
493// ============= RAG Types =============
494
495/// A document in the RAG knowledge base.
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct Document {
498    /// Unique identifier for the document.
499    pub id: String,
500    /// The document's text content.
501    pub content: String,
502    /// Metadata about the document.
503    pub metadata: DocumentMetadata,
504    /// Optional embedding vector for semantic search.
505    pub embedding: Option<Vec<f32>>,
506}
507
508/// Metadata associated with a document.
509#[derive(Debug, Clone, Default, Serialize, Deserialize)]
510pub struct DocumentMetadata {
511    /// Title of the document.
512    #[serde(default)]
513    pub title: String,
514    /// Source of the document (e.g., URL, file path).
515    #[serde(default)]
516    pub source: String,
517    /// When the document was created or ingested.
518    #[serde(default = "default_datetime")]
519    pub created_at: DateTime<Utc>,
520    /// Tags for categorization and filtering.
521    #[serde(default)]
522    pub tags: Vec<String>,
523}
524
525/// Query parameters for semantic search.
526#[derive(Debug, Clone)]
527pub struct SearchQuery {
528    /// The search query text.
529    pub query: String,
530    /// Maximum number of results to return.
531    pub limit: usize,
532    /// Minimum similarity threshold (0.0 to 1.0).
533    pub threshold: f32,
534    /// Optional filters to apply to results.
535    pub filters: Option<Vec<SearchFilter>>,
536}
537
538/// A filter to apply during search.
539#[derive(Debug, Clone)]
540pub struct SearchFilter {
541    /// Field name to filter on.
542    pub field: String,
543    /// Value to filter by.
544    pub value: String,
545}
546
547/// A single search result with relevance score.
548#[derive(Debug, Clone)]
549pub struct SearchResult {
550    /// The matching document.
551    pub document: Document,
552    /// Similarity score (0.0 to 1.0).
553    pub score: f32,
554}
555
556// ============= Authentication Types =============
557
558/// Request payload for user login.
559#[derive(Debug, Serialize, Deserialize)]
560pub struct LoginRequest {
561    /// User's email address.
562    pub email: String,
563    /// User's password.
564    pub password: String,
565}
566
567/// Request payload for user registration.
568#[derive(Debug, Serialize, Deserialize)]
569pub struct RegisterRequest {
570    /// Email address for the new account.
571    pub email: String,
572    /// Password for the new account.
573    pub password: String,
574    /// Display name for the user.
575    pub name: String,
576}
577
578/// Response containing authentication tokens.
579#[derive(Debug, Serialize, Deserialize)]
580pub struct TokenResponse {
581    /// JWT access token for API authentication.
582    pub access_token: String,
583    /// Refresh token for obtaining new access tokens.
584    pub refresh_token: String,
585    /// Time in seconds until the access token expires.
586    pub expires_in: i64,
587}
588
589/// JWT claims embedded in access tokens.
590#[derive(Debug, Serialize, Deserialize, Clone)]
591pub struct Claims {
592    /// Subject (user ID).
593    pub sub: String,
594    /// User's email address.
595    pub email: String,
596    /// Expiration time (Unix timestamp).
597    pub exp: usize,
598    /// Issued at time (Unix timestamp).
599    pub iat: usize,
600    /// JWT ID — unique per token (present on refresh tokens).
601    #[serde(default, skip_serializing_if = "String::is_empty")]
602    pub jti: String,
603    /// Tenant that issued or owns this session, when the token is tenant-scoped.
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub tenant_id: Option<String>,
606}
607
608// ============= Error Types =============
609
610/// Error codes for programmatic error handling.
611/// These are stable identifiers that clients can use to handle specific error cases.
612#[derive(Debug, Clone, Copy, Serialize)]
613#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
614pub enum ErrorCode {
615    /// Database operation failed
616    DatabaseError,
617    /// LLM/AI model operation failed
618    LlmError,
619    /// Authentication failed (invalid credentials)
620    AuthenticationFailed,
621    /// Authorization failed (valid credentials but insufficient permissions)
622    AuthorizationFailed,
623    /// Requested resource was not found
624    NotFound,
625    /// Input validation failed
626    InvalidInput,
627    /// Server configuration error
628    ConfigurationError,
629    /// External service (API, webhook, etc.) failed
630    ExternalServiceError,
631    /// Internal server error
632    InternalError,
633}
634
635/// Application-wide error type.
636#[derive(Debug, thiserror::Error)]
637pub enum AppError {
638    /// Database operation failed.
639    #[error("Database error: {0}")]
640    Database(String),
641
642    /// LLM operation failed.
643    #[error("LLM error: {0}")]
644    LLM(String),
645
646    /// Authentication or authorization failed.
647    #[error("Authentication error: {0}")]
648    Auth(String),
649
650    /// Requested resource was not found.
651    #[error("Not found: {0}")]
652    NotFound(String),
653
654    /// Input validation failed.
655    #[error("Invalid input: {0}")]
656    InvalidInput(String),
657
658    /// Configuration error.
659    #[error("Configuration error: {0}")]
660    Configuration(String),
661
662    /// External service call failed.
663    #[error("External service error: {0}")]
664    External(String),
665
666    /// Internal server error.
667    #[error("Internal error: {0}")]
668    Internal(String),
669
670    /// Service temporarily unavailable (emergency stop, maintenance).
671    #[error("Service unavailable: {0}")]
672 Unavailable(String),
673    /// RAG feature was disabled by configuration
674    #[error("Feature disabled: {0}")]
675    FeatureDisabled(String),
676
677 /// Rate limit / quota exceeded.
678 #[error("Rate limited: {0}")]
679 RateLimited(String),
680}
681
682impl AppError {
683    /// Get the error code for this error type.
684    pub fn code(&self) -> ErrorCode {
685        match self {
686            AppError::Database(_) => ErrorCode::DatabaseError,
687            AppError::LLM(_) => ErrorCode::LlmError,
688            AppError::Auth(_) => ErrorCode::AuthenticationFailed,
689            AppError::NotFound(_) => ErrorCode::NotFound,
690            AppError::InvalidInput(_) => ErrorCode::InvalidInput,
691            AppError::Configuration(_) => ErrorCode::ConfigurationError,
692            AppError::External(_) => ErrorCode::ExternalServiceError,
693            AppError::Internal(_) => ErrorCode::InternalError,
694            AppError::Unavailable(_) => ErrorCode::InternalError,
695AppError::RateLimited(_) => ErrorCode::InternalError,
696AppError::FeatureDisabled(_) => ErrorCode::InternalError,
697    }
698    }
699
700    /// Check if this error is transient and retrying with a fallback provider
701    /// may succeed (timeout, rate limit, 5xx upstream).
702    pub fn is_retryable(&self) -> bool {
703        matches!(
704            self,
705            AppError::External(_) | AppError::Unavailable(_) | AppError::RateLimited(_)
706        )
707    }
708
709    /// HTTP status code as u16 (no axum dependency).
710    pub fn status_code(&self) -> u16 {
711        match self {
712            AppError::Database(_) => 500,
713            AppError::LLM(_) => 500,
714            AppError::Auth(_) => 401,
715            AppError::NotFound(_) => 404,
716            AppError::InvalidInput(_) => 400,
717            AppError::Configuration(_) => 500,
718            AppError::External(_) => 502,
719            AppError::Internal(_) => 500,
720            AppError::Unavailable(_) => 503,
721            AppError::RateLimited(_) => 429,
722            AppError::FeatureDisabled(_) => 400,
723        }
724    }
725}
726
727// ============= Error Conversions =============
728
729impl From<std::io::Error> for AppError {
730    fn from(err: std::io::Error) -> Self {
731        AppError::Internal(format!("IO error: {}", err))
732    }
733}
734
735impl From<serde_json::Error> for AppError {
736    fn from(err: serde_json::Error) -> Self {
737        AppError::InvalidInput(format!("JSON error: {}", err))
738    }
739}
740
741/// A specialized Result type for A.R.E.S operations.
742pub type Result<T> = std::result::Result<T, AppError>;
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747    use chrono::{TimeZone, Utc};
748
749    #[test]
750    fn test_agent_type_display_all_builtins() {
751        let cases = vec![
752            (AgentType::Router, "router"),
753            (AgentType::Orchestrator, "orchestrator"),
754            (AgentType::Product, "product"),
755            (AgentType::Invoice, "invoice"),
756            (AgentType::Sales, "sales"),
757            (AgentType::Finance, "finance"),
758            (AgentType::HR, "hr"),
759        ];
760        for (agent, expected) in cases {
761            assert_eq!(agent.to_string(), expected);
762            assert_eq!(format!("{}", agent), expected);
763        }
764    }
765
766    #[test]
767    fn test_agent_type_custom_display() {
768        let custom = AgentType::Custom("my-agent".into());
769        assert_eq!(custom.to_string(), "my-agent");
770        assert!(!custom.is_builtin());
771    }
772
773    #[test]
774    fn test_agent_type_from_string_roundtrip() {
775        for name in ["router", "finance", "hr"] {
776            let agent = AgentType::from_string(name);
777            assert_eq!(agent.as_str(), name);
778        }
779        let custom = AgentType::from_string("custom-bot");
780        assert_eq!(custom.as_str(), "custom-bot");
781    }
782
783    #[test]
784    fn test_message_role_serde_roundtrip() {
785        let role = MessageRole::Assistant;
786        let json = serde_json::to_string(&role).unwrap();
787        assert_eq!(json, "\"assistant\"");
788        let parsed: MessageRole = serde_json::from_str(&json).unwrap();
789        assert!(matches!(parsed, MessageRole::Assistant));
790    }
791
792    #[test]
793    fn test_chat_request_serde_roundtrip() {
794        let req = ChatRequest {
795            message: "hello".into(),
796            agent_type: Some(AgentType::Router),
797            context_id: Some("ctx-1".into()),
798            workspace_id: None,
799            model: None,
800            parts: None,
801            previous_response_id: None,
802            web_search: None,
803        };
804        let json = serde_json::to_string(&req).unwrap();
805        let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
806        assert_eq!(parsed.message, "hello");
807        assert_eq!(parsed.agent_type, Some(AgentType::Router));
808    }
809
810    #[test]
811    fn test_source_serde_roundtrip() {
812        let source = Source {
813            title: "Doc".into(),
814            url: Some("https://example.com".into()),
815            relevance_score: 0.9,
816        };
817        let parsed: Source = serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
818        assert_eq!(parsed.title, "Doc");
819        assert_eq!(parsed.relevance_score, 0.9);
820    }
821
822    #[test]
823    fn test_document_metadata_default_datetime() {
824        let json = r#"{"title":"t","source":"s"}"#;
825        let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
826        assert_eq!(meta.title, "t");
827        assert!(meta.created_at <= Utc::now());
828    }
829
830    #[test]
831    fn test_tool_call_serde_roundtrip() {
832        let call = ToolCall {
833            id: "c1".into(),
834            name: "search".into(),
835            arguments: serde_json::json!({"q": "ares"}),
836        };
837        let parsed: ToolCall = serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
838        assert_eq!(parsed.name, "search");
839    }
840
841    #[test]
842    fn test_app_error_code_mapping() {
843        assert!(matches!(AppError::Database("x".into()).code(), ErrorCode::DatabaseError));
844        assert!(matches!(AppError::Auth("x".into()).code(), ErrorCode::AuthenticationFailed));
845        assert!(matches!(AppError::NotFound("x".into()).code(), ErrorCode::NotFound));
846        assert!(matches!(AppError::RateLimited("x".into()).code(), ErrorCode::InternalError));
847    }
848
849    #[test]
850    fn test_app_error_from_io() {
851        let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
852        assert!(matches!(err, AppError::Internal(_)));
853        assert!(err.to_string().contains("IO error"));
854    }
855
856    #[test]
857    fn test_app_error_from_serde_json() {
858        let bad = "{not json";
859        let err: AppError = serde_json::from_str::<serde_json::Value>(bad).unwrap_err().into();
860        assert!(matches!(err, AppError::InvalidInput(_)));
861    }
862
863    #[test]
864    fn test_search_filter_application() {
865        let doc = Document {
866            id: "1".into(),
867            content: "body".into(),
868            metadata: DocumentMetadata {
869                title: "Guide".into(),
870                source: "docs/rust".into(),
871                tags: vec!["rust".into(), "rag".into()],
872                ..Default::default()
873            },
874            embedding: None,
875        };
876        let filters = [SearchFilter { field: "tags".into(), value: "rust".into() },
877            SearchFilter { field: "source".into(), value: "docs/rust".into() }];
878        let matches = filters.iter().all(|f| match f.field.as_str() {
879            "tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
880            "source" => doc.metadata.source == f.value,
881            _ => false,
882        });
883        assert!(matches);
884    }
885
886    #[test]
887    fn test_rag_search_request_defaults() {
888        let json = r#"{"collection":"c","query":"q"}"#;
889        let req: RagSearchRequest = serde_json::from_str(json).unwrap();
890        assert_eq!(req.limit, 10);
891        assert!((req.threshold - 0.0).abs() < f32::EPSILON);
892        assert!(!req.rerank);
893    }
894
895    #[test]
896    fn test_chat_response_serde_roundtrip() {
897        let resp = ChatResponse {
898            response: "こんにちは 🌍".into(),
899            agent: "router".into(),
900            context_id: String::new(),
901            sources: Some(vec![]),
902        };
903        let parsed: ChatResponse =
904            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
905        assert_eq!(parsed.response, "こんにちは 🌍");
906        assert_eq!(parsed.context_id, "");
907        assert!(parsed.sources.as_ref().unwrap().is_empty());
908    }
909
910    #[test]
911    fn test_research_request_serde_optional_fields() {
912        let json = r#"{"query":"quantum computing"}"#;
913        let req: ResearchRequest = serde_json::from_str(json).unwrap();
914        assert_eq!(req.query, "quantum computing");
915        assert!(req.depth.is_none());
916        assert!(req.max_iterations.is_none());
917
918        let full = ResearchRequest {
919            query: String::new(),
920            depth: Some(0),
921            max_iterations: Some(u8::MAX),
922        };
923        let parsed: ResearchRequest =
924            serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
925        assert_eq!(parsed.query, "");
926        assert_eq!(parsed.depth, Some(0));
927        assert_eq!(parsed.max_iterations, Some(u8::MAX));
928    }
929
930    #[test]
931    fn test_research_response_serde_empty_sources() {
932        let resp = ResearchResponse {
933            findings: String::new(),
934            sources: vec![],
935            duration_ms: 0,
936        };
937        let parsed: ResearchResponse =
938            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
939        assert!(parsed.findings.is_empty());
940        assert!(parsed.sources.is_empty());
941        assert_eq!(parsed.duration_ms, 0);
942    }
943
944    #[test]
945    fn test_rag_ingest_request_defaults_and_unicode() {
946        let json = r#"{"collection":"docs","content":"café ☕"}"#;
947        let req: RagIngestRequest = serde_json::from_str(json).unwrap();
948        assert_eq!(req.content, "café ☕");
949        assert!(req.title.is_none());
950        assert!(req.source.is_none());
951        assert!(req.tags.is_empty());
952        assert!(req.chunking_strategy.is_none());
953    }
954
955    #[test]
956    fn test_rag_ingest_response_serde_roundtrip() {
957        let resp = RagIngestResponse {
958            chunks_created: 0,
959            document_ids: vec![],
960            collection: "empty".into(),
961        };
962        let parsed: RagIngestResponse =
963            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
964        assert_eq!(parsed.chunks_created, 0);
965        assert!(parsed.document_ids.is_empty());
966    }
967
968    #[test]
969    fn test_rag_search_response_serde_roundtrip() {
970        let resp = RagSearchResponse {
971            results: vec![RagSearchResult {
972                id: "d1".into(),
973                content: "match".into(),
974                score: 1.0,
975                metadata: DocumentMetadata::default(),
976            }],
977            total: 1,
978            strategy: "hybrid".into(),
979            reranked: false,
980            duration_ms: u64::MAX,
981        };
982        let parsed: RagSearchResponse =
983            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
984        assert_eq!(parsed.total, 1);
985        assert_eq!(parsed.duration_ms, u64::MAX);
986    }
987
988    #[test]
989    fn test_rag_delete_collection_serde_roundtrip() {
990        let req = RagDeleteCollectionRequest {
991            collection: "to-delete".into(),
992        };
993        let parsed: RagDeleteCollectionRequest =
994            serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
995        assert_eq!(parsed.collection, "to-delete");
996
997        let resp = RagDeleteCollectionResponse {
998            success: true,
999            collection: "to-delete".into(),
1000            documents_deleted: 0,
1001        };
1002        let parsed_resp: RagDeleteCollectionResponse =
1003            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1004        assert!(parsed_resp.success);
1005        assert_eq!(parsed_resp.documents_deleted, 0);
1006    }
1007
1008    #[test]
1009    fn test_semantic_search_request_defaults() {
1010        let json = r#"{"collection":"c","query":"q"}"#;
1011        let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
1012        assert_eq!(req.limit, 10);
1013        assert!((req.threshold - 0.0).abs() < f32::EPSILON);
1014    }
1015
1016    #[test]
1017    fn test_semantic_search_response_serde_roundtrip() {
1018        let resp = SemanticSearchResponse {
1019            results: vec![],
1020            total: 0,
1021            duration_ms: 0,
1022        };
1023        let parsed: SemanticSearchResponse =
1024            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1025        assert!(parsed.results.is_empty());
1026        assert_eq!(parsed.total, 0);
1027    }
1028
1029    #[test]
1030    fn test_workflow_request_empty_context_default() {
1031        let json = r#"{"query":"run workflow"}"#;
1032        let req: WorkflowRequest = serde_json::from_str(json).unwrap();
1033        assert_eq!(req.query, "run workflow");
1034        assert!(req.context.is_empty());
1035
1036        let with_ctx = WorkflowRequest {
1037            query: "q".into(),
1038            context: [("key".into(), serde_json::json!(null))]
1039                .into_iter()
1040                .collect(),
1041        };
1042        let parsed: WorkflowRequest =
1043            serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
1044        assert!(parsed.context.contains_key("key"));
1045    }
1046
1047    #[test]
1048    fn test_agent_type_serde_builtin_and_custom_unicode() {
1049        for (agent, expected) in [
1050            (AgentType::Router, "\"router\""),
1051            (AgentType::HR, "\"hr\""),
1052        ] {
1053            let json = serde_json::to_string(&agent).unwrap();
1054            assert_eq!(json, expected);
1055            let parsed: AgentType = serde_json::from_str(&json).unwrap();
1056            assert_eq!(parsed, agent);
1057        }
1058        let custom = AgentType::Custom("代理-🤖".into());
1059        let json = serde_json::to_string(&custom).unwrap();
1060        let parsed: AgentType = serde_json::from_str(&json).unwrap();
1061        assert_eq!(parsed.as_str(), "代理-🤖");
1062    }
1063
1064    #[test]
1065    fn test_agent_type_partial_eq_and_clone() {
1066        let a = AgentType::Finance;
1067        let b = a.clone();
1068        assert_eq!(a, b);
1069        assert_ne!(a, AgentType::Sales);
1070        assert!(a.is_builtin());
1071    }
1072
1073    #[test]
1074    fn test_message_role_all_variants_serde() {
1075        for (role, expected) in [
1076            (MessageRole::System, "\"system\""),
1077            (MessageRole::User, "\"user\""),
1078            (MessageRole::Assistant, "\"assistant\""),
1079        ] {
1080            let json = serde_json::to_string(&role).unwrap();
1081            assert_eq!(json, expected);
1082            let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1083            assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
1084        }
1085    }
1086
1087    #[test]
1088    fn test_message_serde_roundtrip_unicode() {
1089        let msg = Message {
1090            role: MessageRole::User,
1091            content: "emoji 🚀 & unicode ñ".into(),
1092            timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1093            parts: vec![],
1094        };
1095        let parsed: Message =
1096            serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1097        assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
1098        assert!(matches!(parsed.role, MessageRole::User));
1099    }
1100
1101    #[test]
1102    fn test_message_parts_serde_default_and_roundtrip() {
1103        let parsed: Message = serde_json::from_str(
1104            r#"{"role":"user","content":"hi","timestamp":"2024-01-01T00:00:00Z"}"#,
1105        )
1106        .unwrap();
1107        assert!(parsed.parts.is_empty());
1108
1109        let msg = Message {
1110            role: MessageRole::User,
1111            content: "hi".into(),
1112            timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1113            parts: vec![ContentPart::Text {
1114                text: "photo".into(),
1115            }],
1116        };
1117        let parsed: Message =
1118            serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1119        assert_eq!(
1120            parsed.parts,
1121            vec![ContentPart::Text {
1122                text: "photo".into(),
1123            }]
1124        );
1125    }
1126
1127    #[test]
1128    fn test_user_memory_empty_collections_serde() {
1129        let mem = UserMemory {
1130            user_id: "u0".into(),
1131            preferences: vec![],
1132            facts: vec![],
1133        };
1134        let parsed: UserMemory =
1135            serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
1136        assert!(parsed.preferences.is_empty());
1137        assert!(parsed.facts.is_empty());
1138    }
1139
1140    #[test]
1141    fn test_preference_and_memory_fact_boundary_confidence() {
1142        let pref = Preference {
1143            category: String::new(),
1144            key: "lang".into(),
1145            value: "rust".into(),
1146            confidence: 0.0,
1147        };
1148        let parsed: Preference =
1149            serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
1150        assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
1151
1152        let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
1153        let fact = MemoryFact {
1154            id: "f1".into(),
1155            user_id: "u1".into(),
1156            category: "work".into(),
1157            fact_key: "role".into(),
1158            fact_value: "engineer".into(),
1159            confidence: 1.0,
1160            created_at: now,
1161            updated_at: now,
1162        };
1163        let parsed_fact: MemoryFact =
1164            serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
1165        assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
1166    }
1167
1168    #[test]
1169    fn test_tool_definition_and_result_serde_roundtrip() {
1170        let def = ToolDefinition {
1171            name: "calc".into(),
1172            description: String::new(),
1173            parameters: serde_json::json!({}),
1174        };
1175        let parsed_def: ToolDefinition =
1176            serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
1177        assert_eq!(parsed_def.name, "calc");
1178        assert!(parsed_def.description.is_empty());
1179
1180        let result = ToolResult {
1181            tool_call_id: "c1".into(),
1182            result: serde_json::Value::Null,
1183        };
1184        let parsed_result: ToolResult =
1185            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1186        assert!(parsed_result.result.is_null());
1187    }
1188
1189    #[test]
1190    fn test_document_serde_none_embedding_and_metadata_default() {
1191        let doc = Document {
1192            id: "doc-1".into(),
1193            content: String::new(),
1194            metadata: DocumentMetadata::default(),
1195            embedding: None,
1196        };
1197        let parsed: Document =
1198            serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1199        assert!(parsed.content.is_empty());
1200        assert!(parsed.embedding.is_none());
1201        assert!(parsed.metadata.title.is_empty());
1202
1203        let default_meta = DocumentMetadata::default();
1204        assert!(default_meta.tags.is_empty());
1205        assert!(default_meta.source.is_empty());
1206    }
1207
1208    #[test]
1209    fn test_search_query_and_result_clone_debug() {
1210        let query = SearchQuery {
1211            query: "find".into(),
1212            limit: 0,
1213            threshold: 1.0,
1214            filters: None,
1215        };
1216        let cloned = query.clone();
1217        assert_eq!(cloned.limit, 0);
1218        assert!(cloned.filters.is_none());
1219        assert!(format!("{:?}", cloned).contains("find"));
1220
1221        let result = SearchResult {
1222            document: Document {
1223                id: "1".into(),
1224                content: "x".into(),
1225                metadata: DocumentMetadata::default(),
1226                embedding: Some(vec![]),
1227            },
1228            score: 0.0,
1229        };
1230        let cloned_result = result.clone();
1231        assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
1232        assert!(cloned_result.document.embedding.as_ref().unwrap().is_empty());
1233    }
1234
1235    #[test]
1236    fn test_agent_context_clone_debug() {
1237        let ctx = AgentContext {
1238            user_id: "u1".into(),
1239            session_id: "s1".into(),
1240            conversation_history: vec![],
1241            user_memory: None,
1242        };
1243        let cloned = ctx.clone();
1244        assert_eq!(cloned.user_id, "u1");
1245        assert!(cloned.user_memory.is_none());
1246        assert!(format!("{:?}", cloned).contains("AgentContext"));
1247    }
1248
1249    #[test]
1250    fn test_login_register_token_claims_serde_roundtrip() {
1251        let login = LoginRequest {
1252            email: "user@example.com".into(),
1253            password: String::new(),
1254        };
1255        let parsed_login: LoginRequest =
1256            serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
1257        assert!(parsed_login.password.is_empty());
1258
1259        let register = RegisterRequest {
1260            email: "new@example.com".into(),
1261            password: "secret".into(),
1262            name: "新規ユーザー".into(),
1263        };
1264        let parsed_register: RegisterRequest =
1265            serde_json::from_str(&serde_json::to_string(&register).unwrap()).unwrap();
1266        assert_eq!(parsed_register.name, "新規ユーザー");
1267
1268        let token = TokenResponse {
1269            access_token: "access".into(),
1270            refresh_token: "refresh".into(),
1271            expires_in: 0,
1272        };
1273        let parsed_token: TokenResponse =
1274            serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
1275        assert_eq!(parsed_token.expires_in, 0);
1276
1277        let claims = Claims {
1278            sub: "user-1".into(),
1279            email: "user@example.com".into(),
1280            exp: usize::MAX,
1281            iat: 0,
1282            jti: String::new(),
1283            tenant_id: None,
1284        };
1285        let json = serde_json::to_string(&claims).unwrap();
1286        assert!(!json.contains("jti"));
1287        let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
1288        assert_eq!(parsed_claims.jti, "");
1289    }
1290
1291    #[test]
1292    fn test_error_code_serialize_all_variants() {
1293        let codes = [
1294            (ErrorCode::DatabaseError, "DATABASE_ERROR"),
1295            (ErrorCode::LlmError, "LLM_ERROR"),
1296            (ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
1297            (ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
1298            (ErrorCode::NotFound, "NOT_FOUND"),
1299            (ErrorCode::InvalidInput, "INVALID_INPUT"),
1300            (ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
1301            (ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
1302            (ErrorCode::InternalError, "INTERNAL_ERROR"),
1303        ];
1304        for (code, expected) in codes {
1305            let json = serde_json::to_string(&code).unwrap();
1306            assert_eq!(json, format!("\"{}\"", expected));
1307        }
1308    }
1309
1310    #[test]
1311    fn test_app_error_remaining_code_mappings() {
1312        assert!(matches!(
1313            AppError::LLM("x".into()).code(),
1314            ErrorCode::LlmError
1315        ));
1316        assert!(matches!(
1317            AppError::Configuration("x".into()).code(),
1318            ErrorCode::ConfigurationError
1319        ));
1320        assert!(matches!(
1321            AppError::External("x".into()).code(),
1322            ErrorCode::ExternalServiceError
1323        ));
1324        assert!(matches!(
1325            AppError::Unavailable("x".into()).code(),
1326            ErrorCode::InternalError
1327        ));
1328        assert!(matches!(
1329            AppError::FeatureDisabled("x".into()).code(),
1330            ErrorCode::InternalError
1331        ));
1332        assert!(matches!(
1333            AppError::Internal("x".into()).code(),
1334            ErrorCode::InternalError
1335        ));
1336    }
1337
1338    #[test]
1339    fn test_source_clone_and_boundary_scores() {
1340        let source = Source {
1341            title: "t".into(),
1342            url: None,
1343            relevance_score: 0.0,
1344        };
1345        let cloned = source.clone();
1346        assert!(cloned.url.is_none());
1347        assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
1348
1349        let max = Source {
1350            title: "max".into(),
1351            url: Some("https://example.com?q=100%".into()),
1352            relevance_score: 1.0,
1353        };
1354        let parsed: Source =
1355            serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
1356        assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
1357    }
1358
1359    #[test]
1360    fn test_chat_request_workspace_id_serde_roundtrip() {
1361        let req = ChatRequest {
1362            message: "ping".into(),
1363            agent_type: None,
1364            context_id: None,
1365            workspace_id: Some("ws-éruka-42".into()),
1366            model: None,
1367            parts: None,
1368            previous_response_id: None,
1369            web_search: None,
1370        };
1371        let json = serde_json::to_string(&req).unwrap();
1372        assert!(json.contains("workspace_id"));
1373        let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
1374        assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
1375    }
1376
1377    #[test]
1378    fn test_rag_search_result_serde_roundtrip() {
1379        let result = RagSearchResult {
1380            id: "chunk-1".into(),
1381            content: "snippet".into(),
1382            score: 0.75,
1383            metadata: DocumentMetadata {
1384                title: "Guide".into(),
1385                source: "docs/guide.md".into(),
1386                tags: vec!["rag".into()],
1387                ..Default::default()
1388            },
1389        };
1390        let parsed: RagSearchResult =
1391            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1392        assert_eq!(parsed.id, "chunk-1");
1393        assert!((parsed.score - 0.75).abs() < f32::EPSILON);
1394        assert_eq!(parsed.metadata.tags, vec!["rag"]);
1395    }
1396
1397    #[test]
1398    fn test_semantic_search_result_serde_roundtrip() {
1399        let result = SemanticSearchResult {
1400            id: "doc-9".into(),
1401            content: "semantic hit".into(),
1402            similarity: 0.91,
1403            metadata: DocumentMetadata::default(),
1404        };
1405        let parsed: SemanticSearchResult =
1406            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1407        assert_eq!(parsed.content, "semantic hit");
1408        assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
1409    }
1410
1411    #[test]
1412    fn test_app_error_into_response_status_codes() {
1413        let cases = [
1414            (AppError::Auth("denied".into()), 401u16),
1415            (AppError::NotFound("gone".into()), 404),
1416            (AppError::InvalidInput("bad".into()), 400),
1417            (AppError::External("upstream".into()), 502),
1418            (AppError::Unavailable("maintenance".into()), 503),
1419            (AppError::RateLimited("slow".into()), 429),
1420            (AppError::FeatureDisabled("off".into()), 400),
1421            (AppError::Database("db".into()), 500),
1422        ];
1423        for (err, expected) in cases {
1424            assert_eq!(err.status_code(), expected);
1425        }
1426    }
1427
1428    #[test]
1429    fn test_agent_type_from_string_is_case_insensitive() {
1430        assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
1431        assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
1432        assert_eq!(
1433            AgentType::from_string("MyCustom"),
1434            AgentType::Custom("MyCustom".into())
1435        );
1436    }
1437}