1use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14
15fn default_datetime() -> DateTime<Utc> {
17 Utc::now()
18}
19
20#[derive(Debug, Serialize, Deserialize)]
24pub struct ChatRequest {
25 pub message: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub agent_type: Option<AgentType>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub context_id: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
36 pub workspace_id: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub model: Option<String>,
40}
41
42#[derive(Debug, Serialize, Deserialize)]
44pub struct ChatResponse {
45 pub response: String,
47 pub agent: String,
49 pub context_id: String,
51 pub sources: Option<Vec<Source>>,
53}
54
55#[derive(Debug, Serialize, Deserialize, Clone)]
57pub struct Source {
58 pub title: String,
60 pub url: Option<String>,
62 pub relevance_score: f32,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
68pub struct ResearchRequest {
69 pub query: String,
71 pub depth: Option<u8>,
73 pub max_iterations: Option<u8>,
75}
76
77#[derive(Debug, Serialize, Deserialize)]
79pub struct ResearchResponse {
80 pub findings: String,
82 pub sources: Vec<Source>,
84 pub duration_ms: u64,
86}
87
88#[derive(Debug, Serialize, Deserialize)]
92pub struct RagIngestRequest {
93 pub collection: String,
95 pub content: String,
97 pub title: Option<String>,
99 pub source: Option<String>,
101 #[serde(default)]
103 pub tags: Vec<String>,
104 #[serde(default)]
106 pub chunking_strategy: Option<String>,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
111pub struct RagIngestResponse {
112 pub chunks_created: usize,
114 pub document_ids: Vec<String>,
116 pub collection: String,
118}
119
120#[derive(Debug, Serialize, Deserialize)]
122pub struct RagSearchRequest {
123 pub collection: String,
125 pub query: String,
127 #[serde(default = "default_search_limit")]
129 pub limit: usize,
130 #[serde(default)]
132 pub strategy: Option<String>,
133 #[serde(default = "default_search_threshold")]
135 pub threshold: f32,
136 #[serde(default)]
138 pub rerank: bool,
139 #[serde(default)]
141 pub reranker_model: Option<String>,
142}
143
144fn default_search_limit() -> usize {
145 10
146}
147
148fn default_search_threshold() -> f32 {
149 0.0
150}
151
152#[derive(Debug, Serialize, Deserialize)]
154pub struct RagSearchResult {
155 pub id: String,
157 pub content: String,
159 pub score: f32,
161 pub metadata: DocumentMetadata,
163}
164
165#[derive(Debug, Serialize, Deserialize)]
167pub struct RagSearchResponse {
168 pub results: Vec<RagSearchResult>,
170 pub total: usize,
172 pub strategy: String,
174 pub reranked: bool,
176 pub duration_ms: u64,
178}
179
180#[derive(Debug, Serialize, Deserialize)]
182pub struct RagDeleteCollectionRequest {
183 pub collection: String,
185}
186
187#[derive(Debug, Serialize, Deserialize)]
189pub struct RagDeleteCollectionResponse {
190 pub success: bool,
192 pub collection: String,
194 pub documents_deleted: usize,
196}
197
198#[derive(Debug, Serialize, Deserialize)]
205pub struct SemanticSearchRequest {
206 pub collection: String,
208 pub query: String,
210 #[serde(default = "default_search_limit")]
212 pub limit: usize,
213 #[serde(default = "default_search_threshold")]
215 pub threshold: f32,
216}
217
218#[derive(Debug, Serialize, Deserialize)]
220pub struct SemanticSearchResult {
221 pub id: String,
223 pub content: String,
225 pub similarity: f32,
227 pub metadata: DocumentMetadata,
229}
230
231#[derive(Debug, Serialize, Deserialize)]
233pub struct SemanticSearchResponse {
234 pub results: Vec<SemanticSearchResult>,
236 pub total: usize,
238 pub duration_ms: u64,
240}
241
242#[derive(Debug, Serialize, Deserialize)]
244pub struct WorkflowRequest {
245 pub query: String,
247 #[serde(default)]
249 pub context: std::collections::HashMap<String, serde_json::Value>,
250}
251
252#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
259#[serde(rename_all = "lowercase")]
260#[non_exhaustive]
261pub enum AgentType {
262 Router,
264 Orchestrator,
266 Product,
268 Invoice,
270 Sales,
272 Finance,
274 #[serde(rename = "hr")]
276 HR,
277 #[serde(untagged)]
280 Custom(String),
281}
282
283impl AgentType {
284 pub fn as_str(&self) -> &str {
286 match self {
287 AgentType::Router => "router",
288 AgentType::Orchestrator => "orchestrator",
289 AgentType::Product => "product",
290 AgentType::Invoice => "invoice",
291 AgentType::Sales => "sales",
292 AgentType::Finance => "finance",
293 AgentType::HR => "hr",
294 AgentType::Custom(name) => name,
295 }
296 }
297
298 pub fn from_string(s: &str) -> Self {
300 match s.to_lowercase().as_str() {
301 "router" => AgentType::Router,
302 "orchestrator" => AgentType::Orchestrator,
303 "product" => AgentType::Product,
304 "invoice" => AgentType::Invoice,
305 "sales" => AgentType::Sales,
306 "finance" => AgentType::Finance,
307 "hr" => AgentType::HR,
308 _ => AgentType::Custom(s.to_string()),
309 }
310 }
311
312 pub fn is_builtin(&self) -> bool {
314 !matches!(self, AgentType::Custom(_))
315 }
316}
317
318impl std::fmt::Display for AgentType {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 write!(f, "{}", self.as_str())
321 }
322}
323
324#[derive(Debug, Clone)]
326pub struct AgentContext {
327 pub user_id: String,
329 pub session_id: String,
331 pub conversation_history: Vec<Message>,
333 pub user_memory: Option<UserMemory>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct Message {
340 pub role: MessageRole,
342 pub content: String,
344 pub timestamp: DateTime<Utc>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(rename_all = "lowercase")]
351pub enum MessageRole {
352 System,
354 User,
356 Assistant,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct UserMemory {
365 pub user_id: String,
367 pub preferences: Vec<Preference>,
369 pub facts: Vec<MemoryFact>,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct Preference {
376 pub category: String,
378 pub key: String,
380 pub value: String,
382 pub confidence: f32,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct MemoryFact {
389 pub id: String,
391 pub user_id: String,
393 pub category: String,
395 pub fact_key: String,
397 pub fact_value: String,
399 pub confidence: f32,
401 pub created_at: DateTime<Utc>,
403 pub updated_at: DateTime<Utc>,
405}
406
407#[derive(Debug, Serialize, Deserialize, Clone)]
411pub struct ToolDefinition {
412 pub name: String,
414 pub description: String,
416 pub parameters: serde_json::Value,
418}
419
420#[derive(Debug, Serialize, Deserialize, Clone)]
422pub struct ToolCall {
423 pub id: String,
425 pub name: String,
427 pub arguments: serde_json::Value,
429}
430
431#[derive(Debug, Serialize, Deserialize)]
433pub struct ToolResult {
434 pub tool_call_id: String,
436 pub result: serde_json::Value,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct Document {
445 pub id: String,
447 pub content: String,
449 pub metadata: DocumentMetadata,
451 pub embedding: Option<Vec<f32>>,
453}
454
455#[derive(Debug, Clone, Default, Serialize, Deserialize)]
457pub struct DocumentMetadata {
458 #[serde(default)]
460 pub title: String,
461 #[serde(default)]
463 pub source: String,
464 #[serde(default = "default_datetime")]
466 pub created_at: DateTime<Utc>,
467 #[serde(default)]
469 pub tags: Vec<String>,
470}
471
472#[derive(Debug, Clone)]
474pub struct SearchQuery {
475 pub query: String,
477 pub limit: usize,
479 pub threshold: f32,
481 pub filters: Option<Vec<SearchFilter>>,
483}
484
485#[derive(Debug, Clone)]
487pub struct SearchFilter {
488 pub field: String,
490 pub value: String,
492}
493
494#[derive(Debug, Clone)]
496pub struct SearchResult {
497 pub document: Document,
499 pub score: f32,
501}
502
503#[derive(Debug, Serialize, Deserialize)]
507pub struct LoginRequest {
508 pub email: String,
510 pub password: String,
512}
513
514#[derive(Debug, Serialize, Deserialize)]
516pub struct RegisterRequest {
517 pub email: String,
519 pub password: String,
521 pub name: String,
523}
524
525#[derive(Debug, Serialize, Deserialize)]
527pub struct TokenResponse {
528 pub access_token: String,
530 pub refresh_token: String,
532 pub expires_in: i64,
534}
535
536#[derive(Debug, Serialize, Deserialize, Clone)]
538pub struct Claims {
539 pub sub: String,
541 pub email: String,
543 pub exp: usize,
545 pub iat: usize,
547 #[serde(default, skip_serializing_if = "String::is_empty")]
549 pub jti: String,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub tenant_id: Option<String>,
553}
554
555#[derive(Debug, Clone, Copy, Serialize)]
560#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
561pub enum ErrorCode {
562 DatabaseError,
564 LlmError,
566 AuthenticationFailed,
568 AuthorizationFailed,
570 NotFound,
572 InvalidInput,
574 ConfigurationError,
576 ExternalServiceError,
578 InternalError,
580}
581
582#[derive(Debug, thiserror::Error)]
584pub enum AppError {
585 #[error("Database error: {0}")]
587 Database(String),
588
589 #[error("LLM error: {0}")]
591 LLM(String),
592
593 #[error("Authentication error: {0}")]
595 Auth(String),
596
597 #[error("Not found: {0}")]
599 NotFound(String),
600
601 #[error("Invalid input: {0}")]
603 InvalidInput(String),
604
605 #[error("Configuration error: {0}")]
607 Configuration(String),
608
609 #[error("External service error: {0}")]
611 External(String),
612
613 #[error("Internal error: {0}")]
615 Internal(String),
616
617 #[error("Service unavailable: {0}")]
619 Unavailable(String),
620 #[error("Feature disabled: {0}")]
622 FeatureDisabled(String),
623
624 #[error("Rate limited: {0}")]
626 RateLimited(String),
627}
628
629impl AppError {
630 pub fn code(&self) -> ErrorCode {
632 match self {
633 AppError::Database(_) => ErrorCode::DatabaseError,
634 AppError::LLM(_) => ErrorCode::LlmError,
635 AppError::Auth(_) => ErrorCode::AuthenticationFailed,
636 AppError::NotFound(_) => ErrorCode::NotFound,
637 AppError::InvalidInput(_) => ErrorCode::InvalidInput,
638 AppError::Configuration(_) => ErrorCode::ConfigurationError,
639 AppError::External(_) => ErrorCode::ExternalServiceError,
640 AppError::Internal(_) => ErrorCode::InternalError,
641 AppError::Unavailable(_) => ErrorCode::InternalError,
642AppError::RateLimited(_) => ErrorCode::InternalError,
643AppError::FeatureDisabled(_) => ErrorCode::InternalError,
644 }
645 }
646
647 pub fn is_retryable(&self) -> bool {
650 matches!(
651 self,
652 AppError::External(_) | AppError::Unavailable(_) | AppError::RateLimited(_)
653 )
654 }
655
656 pub fn status_code(&self) -> u16 {
658 match self {
659 AppError::Database(_) => 500,
660 AppError::LLM(_) => 500,
661 AppError::Auth(_) => 401,
662 AppError::NotFound(_) => 404,
663 AppError::InvalidInput(_) => 400,
664 AppError::Configuration(_) => 500,
665 AppError::External(_) => 502,
666 AppError::Internal(_) => 500,
667 AppError::Unavailable(_) => 503,
668 AppError::RateLimited(_) => 429,
669 AppError::FeatureDisabled(_) => 400,
670 }
671 }
672}
673
674impl From<std::io::Error> for AppError {
677 fn from(err: std::io::Error) -> Self {
678 AppError::Internal(format!("IO error: {}", err))
679 }
680}
681
682impl From<serde_json::Error> for AppError {
683 fn from(err: serde_json::Error) -> Self {
684 AppError::InvalidInput(format!("JSON error: {}", err))
685 }
686}
687
688pub type Result<T> = std::result::Result<T, AppError>;
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694 use chrono::{TimeZone, Utc};
695
696 #[test]
697 fn test_agent_type_display_all_builtins() {
698 let cases = vec![
699 (AgentType::Router, "router"),
700 (AgentType::Orchestrator, "orchestrator"),
701 (AgentType::Product, "product"),
702 (AgentType::Invoice, "invoice"),
703 (AgentType::Sales, "sales"),
704 (AgentType::Finance, "finance"),
705 (AgentType::HR, "hr"),
706 ];
707 for (agent, expected) in cases {
708 assert_eq!(agent.to_string(), expected);
709 assert_eq!(format!("{}", agent), expected);
710 }
711 }
712
713 #[test]
714 fn test_agent_type_custom_display() {
715 let custom = AgentType::Custom("my-agent".into());
716 assert_eq!(custom.to_string(), "my-agent");
717 assert!(!custom.is_builtin());
718 }
719
720 #[test]
721 fn test_agent_type_from_string_roundtrip() {
722 for name in ["router", "finance", "hr"] {
723 let agent = AgentType::from_string(name);
724 assert_eq!(agent.as_str(), name);
725 }
726 let custom = AgentType::from_string("custom-bot");
727 assert_eq!(custom.as_str(), "custom-bot");
728 }
729
730 #[test]
731 fn test_message_role_serde_roundtrip() {
732 let role = MessageRole::Assistant;
733 let json = serde_json::to_string(&role).unwrap();
734 assert_eq!(json, "\"assistant\"");
735 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
736 assert!(matches!(parsed, MessageRole::Assistant));
737 }
738
739 #[test]
740 fn test_chat_request_serde_roundtrip() {
741 let req = ChatRequest {
742 message: "hello".into(),
743 agent_type: Some(AgentType::Router),
744 context_id: Some("ctx-1".into()),
745 workspace_id: None,
746 model: None,
747 };
748 let json = serde_json::to_string(&req).unwrap();
749 let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
750 assert_eq!(parsed.message, "hello");
751 assert_eq!(parsed.agent_type, Some(AgentType::Router));
752 }
753
754 #[test]
755 fn test_source_serde_roundtrip() {
756 let source = Source {
757 title: "Doc".into(),
758 url: Some("https://example.com".into()),
759 relevance_score: 0.9,
760 };
761 let parsed: Source = serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
762 assert_eq!(parsed.title, "Doc");
763 assert_eq!(parsed.relevance_score, 0.9);
764 }
765
766 #[test]
767 fn test_document_metadata_default_datetime() {
768 let json = r#"{"title":"t","source":"s"}"#;
769 let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
770 assert_eq!(meta.title, "t");
771 assert!(meta.created_at <= Utc::now());
772 }
773
774 #[test]
775 fn test_tool_call_serde_roundtrip() {
776 let call = ToolCall {
777 id: "c1".into(),
778 name: "search".into(),
779 arguments: serde_json::json!({"q": "ares"}),
780 };
781 let parsed: ToolCall = serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
782 assert_eq!(parsed.name, "search");
783 }
784
785 #[test]
786 fn test_app_error_code_mapping() {
787 assert!(matches!(AppError::Database("x".into()).code(), ErrorCode::DatabaseError));
788 assert!(matches!(AppError::Auth("x".into()).code(), ErrorCode::AuthenticationFailed));
789 assert!(matches!(AppError::NotFound("x".into()).code(), ErrorCode::NotFound));
790 assert!(matches!(AppError::RateLimited("x".into()).code(), ErrorCode::InternalError));
791 }
792
793 #[test]
794 fn test_app_error_from_io() {
795 let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
796 assert!(matches!(err, AppError::Internal(_)));
797 assert!(err.to_string().contains("IO error"));
798 }
799
800 #[test]
801 fn test_app_error_from_serde_json() {
802 let bad = "{not json";
803 let err: AppError = serde_json::from_str::<serde_json::Value>(bad).unwrap_err().into();
804 assert!(matches!(err, AppError::InvalidInput(_)));
805 }
806
807 #[test]
808 fn test_search_filter_application() {
809 let doc = Document {
810 id: "1".into(),
811 content: "body".into(),
812 metadata: DocumentMetadata {
813 title: "Guide".into(),
814 source: "docs/rust".into(),
815 tags: vec!["rust".into(), "rag".into()],
816 ..Default::default()
817 },
818 embedding: None,
819 };
820 let filters = vec![
821 SearchFilter { field: "tags".into(), value: "rust".into() },
822 SearchFilter { field: "source".into(), value: "docs/rust".into() },
823 ];
824 let matches = filters.iter().all(|f| match f.field.as_str() {
825 "tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
826 "source" => doc.metadata.source == f.value,
827 _ => false,
828 });
829 assert!(matches);
830 }
831
832 #[test]
833 fn test_rag_search_request_defaults() {
834 let json = r#"{"collection":"c","query":"q"}"#;
835 let req: RagSearchRequest = serde_json::from_str(json).unwrap();
836 assert_eq!(req.limit, 10);
837 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
838 assert!(!req.rerank);
839 }
840
841 #[test]
842 fn test_chat_response_serde_roundtrip() {
843 let resp = ChatResponse {
844 response: "こんにちは 🌍".into(),
845 agent: "router".into(),
846 context_id: String::new(),
847 sources: Some(vec![]),
848 };
849 let parsed: ChatResponse =
850 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
851 assert_eq!(parsed.response, "こんにちは 🌍");
852 assert_eq!(parsed.context_id, "");
853 assert!(parsed.sources.as_ref().unwrap().is_empty());
854 }
855
856 #[test]
857 fn test_research_request_serde_optional_fields() {
858 let json = r#"{"query":"quantum computing"}"#;
859 let req: ResearchRequest = serde_json::from_str(json).unwrap();
860 assert_eq!(req.query, "quantum computing");
861 assert!(req.depth.is_none());
862 assert!(req.max_iterations.is_none());
863
864 let full = ResearchRequest {
865 query: String::new(),
866 depth: Some(0),
867 max_iterations: Some(u8::MAX),
868 };
869 let parsed: ResearchRequest =
870 serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
871 assert_eq!(parsed.query, "");
872 assert_eq!(parsed.depth, Some(0));
873 assert_eq!(parsed.max_iterations, Some(u8::MAX));
874 }
875
876 #[test]
877 fn test_research_response_serde_empty_sources() {
878 let resp = ResearchResponse {
879 findings: String::new(),
880 sources: vec![],
881 duration_ms: 0,
882 };
883 let parsed: ResearchResponse =
884 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
885 assert!(parsed.findings.is_empty());
886 assert!(parsed.sources.is_empty());
887 assert_eq!(parsed.duration_ms, 0);
888 }
889
890 #[test]
891 fn test_rag_ingest_request_defaults_and_unicode() {
892 let json = r#"{"collection":"docs","content":"café ☕"}"#;
893 let req: RagIngestRequest = serde_json::from_str(json).unwrap();
894 assert_eq!(req.content, "café ☕");
895 assert!(req.title.is_none());
896 assert!(req.source.is_none());
897 assert!(req.tags.is_empty());
898 assert!(req.chunking_strategy.is_none());
899 }
900
901 #[test]
902 fn test_rag_ingest_response_serde_roundtrip() {
903 let resp = RagIngestResponse {
904 chunks_created: 0,
905 document_ids: vec![],
906 collection: "empty".into(),
907 };
908 let parsed: RagIngestResponse =
909 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
910 assert_eq!(parsed.chunks_created, 0);
911 assert!(parsed.document_ids.is_empty());
912 }
913
914 #[test]
915 fn test_rag_search_response_serde_roundtrip() {
916 let resp = RagSearchResponse {
917 results: vec![RagSearchResult {
918 id: "d1".into(),
919 content: "match".into(),
920 score: 1.0,
921 metadata: DocumentMetadata::default(),
922 }],
923 total: 1,
924 strategy: "hybrid".into(),
925 reranked: false,
926 duration_ms: u64::MAX,
927 };
928 let parsed: RagSearchResponse =
929 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
930 assert_eq!(parsed.total, 1);
931 assert_eq!(parsed.duration_ms, u64::MAX);
932 }
933
934 #[test]
935 fn test_rag_delete_collection_serde_roundtrip() {
936 let req = RagDeleteCollectionRequest {
937 collection: "to-delete".into(),
938 };
939 let parsed: RagDeleteCollectionRequest =
940 serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
941 assert_eq!(parsed.collection, "to-delete");
942
943 let resp = RagDeleteCollectionResponse {
944 success: true,
945 collection: "to-delete".into(),
946 documents_deleted: 0,
947 };
948 let parsed_resp: RagDeleteCollectionResponse =
949 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
950 assert!(parsed_resp.success);
951 assert_eq!(parsed_resp.documents_deleted, 0);
952 }
953
954 #[test]
955 fn test_semantic_search_request_defaults() {
956 let json = r#"{"collection":"c","query":"q"}"#;
957 let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
958 assert_eq!(req.limit, 10);
959 assert!((req.threshold - 0.0).abs() < f32::EPSILON);
960 }
961
962 #[test]
963 fn test_semantic_search_response_serde_roundtrip() {
964 let resp = SemanticSearchResponse {
965 results: vec![],
966 total: 0,
967 duration_ms: 0,
968 };
969 let parsed: SemanticSearchResponse =
970 serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
971 assert!(parsed.results.is_empty());
972 assert_eq!(parsed.total, 0);
973 }
974
975 #[test]
976 fn test_workflow_request_empty_context_default() {
977 let json = r#"{"query":"run workflow"}"#;
978 let req: WorkflowRequest = serde_json::from_str(json).unwrap();
979 assert_eq!(req.query, "run workflow");
980 assert!(req.context.is_empty());
981
982 let with_ctx = WorkflowRequest {
983 query: "q".into(),
984 context: [("key".into(), serde_json::json!(null))]
985 .into_iter()
986 .collect(),
987 };
988 let parsed: WorkflowRequest =
989 serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
990 assert!(parsed.context.contains_key("key"));
991 }
992
993 #[test]
994 fn test_agent_type_serde_builtin_and_custom_unicode() {
995 for (agent, expected) in [
996 (AgentType::Router, "\"router\""),
997 (AgentType::HR, "\"hr\""),
998 ] {
999 let json = serde_json::to_string(&agent).unwrap();
1000 assert_eq!(json, expected);
1001 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1002 assert_eq!(parsed, agent);
1003 }
1004 let custom = AgentType::Custom("代理-🤖".into());
1005 let json = serde_json::to_string(&custom).unwrap();
1006 let parsed: AgentType = serde_json::from_str(&json).unwrap();
1007 assert_eq!(parsed.as_str(), "代理-🤖");
1008 }
1009
1010 #[test]
1011 fn test_agent_type_partial_eq_and_clone() {
1012 let a = AgentType::Finance;
1013 let b = a.clone();
1014 assert_eq!(a, b);
1015 assert_ne!(a, AgentType::Sales);
1016 assert!(a.is_builtin());
1017 }
1018
1019 #[test]
1020 fn test_message_role_all_variants_serde() {
1021 for (role, expected) in [
1022 (MessageRole::System, "\"system\""),
1023 (MessageRole::User, "\"user\""),
1024 (MessageRole::Assistant, "\"assistant\""),
1025 ] {
1026 let json = serde_json::to_string(&role).unwrap();
1027 assert_eq!(json, expected);
1028 let parsed: MessageRole = serde_json::from_str(&json).unwrap();
1029 assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
1030 }
1031 }
1032
1033 #[test]
1034 fn test_message_serde_roundtrip_unicode() {
1035 let msg = Message {
1036 role: MessageRole::User,
1037 content: "emoji 🚀 & unicode ñ".into(),
1038 timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
1039 };
1040 let parsed: Message =
1041 serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
1042 assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
1043 assert!(matches!(parsed.role, MessageRole::User));
1044 }
1045
1046 #[test]
1047 fn test_user_memory_empty_collections_serde() {
1048 let mem = UserMemory {
1049 user_id: "u0".into(),
1050 preferences: vec![],
1051 facts: vec![],
1052 };
1053 let parsed: UserMemory =
1054 serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
1055 assert!(parsed.preferences.is_empty());
1056 assert!(parsed.facts.is_empty());
1057 }
1058
1059 #[test]
1060 fn test_preference_and_memory_fact_boundary_confidence() {
1061 let pref = Preference {
1062 category: String::new(),
1063 key: "lang".into(),
1064 value: "rust".into(),
1065 confidence: 0.0,
1066 };
1067 let parsed: Preference =
1068 serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
1069 assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
1070
1071 let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
1072 let fact = MemoryFact {
1073 id: "f1".into(),
1074 user_id: "u1".into(),
1075 category: "work".into(),
1076 fact_key: "role".into(),
1077 fact_value: "engineer".into(),
1078 confidence: 1.0,
1079 created_at: now,
1080 updated_at: now,
1081 };
1082 let parsed_fact: MemoryFact =
1083 serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
1084 assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
1085 }
1086
1087 #[test]
1088 fn test_tool_definition_and_result_serde_roundtrip() {
1089 let def = ToolDefinition {
1090 name: "calc".into(),
1091 description: String::new(),
1092 parameters: serde_json::json!({}),
1093 };
1094 let parsed_def: ToolDefinition =
1095 serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
1096 assert_eq!(parsed_def.name, "calc");
1097 assert!(parsed_def.description.is_empty());
1098
1099 let result = ToolResult {
1100 tool_call_id: "c1".into(),
1101 result: serde_json::Value::Null,
1102 };
1103 let parsed_result: ToolResult =
1104 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1105 assert!(parsed_result.result.is_null());
1106 }
1107
1108 #[test]
1109 fn test_document_serde_none_embedding_and_metadata_default() {
1110 let doc = Document {
1111 id: "doc-1".into(),
1112 content: String::new(),
1113 metadata: DocumentMetadata::default(),
1114 embedding: None,
1115 };
1116 let parsed: Document =
1117 serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
1118 assert!(parsed.content.is_empty());
1119 assert!(parsed.embedding.is_none());
1120 assert!(parsed.metadata.title.is_empty());
1121
1122 let default_meta = DocumentMetadata::default();
1123 assert!(default_meta.tags.is_empty());
1124 assert!(default_meta.source.is_empty());
1125 }
1126
1127 #[test]
1128 fn test_search_query_and_result_clone_debug() {
1129 let query = SearchQuery {
1130 query: "find".into(),
1131 limit: 0,
1132 threshold: 1.0,
1133 filters: None,
1134 };
1135 let cloned = query.clone();
1136 assert_eq!(cloned.limit, 0);
1137 assert!(cloned.filters.is_none());
1138 assert!(format!("{:?}", cloned).contains("find"));
1139
1140 let result = SearchResult {
1141 document: Document {
1142 id: "1".into(),
1143 content: "x".into(),
1144 metadata: DocumentMetadata::default(),
1145 embedding: Some(vec![]),
1146 },
1147 score: 0.0,
1148 };
1149 let cloned_result = result.clone();
1150 assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
1151 assert!(cloned_result.document.embedding.as_ref().unwrap().is_empty());
1152 }
1153
1154 #[test]
1155 fn test_agent_context_clone_debug() {
1156 let ctx = AgentContext {
1157 user_id: "u1".into(),
1158 session_id: "s1".into(),
1159 conversation_history: vec![],
1160 user_memory: None,
1161 };
1162 let cloned = ctx.clone();
1163 assert_eq!(cloned.user_id, "u1");
1164 assert!(cloned.user_memory.is_none());
1165 assert!(format!("{:?}", cloned).contains("AgentContext"));
1166 }
1167
1168 #[test]
1169 fn test_login_register_token_claims_serde_roundtrip() {
1170 let login = LoginRequest {
1171 email: "user@example.com".into(),
1172 password: String::new(),
1173 };
1174 let parsed_login: LoginRequest =
1175 serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
1176 assert!(parsed_login.password.is_empty());
1177
1178 let register = RegisterRequest {
1179 email: "new@example.com".into(),
1180 password: "secret".into(),
1181 name: "新規ユーザー".into(),
1182 };
1183 let parsed_register: RegisterRequest =
1184 serde_json::from_str(&serde_json::to_string(®ister).unwrap()).unwrap();
1185 assert_eq!(parsed_register.name, "新規ユーザー");
1186
1187 let token = TokenResponse {
1188 access_token: "access".into(),
1189 refresh_token: "refresh".into(),
1190 expires_in: 0,
1191 };
1192 let parsed_token: TokenResponse =
1193 serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
1194 assert_eq!(parsed_token.expires_in, 0);
1195
1196 let claims = Claims {
1197 sub: "user-1".into(),
1198 email: "user@example.com".into(),
1199 exp: usize::MAX,
1200 iat: 0,
1201 jti: String::new(),
1202 tenant_id: None,
1203 };
1204 let json = serde_json::to_string(&claims).unwrap();
1205 assert!(!json.contains("jti"));
1206 let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
1207 assert_eq!(parsed_claims.jti, "");
1208 }
1209
1210 #[test]
1211 fn test_error_code_serialize_all_variants() {
1212 let codes = [
1213 (ErrorCode::DatabaseError, "DATABASE_ERROR"),
1214 (ErrorCode::LlmError, "LLM_ERROR"),
1215 (ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
1216 (ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
1217 (ErrorCode::NotFound, "NOT_FOUND"),
1218 (ErrorCode::InvalidInput, "INVALID_INPUT"),
1219 (ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
1220 (ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
1221 (ErrorCode::InternalError, "INTERNAL_ERROR"),
1222 ];
1223 for (code, expected) in codes {
1224 let json = serde_json::to_string(&code).unwrap();
1225 assert_eq!(json, format!("\"{}\"", expected));
1226 }
1227 }
1228
1229 #[test]
1230 fn test_app_error_remaining_code_mappings() {
1231 assert!(matches!(
1232 AppError::LLM("x".into()).code(),
1233 ErrorCode::LlmError
1234 ));
1235 assert!(matches!(
1236 AppError::Configuration("x".into()).code(),
1237 ErrorCode::ConfigurationError
1238 ));
1239 assert!(matches!(
1240 AppError::External("x".into()).code(),
1241 ErrorCode::ExternalServiceError
1242 ));
1243 assert!(matches!(
1244 AppError::Unavailable("x".into()).code(),
1245 ErrorCode::InternalError
1246 ));
1247 assert!(matches!(
1248 AppError::FeatureDisabled("x".into()).code(),
1249 ErrorCode::InternalError
1250 ));
1251 assert!(matches!(
1252 AppError::Internal("x".into()).code(),
1253 ErrorCode::InternalError
1254 ));
1255 }
1256
1257 #[test]
1258 fn test_source_clone_and_boundary_scores() {
1259 let source = Source {
1260 title: "t".into(),
1261 url: None,
1262 relevance_score: 0.0,
1263 };
1264 let cloned = source.clone();
1265 assert!(cloned.url.is_none());
1266 assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
1267
1268 let max = Source {
1269 title: "max".into(),
1270 url: Some("https://example.com?q=100%".into()),
1271 relevance_score: 1.0,
1272 };
1273 let parsed: Source =
1274 serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
1275 assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
1276 }
1277
1278 #[test]
1279 fn test_chat_request_workspace_id_serde_roundtrip() {
1280 let req = ChatRequest {
1281 message: "ping".into(),
1282 agent_type: None,
1283 context_id: None,
1284 workspace_id: Some("ws-éruka-42".into()),
1285 model: None,
1286 };
1287 let json = serde_json::to_string(&req).unwrap();
1288 assert!(json.contains("workspace_id"));
1289 let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
1290 assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
1291 }
1292
1293 #[test]
1294 fn test_rag_search_result_serde_roundtrip() {
1295 let result = RagSearchResult {
1296 id: "chunk-1".into(),
1297 content: "snippet".into(),
1298 score: 0.75,
1299 metadata: DocumentMetadata {
1300 title: "Guide".into(),
1301 source: "docs/guide.md".into(),
1302 tags: vec!["rag".into()],
1303 ..Default::default()
1304 },
1305 };
1306 let parsed: RagSearchResult =
1307 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1308 assert_eq!(parsed.id, "chunk-1");
1309 assert!((parsed.score - 0.75).abs() < f32::EPSILON);
1310 assert_eq!(parsed.metadata.tags, vec!["rag"]);
1311 }
1312
1313 #[test]
1314 fn test_semantic_search_result_serde_roundtrip() {
1315 let result = SemanticSearchResult {
1316 id: "doc-9".into(),
1317 content: "semantic hit".into(),
1318 similarity: 0.91,
1319 metadata: DocumentMetadata::default(),
1320 };
1321 let parsed: SemanticSearchResult =
1322 serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
1323 assert_eq!(parsed.content, "semantic hit");
1324 assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
1325 }
1326
1327 #[test]
1328 fn test_app_error_into_response_status_codes() {
1329 let cases = [
1330 (AppError::Auth("denied".into()), 401u16),
1331 (AppError::NotFound("gone".into()), 404),
1332 (AppError::InvalidInput("bad".into()), 400),
1333 (AppError::External("upstream".into()), 502),
1334 (AppError::Unavailable("maintenance".into()), 503),
1335 (AppError::RateLimited("slow".into()), 429),
1336 (AppError::FeatureDisabled("off".into()), 400),
1337 (AppError::Database("db".into()), 500),
1338 ];
1339 for (err, expected) in cases {
1340 assert_eq!(err.status_code(), expected);
1341 }
1342 }
1343
1344 #[test]
1345 fn test_agent_type_from_string_is_case_insensitive() {
1346 assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
1347 assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
1348 assert_eq!(
1349 AgentType::from_string("MyCustom"),
1350 AgentType::Custom("MyCustom".into())
1351 );
1352 }
1353}