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,
695            AppError::RateLimited(_) => ErrorCode::InternalError,
696            AppError::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 =
818            serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
819        assert_eq!(parsed.title, "Doc");
820        assert_eq!(parsed.relevance_score, 0.9);
821    }
822
823    #[test]
824    fn test_document_metadata_default_datetime() {
825        let json = r#"{"title":"t","source":"s"}"#;
826        let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
827        assert_eq!(meta.title, "t");
828        assert!(meta.created_at <= Utc::now());
829    }
830
831    #[test]
832    fn test_tool_call_serde_roundtrip() {
833        let call = ToolCall {
834            id: "c1".into(),
835            name: "search".into(),
836            arguments: serde_json::json!({"q": "ares"}),
837        };
838        let parsed: ToolCall =
839            serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
840        assert_eq!(parsed.name, "search");
841    }
842
843    #[test]
844    fn test_app_error_code_mapping() {
845        assert!(matches!(
846            AppError::Database("x".into()).code(),
847            ErrorCode::DatabaseError
848        ));
849        assert!(matches!(
850            AppError::Auth("x".into()).code(),
851            ErrorCode::AuthenticationFailed
852        ));
853        assert!(matches!(
854            AppError::NotFound("x".into()).code(),
855            ErrorCode::NotFound
856        ));
857        assert!(matches!(
858            AppError::RateLimited("x".into()).code(),
859            ErrorCode::InternalError
860        ));
861    }
862
863    #[test]
864    fn test_app_error_from_io() {
865        let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
866        assert!(matches!(err, AppError::Internal(_)));
867        assert!(err.to_string().contains("IO error"));
868    }
869
870    #[test]
871    fn test_app_error_from_serde_json() {
872        let bad = "{not json";
873        let err: AppError = serde_json::from_str::<serde_json::Value>(bad)
874            .unwrap_err()
875            .into();
876        assert!(matches!(err, AppError::InvalidInput(_)));
877    }
878
879    #[test]
880    fn test_search_filter_application() {
881        let doc = Document {
882            id: "1".into(),
883            content: "body".into(),
884            metadata: DocumentMetadata {
885                title: "Guide".into(),
886                source: "docs/rust".into(),
887                tags: vec!["rust".into(), "rag".into()],
888                ..Default::default()
889            },
890            embedding: None,
891        };
892        let filters = [
893            SearchFilter {
894                field: "tags".into(),
895                value: "rust".into(),
896            },
897            SearchFilter {
898                field: "source".into(),
899                value: "docs/rust".into(),
900            },
901        ];
902        let matches = filters.iter().all(|f| match f.field.as_str() {
903            "tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
904            "source" => doc.metadata.source == f.value,
905            _ => false,
906        });
907        assert!(matches);
908    }
909
910    #[test]
911    fn test_rag_search_request_defaults() {
912        let json = r#"{"collection":"c","query":"q"}"#;
913        let req: RagSearchRequest = serde_json::from_str(json).unwrap();
914        assert_eq!(req.limit, 10);
915        assert!((req.threshold - 0.0).abs() < f32::EPSILON);
916        assert!(!req.rerank);
917    }
918
919    #[test]
920    fn test_chat_response_serde_roundtrip() {
921        let resp = ChatResponse {
922            response: "こんにちは 🌍".into(),
923            agent: "router".into(),
924            context_id: String::new(),
925            sources: Some(vec![]),
926        };
927        let parsed: ChatResponse =
928            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
929        assert_eq!(parsed.response, "こんにちは 🌍");
930        assert_eq!(parsed.context_id, "");
931        assert!(parsed.sources.as_ref().unwrap().is_empty());
932    }
933
934    #[test]
935    fn test_research_request_serde_optional_fields() {
936        let json = r#"{"query":"quantum computing"}"#;
937        let req: ResearchRequest = serde_json::from_str(json).unwrap();
938        assert_eq!(req.query, "quantum computing");
939        assert!(req.depth.is_none());
940        assert!(req.max_iterations.is_none());
941
942        let full = ResearchRequest {
943            query: String::new(),
944            depth: Some(0),
945            max_iterations: Some(u8::MAX),
946        };
947        let parsed: ResearchRequest =
948            serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
949        assert_eq!(parsed.query, "");
950        assert_eq!(parsed.depth, Some(0));
951        assert_eq!(parsed.max_iterations, Some(u8::MAX));
952    }
953
954    #[test]
955    fn test_research_response_serde_empty_sources() {
956        let resp = ResearchResponse {
957            findings: String::new(),
958            sources: vec![],
959            duration_ms: 0,
960        };
961        let parsed: ResearchResponse =
962            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
963        assert!(parsed.findings.is_empty());
964        assert!(parsed.sources.is_empty());
965        assert_eq!(parsed.duration_ms, 0);
966    }
967
968    #[test]
969    fn test_rag_ingest_request_defaults_and_unicode() {
970        let json = r#"{"collection":"docs","content":"café ☕"}"#;
971        let req: RagIngestRequest = serde_json::from_str(json).unwrap();
972        assert_eq!(req.content, "café ☕");
973        assert!(req.title.is_none());
974        assert!(req.source.is_none());
975        assert!(req.tags.is_empty());
976        assert!(req.chunking_strategy.is_none());
977    }
978
979    #[test]
980    fn test_rag_ingest_response_serde_roundtrip() {
981        let resp = RagIngestResponse {
982            chunks_created: 0,
983            document_ids: vec![],
984            collection: "empty".into(),
985        };
986        let parsed: RagIngestResponse =
987            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
988        assert_eq!(parsed.chunks_created, 0);
989        assert!(parsed.document_ids.is_empty());
990    }
991
992    #[test]
993    fn test_rag_search_response_serde_roundtrip() {
994        let resp = RagSearchResponse {
995            results: vec![RagSearchResult {
996                id: "d1".into(),
997                content: "match".into(),
998                score: 1.0,
999                metadata: DocumentMetadata::default(),
1000            }],
1001            total: 1,
1002            strategy: "hybrid".into(),
1003            reranked: false,
1004            duration_ms: u64::MAX,
1005        };
1006        let parsed: RagSearchResponse =
1007            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1008        assert_eq!(parsed.total, 1);
1009        assert_eq!(parsed.duration_ms, u64::MAX);
1010    }
1011
1012    #[test]
1013    fn test_rag_delete_collection_serde_roundtrip() {
1014        let req = RagDeleteCollectionRequest {
1015            collection: "to-delete".into(),
1016        };
1017        let parsed: RagDeleteCollectionRequest =
1018            serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
1019        assert_eq!(parsed.collection, "to-delete");
1020
1021        let resp = RagDeleteCollectionResponse {
1022            success: true,
1023            collection: "to-delete".into(),
1024            documents_deleted: 0,
1025        };
1026        let parsed_resp: RagDeleteCollectionResponse =
1027            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1028        assert!(parsed_resp.success);
1029        assert_eq!(parsed_resp.documents_deleted, 0);
1030    }
1031
1032    #[test]
1033    fn test_semantic_search_request_defaults() {
1034        let json = r#"{"collection":"c","query":"q"}"#;
1035        let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
1036        assert_eq!(req.limit, 10);
1037        assert!((req.threshold - 0.0).abs() < f32::EPSILON);
1038    }
1039
1040    #[test]
1041    fn test_semantic_search_response_serde_roundtrip() {
1042        let resp = SemanticSearchResponse {
1043            results: vec![],
1044            total: 0,
1045            duration_ms: 0,
1046        };
1047        let parsed: SemanticSearchResponse =
1048            serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
1049        assert!(parsed.results.is_empty());
1050        assert_eq!(parsed.total, 0);
1051    }
1052
1053    #[test]
1054    fn test_workflow_request_empty_context_default() {
1055        let json = r#"{"query":"run workflow"}"#;
1056        let req: WorkflowRequest = serde_json::from_str(json).unwrap();
1057        assert_eq!(req.query, "run workflow");
1058        assert!(req.context.is_empty());
1059
1060        let with_ctx = WorkflowRequest {
1061            query: "q".into(),
1062            context: [("key".into(), serde_json::json!(null))]
1063                .into_iter()
1064                .collect(),
1065        };
1066        let parsed: WorkflowRequest =
1067            serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
1068        assert!(parsed.context.contains_key("key"));
1069    }
1070
1071    #[test]
1072    fn test_agent_type_serde_builtin_and_custom_unicode() {
1073        for (agent, expected) in [(AgentType::Router, "\"router\""), (AgentType::HR, "\"hr\"")] {
1074            let json = serde_json::to_string(&agent).unwrap();
1075            assert_eq!(json, expected);
1076            let parsed: AgentType = serde_json::from_str(&json).unwrap();
1077            assert_eq!(parsed, agent);
1078        }
1079        let custom = AgentType::Custom("代理-🤖".into());
1080        let json = serde_json::to_string(&custom).unwrap();
1081        let parsed: AgentType = serde_json::from_str(&json).unwrap();
1082        assert_eq!(parsed.as_str(), "代理-🤖");
1083    }
1084
1085    #[test]
1086    fn test_agent_type_partial_eq_and_clone() {
1087        let a = AgentType::Finance;
1088        let b = a.clone();
1089        assert_eq!(a, b);
1090        assert_ne!(a, AgentType::Sales);
1091        assert!(a.is_builtin());
1092    }
1093
1094    #[test]
1095    fn test_message_role_all_variants_serde() {
1096        for (role, expected) in [
1097            (MessageRole::System, "\"system\""),
1098            (MessageRole::User, "\"user\""),
1099            (MessageRole::Assistant, "\"assistant\""),
1100        ] {
1101            let json = serde_json::to_string(&role).unwrap();
1102            assert_eq!(json, expected);
1103            let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1104            assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
1105        }
1106    }
1107
1108    #[test]
1109    fn test_message_serde_roundtrip_unicode() {
1110        let msg = Message {
1111            role: MessageRole::User,
1112            content: "emoji 🚀 & unicode ñ".into(),
1113            timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1114            parts: vec![],
1115        };
1116        let parsed: Message = serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1117        assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
1118        assert!(matches!(parsed.role, MessageRole::User));
1119    }
1120
1121    #[test]
1122    fn test_message_parts_serde_default_and_roundtrip() {
1123        let parsed: Message = serde_json::from_str(
1124            r#"{"role":"user","content":"hi","timestamp":"2024-01-01T00:00:00Z"}"#,
1125        )
1126        .unwrap();
1127        assert!(parsed.parts.is_empty());
1128
1129        let msg = Message {
1130            role: MessageRole::User,
1131            content: "hi".into(),
1132            timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1133            parts: vec![ContentPart::Text {
1134                text: "photo".into(),
1135            }],
1136        };
1137        let parsed: Message = serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1138        assert_eq!(
1139            parsed.parts,
1140            vec![ContentPart::Text {
1141                text: "photo".into(),
1142            }]
1143        );
1144    }
1145
1146    #[test]
1147    fn test_user_memory_empty_collections_serde() {
1148        let mem = UserMemory {
1149            user_id: "u0".into(),
1150            preferences: vec![],
1151            facts: vec![],
1152        };
1153        let parsed: UserMemory =
1154            serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
1155        assert!(parsed.preferences.is_empty());
1156        assert!(parsed.facts.is_empty());
1157    }
1158
1159    #[test]
1160    fn test_preference_and_memory_fact_boundary_confidence() {
1161        let pref = Preference {
1162            category: String::new(),
1163            key: "lang".into(),
1164            value: "rust".into(),
1165            confidence: 0.0,
1166        };
1167        let parsed: Preference =
1168            serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
1169        assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
1170
1171        let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
1172        let fact = MemoryFact {
1173            id: "f1".into(),
1174            user_id: "u1".into(),
1175            category: "work".into(),
1176            fact_key: "role".into(),
1177            fact_value: "engineer".into(),
1178            confidence: 1.0,
1179            created_at: now,
1180            updated_at: now,
1181        };
1182        let parsed_fact: MemoryFact =
1183            serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
1184        assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
1185    }
1186
1187    #[test]
1188    fn test_tool_definition_and_result_serde_roundtrip() {
1189        let def = ToolDefinition {
1190            name: "calc".into(),
1191            description: String::new(),
1192            parameters: serde_json::json!({}),
1193        };
1194        let parsed_def: ToolDefinition =
1195            serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
1196        assert_eq!(parsed_def.name, "calc");
1197        assert!(parsed_def.description.is_empty());
1198
1199        let result = ToolResult {
1200            tool_call_id: "c1".into(),
1201            result: serde_json::Value::Null,
1202        };
1203        let parsed_result: ToolResult =
1204            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1205        assert!(parsed_result.result.is_null());
1206    }
1207
1208    #[test]
1209    fn test_document_serde_none_embedding_and_metadata_default() {
1210        let doc = Document {
1211            id: "doc-1".into(),
1212            content: String::new(),
1213            metadata: DocumentMetadata::default(),
1214            embedding: None,
1215        };
1216        let parsed: Document = serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1217        assert!(parsed.content.is_empty());
1218        assert!(parsed.embedding.is_none());
1219        assert!(parsed.metadata.title.is_empty());
1220
1221        let default_meta = DocumentMetadata::default();
1222        assert!(default_meta.tags.is_empty());
1223        assert!(default_meta.source.is_empty());
1224    }
1225
1226    #[test]
1227    fn test_search_query_and_result_clone_debug() {
1228        let query = SearchQuery {
1229            query: "find".into(),
1230            limit: 0,
1231            threshold: 1.0,
1232            filters: None,
1233        };
1234        let cloned = query.clone();
1235        assert_eq!(cloned.limit, 0);
1236        assert!(cloned.filters.is_none());
1237        assert!(format!("{:?}", cloned).contains("find"));
1238
1239        let result = SearchResult {
1240            document: Document {
1241                id: "1".into(),
1242                content: "x".into(),
1243                metadata: DocumentMetadata::default(),
1244                embedding: Some(vec![]),
1245            },
1246            score: 0.0,
1247        };
1248        let cloned_result = result.clone();
1249        assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
1250        assert!(cloned_result
1251            .document
1252            .embedding
1253            .as_ref()
1254            .unwrap()
1255            .is_empty());
1256    }
1257
1258    #[test]
1259    fn test_agent_context_clone_debug() {
1260        let ctx = AgentContext {
1261            user_id: "u1".into(),
1262            session_id: "s1".into(),
1263            conversation_history: vec![],
1264            user_memory: None,
1265        };
1266        let cloned = ctx.clone();
1267        assert_eq!(cloned.user_id, "u1");
1268        assert!(cloned.user_memory.is_none());
1269        assert!(format!("{:?}", cloned).contains("AgentContext"));
1270    }
1271
1272    #[test]
1273    fn test_login_register_token_claims_serde_roundtrip() {
1274        let login = LoginRequest {
1275            email: "user@example.com".into(),
1276            password: String::new(),
1277        };
1278        let parsed_login: LoginRequest =
1279            serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
1280        assert!(parsed_login.password.is_empty());
1281
1282        let register = RegisterRequest {
1283            email: "new@example.com".into(),
1284            password: "secret".into(),
1285            name: "新規ユーザー".into(),
1286        };
1287        let parsed_register: RegisterRequest =
1288            serde_json::from_str(&serde_json::to_string(&register).unwrap()).unwrap();
1289        assert_eq!(parsed_register.name, "新規ユーザー");
1290
1291        let token = TokenResponse {
1292            access_token: "access".into(),
1293            refresh_token: "refresh".into(),
1294            expires_in: 0,
1295        };
1296        let parsed_token: TokenResponse =
1297            serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
1298        assert_eq!(parsed_token.expires_in, 0);
1299
1300        let claims = Claims {
1301            sub: "user-1".into(),
1302            email: "user@example.com".into(),
1303            exp: usize::MAX,
1304            iat: 0,
1305            jti: String::new(),
1306            tenant_id: None,
1307        };
1308        let json = serde_json::to_string(&claims).unwrap();
1309        assert!(!json.contains("jti"));
1310        let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
1311        assert_eq!(parsed_claims.jti, "");
1312    }
1313
1314    #[test]
1315    fn test_error_code_serialize_all_variants() {
1316        let codes = [
1317            (ErrorCode::DatabaseError, "DATABASE_ERROR"),
1318            (ErrorCode::LlmError, "LLM_ERROR"),
1319            (ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
1320            (ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
1321            (ErrorCode::NotFound, "NOT_FOUND"),
1322            (ErrorCode::InvalidInput, "INVALID_INPUT"),
1323            (ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
1324            (ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
1325            (ErrorCode::InternalError, "INTERNAL_ERROR"),
1326        ];
1327        for (code, expected) in codes {
1328            let json = serde_json::to_string(&code).unwrap();
1329            assert_eq!(json, format!("\"{}\"", expected));
1330        }
1331    }
1332
1333    #[test]
1334    fn test_app_error_remaining_code_mappings() {
1335        assert!(matches!(
1336            AppError::LLM("x".into()).code(),
1337            ErrorCode::LlmError
1338        ));
1339        assert!(matches!(
1340            AppError::Configuration("x".into()).code(),
1341            ErrorCode::ConfigurationError
1342        ));
1343        assert!(matches!(
1344            AppError::External("x".into()).code(),
1345            ErrorCode::ExternalServiceError
1346        ));
1347        assert!(matches!(
1348            AppError::Unavailable("x".into()).code(),
1349            ErrorCode::InternalError
1350        ));
1351        assert!(matches!(
1352            AppError::FeatureDisabled("x".into()).code(),
1353            ErrorCode::InternalError
1354        ));
1355        assert!(matches!(
1356            AppError::Internal("x".into()).code(),
1357            ErrorCode::InternalError
1358        ));
1359    }
1360
1361    #[test]
1362    fn test_source_clone_and_boundary_scores() {
1363        let source = Source {
1364            title: "t".into(),
1365            url: None,
1366            relevance_score: 0.0,
1367        };
1368        let cloned = source.clone();
1369        assert!(cloned.url.is_none());
1370        assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
1371
1372        let max = Source {
1373            title: "max".into(),
1374            url: Some("https://example.com?q=100%".into()),
1375            relevance_score: 1.0,
1376        };
1377        let parsed: Source = serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
1378        assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
1379    }
1380
1381    #[test]
1382    fn test_chat_request_workspace_id_serde_roundtrip() {
1383        let req = ChatRequest {
1384            message: "ping".into(),
1385            agent_type: None,
1386            context_id: None,
1387            workspace_id: Some("ws-éruka-42".into()),
1388            model: None,
1389            parts: None,
1390            previous_response_id: None,
1391            web_search: None,
1392        };
1393        let json = serde_json::to_string(&req).unwrap();
1394        assert!(json.contains("workspace_id"));
1395        let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
1396        assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
1397    }
1398
1399    #[test]
1400    fn test_rag_search_result_serde_roundtrip() {
1401        let result = RagSearchResult {
1402            id: "chunk-1".into(),
1403            content: "snippet".into(),
1404            score: 0.75,
1405            metadata: DocumentMetadata {
1406                title: "Guide".into(),
1407                source: "docs/guide.md".into(),
1408                tags: vec!["rag".into()],
1409                ..Default::default()
1410            },
1411        };
1412        let parsed: RagSearchResult =
1413            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1414        assert_eq!(parsed.id, "chunk-1");
1415        assert!((parsed.score - 0.75).abs() < f32::EPSILON);
1416        assert_eq!(parsed.metadata.tags, vec!["rag"]);
1417    }
1418
1419    #[test]
1420    fn test_semantic_search_result_serde_roundtrip() {
1421        let result = SemanticSearchResult {
1422            id: "doc-9".into(),
1423            content: "semantic hit".into(),
1424            similarity: 0.91,
1425            metadata: DocumentMetadata::default(),
1426        };
1427        let parsed: SemanticSearchResult =
1428            serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1429        assert_eq!(parsed.content, "semantic hit");
1430        assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
1431    }
1432
1433    #[test]
1434    fn test_app_error_into_response_status_codes() {
1435        let cases = [
1436            (AppError::Auth("denied".into()), 401u16),
1437            (AppError::NotFound("gone".into()), 404),
1438            (AppError::InvalidInput("bad".into()), 400),
1439            (AppError::External("upstream".into()), 502),
1440            (AppError::Unavailable("maintenance".into()), 503),
1441            (AppError::RateLimited("slow".into()), 429),
1442            (AppError::FeatureDisabled("off".into()), 400),
1443            (AppError::Database("db".into()), 500),
1444        ];
1445        for (err, expected) in cases {
1446            assert_eq!(err.status_code(), expected);
1447        }
1448    }
1449
1450    #[test]
1451    fn test_agent_type_from_string_is_case_insensitive() {
1452        assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
1453        assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
1454        assert_eq!(
1455            AgentType::from_string("MyCustom"),
1456            AgentType::Custom("MyCustom".into())
1457        );
1458    }
1459}