use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
fn default_datetime() -> DateTime<Utc> {
Utc::now()
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChatRequest {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_type: Option<AgentType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChatResponse {
pub response: String,
pub agent: String,
pub context_id: String,
pub sources: Option<Vec<Source>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Source {
pub title: String,
pub url: Option<String>,
pub relevance_score: f32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResearchRequest {
pub query: String,
pub depth: Option<u8>,
pub max_iterations: Option<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResearchResponse {
pub findings: String,
pub sources: Vec<Source>,
pub duration_ms: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RagIngestRequest {
pub collection: String,
pub content: String,
pub title: Option<String>,
pub source: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub chunking_strategy: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RagIngestResponse {
pub chunks_created: usize,
pub document_ids: Vec<String>,
pub collection: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RagSearchRequest {
pub collection: String,
pub query: String,
#[serde(default = "default_search_limit")]
pub limit: usize,
#[serde(default)]
pub strategy: Option<String>,
#[serde(default = "default_search_threshold")]
pub threshold: f32,
#[serde(default)]
pub rerank: bool,
#[serde(default)]
pub reranker_model: Option<String>,
}
fn default_search_limit() -> usize {
10
}
fn default_search_threshold() -> f32 {
0.0
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RagSearchResult {
pub id: String,
pub content: String,
pub score: f32,
pub metadata: DocumentMetadata,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RagSearchResponse {
pub results: Vec<RagSearchResult>,
pub total: usize,
pub strategy: String,
pub reranked: bool,
pub duration_ms: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RagDeleteCollectionRequest {
pub collection: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RagDeleteCollectionResponse {
pub success: bool,
pub collection: String,
pub documents_deleted: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SemanticSearchRequest {
pub collection: String,
pub query: String,
#[serde(default = "default_search_limit")]
pub limit: usize,
#[serde(default = "default_search_threshold")]
pub threshold: f32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SemanticSearchResult {
pub id: String,
pub content: String,
pub similarity: f32,
pub metadata: DocumentMetadata,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SemanticSearchResponse {
pub results: Vec<SemanticSearchResult>,
pub total: usize,
pub duration_ms: u64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct WorkflowRequest {
pub query: String,
#[serde(default)]
pub context: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum AgentType {
Router,
Orchestrator,
Product,
Invoice,
Sales,
Finance,
#[serde(rename = "hr")]
HR,
#[serde(untagged)]
Custom(String),
}
impl AgentType {
pub fn as_str(&self) -> &str {
match self {
AgentType::Router => "router",
AgentType::Orchestrator => "orchestrator",
AgentType::Product => "product",
AgentType::Invoice => "invoice",
AgentType::Sales => "sales",
AgentType::Finance => "finance",
AgentType::HR => "hr",
AgentType::Custom(name) => name,
}
}
pub fn from_string(s: &str) -> Self {
match s.to_lowercase().as_str() {
"router" => AgentType::Router,
"orchestrator" => AgentType::Orchestrator,
"product" => AgentType::Product,
"invoice" => AgentType::Invoice,
"sales" => AgentType::Sales,
"finance" => AgentType::Finance,
"hr" => AgentType::HR,
_ => AgentType::Custom(s.to_string()),
}
}
pub fn is_builtin(&self) -> bool {
!matches!(self, AgentType::Custom(_))
}
}
impl std::fmt::Display for AgentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct AgentContext {
pub user_id: String,
pub session_id: String,
pub conversation_history: Vec<Message>,
pub user_memory: Option<UserMemory>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: MessageRole,
pub content: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMemory {
pub user_id: String,
pub preferences: Vec<Preference>,
pub facts: Vec<MemoryFact>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Preference {
pub category: String,
pub key: String,
pub value: String,
pub confidence: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryFact {
pub id: String,
pub user_id: String,
pub category: String,
pub fact_key: String,
pub fact_value: String,
pub confidence: f32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_call_id: String,
pub result: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
pub id: String,
pub content: String,
pub metadata: DocumentMetadata,
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DocumentMetadata {
#[serde(default)]
pub title: String,
#[serde(default)]
pub source: String,
#[serde(default = "default_datetime")]
pub created_at: DateTime<Utc>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct SearchQuery {
pub query: String,
pub limit: usize,
pub threshold: f32,
pub filters: Option<Vec<SearchFilter>>,
}
#[derive(Debug, Clone)]
pub struct SearchFilter {
pub field: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub document: Document,
pub score: f32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
pub email: String,
pub password: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterRequest {
pub email: String,
pub password: String,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenResponse {
pub access_token: String,
pub refresh_token: String,
pub expires_in: i64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: String,
pub email: String,
pub exp: usize,
pub iat: usize,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub jti: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
DatabaseError,
LlmError,
AuthenticationFailed,
AuthorizationFailed,
NotFound,
InvalidInput,
ConfigurationError,
ExternalServiceError,
InternalError,
}
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("Database error: {0}")]
Database(String),
#[error("LLM error: {0}")]
LLM(String),
#[error("Authentication error: {0}")]
Auth(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("External service error: {0}")]
External(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("Service unavailable: {0}")]
Unavailable(String),
#[error("Feature disabled: {0}")]
FeatureDisabled(String),
#[error("Rate limited: {0}")]
RateLimited(String),
}
impl AppError {
pub fn code(&self) -> ErrorCode {
match self {
AppError::Database(_) => ErrorCode::DatabaseError,
AppError::LLM(_) => ErrorCode::LlmError,
AppError::Auth(_) => ErrorCode::AuthenticationFailed,
AppError::NotFound(_) => ErrorCode::NotFound,
AppError::InvalidInput(_) => ErrorCode::InvalidInput,
AppError::Configuration(_) => ErrorCode::ConfigurationError,
AppError::External(_) => ErrorCode::ExternalServiceError,
AppError::Internal(_) => ErrorCode::InternalError,
AppError::Unavailable(_) => ErrorCode::InternalError,
AppError::RateLimited(_) => ErrorCode::InternalError,
AppError::FeatureDisabled(_) => ErrorCode::InternalError,
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
AppError::External(_) | AppError::Unavailable(_) | AppError::RateLimited(_)
)
}
pub fn status_code(&self) -> u16 {
match self {
AppError::Database(_) => 500,
AppError::LLM(_) => 500,
AppError::Auth(_) => 401,
AppError::NotFound(_) => 404,
AppError::InvalidInput(_) => 400,
AppError::Configuration(_) => 500,
AppError::External(_) => 502,
AppError::Internal(_) => 500,
AppError::Unavailable(_) => 503,
AppError::RateLimited(_) => 429,
AppError::FeatureDisabled(_) => 400,
}
}
}
impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::Internal(format!("IO error: {}", err))
}
}
impl From<serde_json::Error> for AppError {
fn from(err: serde_json::Error) -> Self {
AppError::InvalidInput(format!("JSON error: {}", err))
}
}
pub type Result<T> = std::result::Result<T, AppError>;
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
#[test]
fn test_agent_type_display_all_builtins() {
let cases = vec![
(AgentType::Router, "router"),
(AgentType::Orchestrator, "orchestrator"),
(AgentType::Product, "product"),
(AgentType::Invoice, "invoice"),
(AgentType::Sales, "sales"),
(AgentType::Finance, "finance"),
(AgentType::HR, "hr"),
];
for (agent, expected) in cases {
assert_eq!(agent.to_string(), expected);
assert_eq!(format!("{}", agent), expected);
}
}
#[test]
fn test_agent_type_custom_display() {
let custom = AgentType::Custom("my-agent".into());
assert_eq!(custom.to_string(), "my-agent");
assert!(!custom.is_builtin());
}
#[test]
fn test_agent_type_from_string_roundtrip() {
for name in ["router", "finance", "hr"] {
let agent = AgentType::from_string(name);
assert_eq!(agent.as_str(), name);
}
let custom = AgentType::from_string("custom-bot");
assert_eq!(custom.as_str(), "custom-bot");
}
#[test]
fn test_message_role_serde_roundtrip() {
let role = MessageRole::Assistant;
let json = serde_json::to_string(&role).unwrap();
assert_eq!(json, "\"assistant\"");
let parsed: MessageRole = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, MessageRole::Assistant));
}
#[test]
fn test_chat_request_serde_roundtrip() {
let req = ChatRequest {
message: "hello".into(),
agent_type: Some(AgentType::Router),
context_id: Some("ctx-1".into()),
workspace_id: None,
model: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.message, "hello");
assert_eq!(parsed.agent_type, Some(AgentType::Router));
}
#[test]
fn test_source_serde_roundtrip() {
let source = Source {
title: "Doc".into(),
url: Some("https://example.com".into()),
relevance_score: 0.9,
};
let parsed: Source = serde_json::from_str(&serde_json::to_string(&source).unwrap()).unwrap();
assert_eq!(parsed.title, "Doc");
assert_eq!(parsed.relevance_score, 0.9);
}
#[test]
fn test_document_metadata_default_datetime() {
let json = r#"{"title":"t","source":"s"}"#;
let meta: DocumentMetadata = serde_json::from_str(json).unwrap();
assert_eq!(meta.title, "t");
assert!(meta.created_at <= Utc::now());
}
#[test]
fn test_tool_call_serde_roundtrip() {
let call = ToolCall {
id: "c1".into(),
name: "search".into(),
arguments: serde_json::json!({"q": "ares"}),
};
let parsed: ToolCall = serde_json::from_str(&serde_json::to_string(&call).unwrap()).unwrap();
assert_eq!(parsed.name, "search");
}
#[test]
fn test_app_error_code_mapping() {
assert!(matches!(AppError::Database("x".into()).code(), ErrorCode::DatabaseError));
assert!(matches!(AppError::Auth("x".into()).code(), ErrorCode::AuthenticationFailed));
assert!(matches!(AppError::NotFound("x".into()).code(), ErrorCode::NotFound));
assert!(matches!(AppError::RateLimited("x".into()).code(), ErrorCode::InternalError));
}
#[test]
fn test_app_error_from_io() {
let err: AppError = std::io::Error::new(std::io::ErrorKind::NotFound, "missing").into();
assert!(matches!(err, AppError::Internal(_)));
assert!(err.to_string().contains("IO error"));
}
#[test]
fn test_app_error_from_serde_json() {
let bad = "{not json";
let err: AppError = serde_json::from_str::<serde_json::Value>(bad).unwrap_err().into();
assert!(matches!(err, AppError::InvalidInput(_)));
}
#[test]
fn test_search_filter_application() {
let doc = Document {
id: "1".into(),
content: "body".into(),
metadata: DocumentMetadata {
title: "Guide".into(),
source: "docs/rust".into(),
tags: vec!["rust".into(), "rag".into()],
..Default::default()
},
embedding: None,
};
let filters = vec![
SearchFilter { field: "tags".into(), value: "rust".into() },
SearchFilter { field: "source".into(), value: "docs/rust".into() },
];
let matches = filters.iter().all(|f| match f.field.as_str() {
"tags" => doc.metadata.tags.iter().any(|t| t == &f.value),
"source" => doc.metadata.source == f.value,
_ => false,
});
assert!(matches);
}
#[test]
fn test_rag_search_request_defaults() {
let json = r#"{"collection":"c","query":"q"}"#;
let req: RagSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.limit, 10);
assert!((req.threshold - 0.0).abs() < f32::EPSILON);
assert!(!req.rerank);
}
#[test]
fn test_chat_response_serde_roundtrip() {
let resp = ChatResponse {
response: "こんにちは 🌍".into(),
agent: "router".into(),
context_id: String::new(),
sources: Some(vec![]),
};
let parsed: ChatResponse =
serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
assert_eq!(parsed.response, "こんにちは 🌍");
assert_eq!(parsed.context_id, "");
assert!(parsed.sources.as_ref().unwrap().is_empty());
}
#[test]
fn test_research_request_serde_optional_fields() {
let json = r#"{"query":"quantum computing"}"#;
let req: ResearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.query, "quantum computing");
assert!(req.depth.is_none());
assert!(req.max_iterations.is_none());
let full = ResearchRequest {
query: String::new(),
depth: Some(0),
max_iterations: Some(u8::MAX),
};
let parsed: ResearchRequest =
serde_json::from_str(&serde_json::to_string(&full).unwrap()).unwrap();
assert_eq!(parsed.query, "");
assert_eq!(parsed.depth, Some(0));
assert_eq!(parsed.max_iterations, Some(u8::MAX));
}
#[test]
fn test_research_response_serde_empty_sources() {
let resp = ResearchResponse {
findings: String::new(),
sources: vec![],
duration_ms: 0,
};
let parsed: ResearchResponse =
serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
assert!(parsed.findings.is_empty());
assert!(parsed.sources.is_empty());
assert_eq!(parsed.duration_ms, 0);
}
#[test]
fn test_rag_ingest_request_defaults_and_unicode() {
let json = r#"{"collection":"docs","content":"café ☕"}"#;
let req: RagIngestRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.content, "café ☕");
assert!(req.title.is_none());
assert!(req.source.is_none());
assert!(req.tags.is_empty());
assert!(req.chunking_strategy.is_none());
}
#[test]
fn test_rag_ingest_response_serde_roundtrip() {
let resp = RagIngestResponse {
chunks_created: 0,
document_ids: vec![],
collection: "empty".into(),
};
let parsed: RagIngestResponse =
serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
assert_eq!(parsed.chunks_created, 0);
assert!(parsed.document_ids.is_empty());
}
#[test]
fn test_rag_search_response_serde_roundtrip() {
let resp = RagSearchResponse {
results: vec![RagSearchResult {
id: "d1".into(),
content: "match".into(),
score: 1.0,
metadata: DocumentMetadata::default(),
}],
total: 1,
strategy: "hybrid".into(),
reranked: false,
duration_ms: u64::MAX,
};
let parsed: RagSearchResponse =
serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
assert_eq!(parsed.total, 1);
assert_eq!(parsed.duration_ms, u64::MAX);
}
#[test]
fn test_rag_delete_collection_serde_roundtrip() {
let req = RagDeleteCollectionRequest {
collection: "to-delete".into(),
};
let parsed: RagDeleteCollectionRequest =
serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
assert_eq!(parsed.collection, "to-delete");
let resp = RagDeleteCollectionResponse {
success: true,
collection: "to-delete".into(),
documents_deleted: 0,
};
let parsed_resp: RagDeleteCollectionResponse =
serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
assert!(parsed_resp.success);
assert_eq!(parsed_resp.documents_deleted, 0);
}
#[test]
fn test_semantic_search_request_defaults() {
let json = r#"{"collection":"c","query":"q"}"#;
let req: SemanticSearchRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.limit, 10);
assert!((req.threshold - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_semantic_search_response_serde_roundtrip() {
let resp = SemanticSearchResponse {
results: vec![],
total: 0,
duration_ms: 0,
};
let parsed: SemanticSearchResponse =
serde_json::from_str(&serde_json::to_string(&resp).unwrap()).unwrap();
assert!(parsed.results.is_empty());
assert_eq!(parsed.total, 0);
}
#[test]
fn test_workflow_request_empty_context_default() {
let json = r#"{"query":"run workflow"}"#;
let req: WorkflowRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.query, "run workflow");
assert!(req.context.is_empty());
let with_ctx = WorkflowRequest {
query: "q".into(),
context: [("key".into(), serde_json::json!(null))]
.into_iter()
.collect(),
};
let parsed: WorkflowRequest =
serde_json::from_str(&serde_json::to_string(&with_ctx).unwrap()).unwrap();
assert!(parsed.context.contains_key("key"));
}
#[test]
fn test_agent_type_serde_builtin_and_custom_unicode() {
for (agent, expected) in [
(AgentType::Router, "\"router\""),
(AgentType::HR, "\"hr\""),
] {
let json = serde_json::to_string(&agent).unwrap();
assert_eq!(json, expected);
let parsed: AgentType = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, agent);
}
let custom = AgentType::Custom("代理-🤖".into());
let json = serde_json::to_string(&custom).unwrap();
let parsed: AgentType = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.as_str(), "代理-🤖");
}
#[test]
fn test_agent_type_partial_eq_and_clone() {
let a = AgentType::Finance;
let b = a.clone();
assert_eq!(a, b);
assert_ne!(a, AgentType::Sales);
assert!(a.is_builtin());
}
#[test]
fn test_message_role_all_variants_serde() {
for (role, expected) in [
(MessageRole::System, "\"system\""),
(MessageRole::User, "\"user\""),
(MessageRole::Assistant, "\"assistant\""),
] {
let json = serde_json::to_string(&role).unwrap();
assert_eq!(json, expected);
let parsed: MessageRole = serde_json::from_str(&json).unwrap();
assert_eq!(format!("{:?}", parsed), format!("{:?}", role));
}
}
#[test]
fn test_message_serde_roundtrip_unicode() {
let msg = Message {
role: MessageRole::User,
content: "emoji 🚀 & unicode ñ".into(),
timestamp: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
};
let parsed: Message =
serde_json::from_str(&serde_json::to_string(&msg).unwrap()).unwrap();
assert_eq!(parsed.content, "emoji 🚀 & unicode ñ");
assert!(matches!(parsed.role, MessageRole::User));
}
#[test]
fn test_user_memory_empty_collections_serde() {
let mem = UserMemory {
user_id: "u0".into(),
preferences: vec![],
facts: vec![],
};
let parsed: UserMemory =
serde_json::from_str(&serde_json::to_string(&mem).unwrap()).unwrap();
assert!(parsed.preferences.is_empty());
assert!(parsed.facts.is_empty());
}
#[test]
fn test_preference_and_memory_fact_boundary_confidence() {
let pref = Preference {
category: String::new(),
key: "lang".into(),
value: "rust".into(),
confidence: 0.0,
};
let parsed: Preference =
serde_json::from_str(&serde_json::to_string(&pref).unwrap()).unwrap();
assert!((parsed.confidence - 0.0).abs() < f32::EPSILON);
let now = Utc.with_ymd_and_hms(2024, 6, 1, 12, 0, 0).unwrap();
let fact = MemoryFact {
id: "f1".into(),
user_id: "u1".into(),
category: "work".into(),
fact_key: "role".into(),
fact_value: "engineer".into(),
confidence: 1.0,
created_at: now,
updated_at: now,
};
let parsed_fact: MemoryFact =
serde_json::from_str(&serde_json::to_string(&fact).unwrap()).unwrap();
assert!((parsed_fact.confidence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_tool_definition_and_result_serde_roundtrip() {
let def = ToolDefinition {
name: "calc".into(),
description: String::new(),
parameters: serde_json::json!({}),
};
let parsed_def: ToolDefinition =
serde_json::from_str(&serde_json::to_string(&def).unwrap()).unwrap();
assert_eq!(parsed_def.name, "calc");
assert!(parsed_def.description.is_empty());
let result = ToolResult {
tool_call_id: "c1".into(),
result: serde_json::Value::Null,
};
let parsed_result: ToolResult =
serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
assert!(parsed_result.result.is_null());
}
#[test]
fn test_document_serde_none_embedding_and_metadata_default() {
let doc = Document {
id: "doc-1".into(),
content: String::new(),
metadata: DocumentMetadata::default(),
embedding: None,
};
let parsed: Document =
serde_json::from_str(&serde_json::to_string(&doc).unwrap()).unwrap();
assert!(parsed.content.is_empty());
assert!(parsed.embedding.is_none());
assert!(parsed.metadata.title.is_empty());
let default_meta = DocumentMetadata::default();
assert!(default_meta.tags.is_empty());
assert!(default_meta.source.is_empty());
}
#[test]
fn test_search_query_and_result_clone_debug() {
let query = SearchQuery {
query: "find".into(),
limit: 0,
threshold: 1.0,
filters: None,
};
let cloned = query.clone();
assert_eq!(cloned.limit, 0);
assert!(cloned.filters.is_none());
assert!(format!("{:?}", cloned).contains("find"));
let result = SearchResult {
document: Document {
id: "1".into(),
content: "x".into(),
metadata: DocumentMetadata::default(),
embedding: Some(vec![]),
},
score: 0.0,
};
let cloned_result = result.clone();
assert!((cloned_result.score - 0.0).abs() < f32::EPSILON);
assert!(cloned_result.document.embedding.as_ref().unwrap().is_empty());
}
#[test]
fn test_agent_context_clone_debug() {
let ctx = AgentContext {
user_id: "u1".into(),
session_id: "s1".into(),
conversation_history: vec![],
user_memory: None,
};
let cloned = ctx.clone();
assert_eq!(cloned.user_id, "u1");
assert!(cloned.user_memory.is_none());
assert!(format!("{:?}", cloned).contains("AgentContext"));
}
#[test]
fn test_login_register_token_claims_serde_roundtrip() {
let login = LoginRequest {
email: "user@example.com".into(),
password: String::new(),
};
let parsed_login: LoginRequest =
serde_json::from_str(&serde_json::to_string(&login).unwrap()).unwrap();
assert!(parsed_login.password.is_empty());
let register = RegisterRequest {
email: "new@example.com".into(),
password: "secret".into(),
name: "新規ユーザー".into(),
};
let parsed_register: RegisterRequest =
serde_json::from_str(&serde_json::to_string(®ister).unwrap()).unwrap();
assert_eq!(parsed_register.name, "新規ユーザー");
let token = TokenResponse {
access_token: "access".into(),
refresh_token: "refresh".into(),
expires_in: 0,
};
let parsed_token: TokenResponse =
serde_json::from_str(&serde_json::to_string(&token).unwrap()).unwrap();
assert_eq!(parsed_token.expires_in, 0);
let claims = Claims {
sub: "user-1".into(),
email: "user@example.com".into(),
exp: usize::MAX,
iat: 0,
jti: String::new(),
tenant_id: None,
};
let json = serde_json::to_string(&claims).unwrap();
assert!(!json.contains("jti"));
let parsed_claims: Claims = serde_json::from_str(&json).unwrap();
assert_eq!(parsed_claims.jti, "");
}
#[test]
fn test_error_code_serialize_all_variants() {
let codes = [
(ErrorCode::DatabaseError, "DATABASE_ERROR"),
(ErrorCode::LlmError, "LLM_ERROR"),
(ErrorCode::AuthenticationFailed, "AUTHENTICATION_FAILED"),
(ErrorCode::AuthorizationFailed, "AUTHORIZATION_FAILED"),
(ErrorCode::NotFound, "NOT_FOUND"),
(ErrorCode::InvalidInput, "INVALID_INPUT"),
(ErrorCode::ConfigurationError, "CONFIGURATION_ERROR"),
(ErrorCode::ExternalServiceError, "EXTERNAL_SERVICE_ERROR"),
(ErrorCode::InternalError, "INTERNAL_ERROR"),
];
for (code, expected) in codes {
let json = serde_json::to_string(&code).unwrap();
assert_eq!(json, format!("\"{}\"", expected));
}
}
#[test]
fn test_app_error_remaining_code_mappings() {
assert!(matches!(
AppError::LLM("x".into()).code(),
ErrorCode::LlmError
));
assert!(matches!(
AppError::Configuration("x".into()).code(),
ErrorCode::ConfigurationError
));
assert!(matches!(
AppError::External("x".into()).code(),
ErrorCode::ExternalServiceError
));
assert!(matches!(
AppError::Unavailable("x".into()).code(),
ErrorCode::InternalError
));
assert!(matches!(
AppError::FeatureDisabled("x".into()).code(),
ErrorCode::InternalError
));
assert!(matches!(
AppError::Internal("x".into()).code(),
ErrorCode::InternalError
));
}
#[test]
fn test_source_clone_and_boundary_scores() {
let source = Source {
title: "t".into(),
url: None,
relevance_score: 0.0,
};
let cloned = source.clone();
assert!(cloned.url.is_none());
assert!((cloned.relevance_score - 0.0).abs() < f32::EPSILON);
let max = Source {
title: "max".into(),
url: Some("https://example.com?q=100%".into()),
relevance_score: 1.0,
};
let parsed: Source =
serde_json::from_str(&serde_json::to_string(&max).unwrap()).unwrap();
assert!((parsed.relevance_score - 1.0).abs() < f32::EPSILON);
}
#[test]
fn test_chat_request_workspace_id_serde_roundtrip() {
let req = ChatRequest {
message: "ping".into(),
agent_type: None,
context_id: None,
workspace_id: Some("ws-éruka-42".into()),
model: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("workspace_id"));
let parsed: ChatRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.workspace_id.as_deref(), Some("ws-éruka-42"));
}
#[test]
fn test_rag_search_result_serde_roundtrip() {
let result = RagSearchResult {
id: "chunk-1".into(),
content: "snippet".into(),
score: 0.75,
metadata: DocumentMetadata {
title: "Guide".into(),
source: "docs/guide.md".into(),
tags: vec!["rag".into()],
..Default::default()
},
};
let parsed: RagSearchResult =
serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
assert_eq!(parsed.id, "chunk-1");
assert!((parsed.score - 0.75).abs() < f32::EPSILON);
assert_eq!(parsed.metadata.tags, vec!["rag"]);
}
#[test]
fn test_semantic_search_result_serde_roundtrip() {
let result = SemanticSearchResult {
id: "doc-9".into(),
content: "semantic hit".into(),
similarity: 0.91,
metadata: DocumentMetadata::default(),
};
let parsed: SemanticSearchResult =
serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap();
assert_eq!(parsed.content, "semantic hit");
assert!((parsed.similarity - 0.91).abs() < f32::EPSILON);
}
#[test]
fn test_app_error_into_response_status_codes() {
let cases = [
(AppError::Auth("denied".into()), 401u16),
(AppError::NotFound("gone".into()), 404),
(AppError::InvalidInput("bad".into()), 400),
(AppError::External("upstream".into()), 502),
(AppError::Unavailable("maintenance".into()), 503),
(AppError::RateLimited("slow".into()), 429),
(AppError::FeatureDisabled("off".into()), 400),
(AppError::Database("db".into()), 500),
];
for (err, expected) in cases {
assert_eq!(err.status_code(), expected);
}
}
#[test]
fn test_agent_type_from_string_is_case_insensitive() {
assert_eq!(AgentType::from_string("ROUTER"), AgentType::Router);
assert_eq!(AgentType::from_string("Hr"), AgentType::HR);
assert_eq!(
AgentType::from_string("MyCustom"),
AgentType::Custom("MyCustom".into())
);
}
}