1use crate::commands::{Command, OutputFormat};
4use crate::errors::prelude::Result as CliResult;
5use crate::output::{CliResponse, OutputFormatter};
6use async_trait::async_trait;
7use clap::Subcommand;
8use serde_json::json;
9use vkteams_bot::prelude::*;
10
11use vkteams_bot::storage::config::{DatabaseConfig, StorageSettings};
12use vkteams_bot::storage::{StorageConfig, StorageManager};
13
14#[derive(Debug, Clone, Subcommand)]
15pub enum StorageCommands {
16 Database {
18 #[command(subcommand)]
19 action: DatabaseAction,
20 },
21 Search {
23 #[command(subcommand)]
24 action: SearchAction,
25 },
26 Context {
28 #[command(subcommand)]
29 action: ContextAction,
30 },
31}
32
33#[derive(Debug, Clone, Subcommand)]
34pub enum DatabaseAction {
35 Init,
37 Stats {
39 #[arg(long)]
40 chat_id: Option<String>,
41 #[arg(long)]
42 since: Option<String>,
43 },
44 Cleanup {
46 #[arg(long, default_value = "365")]
47 older_than_days: u32,
48 },
49 VectorMetrics,
51 VectorMaintenance,
53}
54
55#[derive(Debug, Clone, Subcommand)]
56pub enum SearchAction {
57 Semantic {
59 query: String,
61 #[arg(long)]
62 chat_id: Option<String>,
63 #[arg(long, default_value = "10")]
64 limit: usize,
65 },
66 Text {
68 query: String,
70 #[arg(long)]
71 chat_id: Option<String>,
72 #[arg(long, default_value = "10")]
73 limit: i64,
74 },
75 Advanced {
77 #[arg(long)]
78 user_id: Option<String>,
79 #[arg(long)]
80 event_type: Option<String>,
81 #[arg(long)]
82 since: Option<String>,
83 #[arg(long)]
84 until: Option<String>,
85 #[arg(long, default_value = "10")]
86 limit: i64,
87 },
88}
89
90#[derive(Debug, Clone, Subcommand)]
91pub enum ContextAction {
92 Get {
94 #[arg(long)]
95 chat_id: Option<String>,
96 #[arg(long, value_enum, default_value = "recent")]
97 context_type: ContextType,
98 #[arg(long)]
99 timeframe: Option<String>,
100 },
101 Create {
103 #[arg(long)]
104 chat_id: String,
105 #[arg(long)]
106 summary: String,
107 #[arg(long)]
108 context_type: String,
109 },
110}
111
112#[derive(Debug, Clone, clap::ValueEnum)]
113pub enum ContextType {
114 Recent,
115 Topic,
116 UserProfile,
117}
118
119impl StorageCommands {
120 pub async fn execute_with_output(
121 &self,
122 _bot: &Bot,
123 output_format: &OutputFormat,
124 ) -> CliResult<()> {
125 {
126 let response = match self {
127 StorageCommands::Database { action } => self.handle_database(action).await,
128 StorageCommands::Search { action } => self.handle_search(action).await,
129 StorageCommands::Context { action } => self.handle_context(action).await,
130 };
131
132 OutputFormatter::print(&response, output_format)?;
133
134 if !response.success {
135 std::process::exit(1);
136 }
137 }
138
139 Ok(())
140 }
141
142 pub async fn get_storage_manager(&self) -> std::result::Result<StorageManager, String> {
143 let config = match self.load_storage_config().await {
145 Ok(config) => config,
146 Err(e) => return Err(format!("Failed to load storage configuration: {e}")),
147 };
148
149 match StorageManager::new(&config).await {
150 Ok(storage) => Ok(storage),
151 Err(e) => Err(format!("Failed to initialize storage manager: {e}")),
152 }
153 }
154
155 pub async fn load_storage_config(&self) -> std::result::Result<StorageConfig, String> {
156 #[cfg(feature = "storage")]
158 {
159 use vkteams_bot::config::get_config;
160
161 if let Ok(main_config) = get_config() {
163 return Ok(main_config.get_storage_config());
164 }
165 }
166
167 let database_url = std::env::var("DATABASE_URL")
169 .or_else(|_| std::env::var("VKTEAMS_BOT_DATABASE_URL"))
170 .unwrap_or_else(|_| "postgresql://localhost/vkteams_bot".to_string());
171
172 let config = StorageConfig {
173 database: DatabaseConfig {
174 url: database_url,
175 max_connections: 20,
176 connection_timeout: 30,
177 auto_migrate: true,
178 ssl: Default::default(),
179 },
180 settings: StorageSettings {
181 event_retention_days: 365,
182 cleanup_interval_hours: 24,
183 batch_size: 100,
184 max_memory_events: 10000,
185 },
186 ..Default::default()
187 };
188
189 Ok(config)
190 }
191
192 pub async fn handle_database(&self, action: &DatabaseAction) -> CliResponse<serde_json::Value> {
193 let command_name = match action {
194 DatabaseAction::Init => "database-init",
195 DatabaseAction::Stats { .. } => "database-stats",
196 DatabaseAction::Cleanup { .. } => "database-cleanup",
197 DatabaseAction::VectorMetrics => "database-vector-metrics",
198 DatabaseAction::VectorMaintenance => "database-vector-maintenance",
199 };
200
201 let storage = match self.get_storage_manager().await {
202 Ok(storage) => storage,
203 Err(e) => return CliResponse::error(command_name, e.to_string()),
204 };
205
206 match action {
207 DatabaseAction::Init => match storage.initialize().await {
208 Ok(_) => {
209 let data = json!({
210 "message": "Database initialized successfully",
211 "migrations_applied": true
212 });
213 CliResponse::success("database-init", data)
214 }
215 Err(e) => CliResponse::error(
216 "database-init",
217 format!("Failed to initialize database: {e}"),
218 ),
219 },
220 DatabaseAction::Stats {
221 chat_id,
222 since: _since,
223 } => match storage.get_stats(chat_id.as_deref()).await {
224 Ok(stats) => {
225 let data = json!({
226 "total_events": stats.total_events,
227 "total_messages": stats.total_messages,
228 "unique_chats": stats.unique_chats,
229 "unique_users": stats.unique_users,
230 "events_last_24h": stats.events_last_24h,
231 "events_last_week": stats.events_last_week,
232 "oldest_event": stats.oldest_event,
233 "newest_event": stats.newest_event,
234 "storage_size_bytes": stats.storage_size_bytes
235 });
236 CliResponse::success("database-stats", data)
237 }
238 Err(e) => CliResponse::error("database-stats", format!("Failed to get stats: {e}")),
239 },
240 DatabaseAction::Cleanup { older_than_days } => {
241 match storage.cleanup_old_data(*older_than_days).await {
242 Ok(deleted_count) => {
243 let data = json!({
244 "deleted_events": deleted_count,
245 "older_than_days": older_than_days
246 });
247 CliResponse::success("database-cleanup", data)
248 }
249 Err(e) => {
250 CliResponse::error("database-cleanup", format!("Failed to cleanup: {e}"))
251 }
252 }
253 }
254 DatabaseAction::VectorMetrics => {
255 #[cfg(feature = "vector-search")]
256 {
257 match storage.get_vector_metrics().await {
258 Ok(Some(metrics)) => {
259 let data = json!({
260 "collection_name": metrics.collection_name,
261 "total_documents": metrics.total_documents,
262 "total_size_bytes": metrics.total_size_bytes,
263 "total_size_mb": metrics.total_size_bytes as f64 / 1024.0 / 1024.0,
264 "index_size_bytes": metrics.index_size_bytes,
265 "index_size_mb": metrics.index_size_bytes as f64 / 1024.0 / 1024.0,
266 "dimensions": metrics.dimensions,
267 "performance": {
268 "total_queries": metrics.total_queries,
269 "failed_queries": metrics.failed_queries,
270 "success_rate": if metrics.total_queries > 0 {
271 1.0 - (metrics.failed_queries as f64 / metrics.total_queries as f64)
272 } else { 0.0 },
273 "avg_query_time_ms": metrics.avg_query_time_ms,
274 "last_query_time_ms": metrics.last_query_time_ms
275 },
276 "index_usage": {
277 "index_scans": metrics.index_usage.index_scans,
278 "index_tuples_read": metrics.index_usage.index_tuples_read,
279 "index_tuples_fetched": metrics.index_usage.index_tuples_fetched,
280 "cache_hit_ratio": metrics.index_usage.cache_hit_ratio,
281 "index_blocks_read": metrics.index_usage.index_blocks_read,
282 "index_blocks_hit": metrics.index_usage.index_blocks_hit
283 },
284 "last_maintenance": metrics.last_maintenance
285 });
286 CliResponse::success("database-vector-metrics", data)
287 }
288 Ok(None) => CliResponse::error(
289 "database-vector-metrics",
290 "Vector store not configured",
291 ),
292 Err(e) => CliResponse::error(
293 "database-vector-metrics",
294 format!("Failed to get vector metrics: {e}"),
295 ),
296 }
297 }
298 #[cfg(not(feature = "vector-search"))]
299 {
300 CliResponse::error(
301 "database-vector-metrics",
302 "Vector search feature not enabled",
303 )
304 }
305 }
306 DatabaseAction::VectorMaintenance => {
307 #[cfg(feature = "vector-search")]
308 {
309 match storage.perform_vector_maintenance().await {
310 Ok(()) => {
311 let data = json!({
312 "message": "Vector store maintenance completed successfully",
313 "operations": ["VACUUM ANALYZE", "REINDEX"],
314 "timestamp": chrono::Utc::now().to_rfc3339()
315 });
316 CliResponse::success("database-vector-maintenance", data)
317 }
318 Err(e) => CliResponse::error(
319 "database-vector-maintenance",
320 format!("Failed to perform maintenance: {e}"),
321 ),
322 }
323 }
324 #[cfg(not(feature = "vector-search"))]
325 {
326 CliResponse::error(
327 "database-vector-maintenance",
328 "Vector search feature not enabled",
329 )
330 }
331 }
332 }
333 }
334
335 pub async fn handle_search(&self, action: &SearchAction) -> CliResponse<serde_json::Value> {
336 let command_name = match action {
337 SearchAction::Semantic { .. } => "search-semantic",
338 SearchAction::Text { .. } => "search-text",
339 SearchAction::Advanced { .. } => "search-advanced",
340 };
341
342 let storage = match self.get_storage_manager().await {
343 Ok(storage) => storage,
344 Err(e) => return CliResponse::error(command_name, e.to_string()),
345 };
346
347 match action {
348 SearchAction::Semantic {
349 query,
350 chat_id,
351 limit,
352 } => {
353 #[cfg(feature = "vector-search")]
354 {
355 match storage
356 .search_similar_events(query, chat_id.as_deref(), *limit)
357 .await
358 {
359 Ok(results) => {
360 let data = json!({
361 "query": query,
362 "results_count": results.len(),
363 "results": results.into_iter().map(|r| json!({
364 "id": r.id,
365 "content": r.content,
366 "metadata": r.metadata,
367 "score": r.score,
368 "created_at": r.created_at
369 })).collect::<Vec<_>>()
370 });
371 CliResponse::success("search-semantic", data)
372 }
373 Err(e) => CliResponse::error(
374 "search-semantic",
375 format!("Semantic search failed: {e}"),
376 ),
377 }
378 }
379 #[cfg(not(feature = "vector-search"))]
380 {
381 let _ = (query, chat_id, limit); CliResponse::error("search-semantic", "Vector search feature not enabled")
383 }
384 }
385 SearchAction::Text {
386 query,
387 chat_id,
388 limit,
389 } => {
390 match storage
391 .search_messages(query, chat_id.as_deref(), *limit)
392 .await
393 {
394 Ok(messages) => {
395 let data = json!({
396 "query": query,
397 "results_count": messages.len(),
398 "messages": messages.into_iter().map(|m| json!({
399 "id": m.id,
400 "message_id": m.message_id,
401 "user_id": m.user_id,
402 "text": m.text,
403 "timestamp": m.timestamp,
404 "chat_id": m.chat_id
405 })).collect::<Vec<_>>()
406 });
407 CliResponse::success("search-text", data)
408 }
409 Err(e) => CliResponse::error("search-text", format!("Search failed: {e}")),
410 }
411 }
412 SearchAction::Advanced {
413 user_id,
414 event_type,
415 since,
416 until,
417 limit,
418 } => {
419 let since_date = match since.as_ref().map(|s| parse_datetime(s)) {
421 Some(Ok(date)) => Some(date),
422 Some(Err(_)) => {
423 return CliResponse::error(
424 "search-advanced",
425 "Invalid 'since' date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)",
426 );
427 }
428 None => None,
429 };
430
431 let until_date = match until.as_ref().map(|s| parse_datetime(s)) {
432 Some(Ok(date)) => Some(date),
433 Some(Err(_)) => {
434 return CliResponse::error(
435 "search-advanced",
436 "Invalid 'until' date format. Use ISO 8601 format (e.g., 2023-01-01T00:00:00Z)",
437 );
438 }
439 None => None,
440 };
441
442 match storage
443 .search_events_advanced(
444 user_id.as_deref(),
445 event_type.as_deref(),
446 since_date,
447 until_date,
448 *limit,
449 )
450 .await
451 {
452 Ok(events) => {
453 let data = json!({
454 "filters": {
455 "user_id": user_id,
456 "event_type": event_type,
457 "since": since,
458 "until": until,
459 "limit": limit
460 },
461 "results_count": events.len(),
462 "events": events.into_iter().map(|e| json!({
463 "id": e.id,
464 "event_id": e.event_id,
465 "event_type": e.event_type,
466 "chat_id": e.chat_id,
467 "user_id": e.user_id,
468 "timestamp": e.timestamp,
469 "processed_data": e.processed_data
470 })).collect::<Vec<_>>()
471 });
472 CliResponse::success("search-advanced", data)
473 }
474 Err(e) => CliResponse::error(
475 "search-advanced",
476 format!("Advanced search failed: {e}"),
477 ),
478 }
479 }
480 }
481 }
482
483 pub async fn handle_context(&self, action: &ContextAction) -> CliResponse<serde_json::Value> {
484 let command_name = match action {
485 ContextAction::Get { .. } => "context-get",
486 ContextAction::Create { .. } => "context-create",
487 };
488
489 let storage = match self.get_storage_manager().await {
490 Ok(storage) => storage,
491 Err(e) => return CliResponse::error(command_name, e.to_string()),
492 };
493
494 match action {
495 ContextAction::Get {
496 chat_id,
497 context_type: _,
498 timeframe: _,
499 } => {
500 let default_chat_id =
501 std::env::var("VKTEAMS_BOT_CHAT_ID").unwrap_or_else(|_| "default".to_string());
502 let chat_id_ref = chat_id.as_deref().unwrap_or(&default_chat_id);
503
504 match storage.get_recent_events(Some(chat_id_ref), None, 20).await {
506 Ok(events) => {
507 let data = json!({
508 "chat_id": chat_id_ref,
509 "context_type": "recent",
510 "events_count": events.len(),
511 "events": events.into_iter().map(|e| json!({
512 "id": e.id,
513 "event_id": e.event_id,
514 "event_type": e.event_type,
515 "timestamp": e.timestamp,
516 "user_id": e.user_id
517 })).collect::<Vec<_>>()
518 });
519 CliResponse::success("context-get", data)
520 }
521 Err(e) => {
522 CliResponse::error("context-get", format!("Failed to get context: {e}"))
523 }
524 }
525 }
526 ContextAction::Create {
527 chat_id,
528 summary,
529 context_type,
530 } => {
531 let context_id = uuid::Uuid::new_v4().to_string();
533
534 #[cfg(feature = "vector-search")]
536 {
537 use std::collections::HashMap;
538 use vkteams_bot::storage::VectorDocument;
539
540 let mut metadata_map = HashMap::new();
541 metadata_map.insert(
542 "chat_id".to_string(),
543 serde_json::Value::String(chat_id.clone()),
544 );
545 metadata_map.insert(
546 "context_type".to_string(),
547 serde_json::Value::String(format!("{context_type:?}")),
548 );
549 metadata_map.insert(
550 "created_at".to_string(),
551 serde_json::Value::String(chrono::Utc::now().to_rfc3339()),
552 );
553
554 let metadata =
555 serde_json::to_value(metadata_map).unwrap_or(serde_json::Value::Null);
556
557 let dimensions = storage.get_embedding_dimensions();
559
560 let document = VectorDocument {
561 id: context_id.clone(),
562 content: summary.clone(),
563 metadata,
564 embedding: pgvector::Vector::from(vec![0.0; dimensions]), created_at: chrono::Utc::now(),
566 };
567
568 match storage.store_vector_document(&document).await {
569 Ok(_) => {
570 let data = json!({
571 "context_id": context_id,
572 "chat_id": chat_id,
573 "summary": summary,
574 "context_type": format!("{:?}", context_type),
575 "created_at": chrono::Utc::now().to_rfc3339(),
576 "status": "created"
577 });
578 CliResponse::success("context-create", data)
579 }
580 Err(e) => CliResponse::error(
581 "context-create",
582 format!("Failed to create context: {e}"),
583 ),
584 }
585 }
586
587 #[cfg(not(feature = "vector-search"))]
588 {
589 let _ = (chat_id, summary, context_type); CliResponse::error(
591 "context-create",
592 "Vector search feature not enabled. Context creation requires vector storage.",
593 )
594 }
595 }
596 }
597 }
598}
599
600#[async_trait]
601impl Command for StorageCommands {
602 async fn execute(&self, bot: &Bot) -> CliResult<()> {
603 self.execute_with_output(bot, &OutputFormat::Pretty).await
604 }
605
606 fn name(&self) -> &'static str {
607 match self {
608 StorageCommands::Database { .. } => "database",
609 StorageCommands::Search { .. } => "search",
610 StorageCommands::Context { .. } => "context",
611 }
612 }
613
614 fn validate(&self) -> CliResult<()> {
615 Ok(())
617 }
618}
619
620fn parse_datetime(
622 date_str: &str,
623) -> std::result::Result<chrono::DateTime<chrono::Utc>, &'static str> {
624 use chrono::{DateTime, TimeZone};
625
626 if let Ok(dt) = DateTime::parse_from_rfc3339(date_str) {
628 return Ok(dt.with_timezone(&chrono::Utc));
629 }
630
631 if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(date_str, "%Y-%m-%dT%H:%M:%S") {
633 return Ok(chrono::Utc.from_utc_datetime(&dt));
634 }
635
636 if let Ok(date) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
638 && let Some(datetime) = date.and_hms_opt(0, 0, 0)
639 {
640 return Ok(chrono::Utc.from_utc_datetime(&datetime));
641 }
642
643 Err("Invalid date format")
644}
645
646#[cfg(test)]
647mod storage_tests;
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652
653 #[test]
654 fn test_storage_commands_name() {
655 let cmd = StorageCommands::Database {
656 action: DatabaseAction::Init,
657 };
658 assert_eq!(cmd.name(), "database");
659
660 let cmd = StorageCommands::Search {
661 action: SearchAction::Text {
662 query: "test".to_string(),
663 chat_id: None,
664 limit: 10,
665 },
666 };
667 assert_eq!(cmd.name(), "search");
668 }
669
670 #[test]
671 fn test_context_type_enum() {
672 let context_type = ContextType::Recent;
673 assert!(matches!(context_type, ContextType::Recent));
674 }
675
676 #[test]
677 fn test_parse_datetime() {
678 assert!(parse_datetime("2023-01-01T00:00:00Z").is_ok());
680
681 assert!(parse_datetime("2023-01-01T00:00:00").is_ok());
683
684 assert!(parse_datetime("2023-01-01").is_ok());
686
687 assert!(parse_datetime("invalid-date").is_err());
689 }
690
691 #[test]
692 fn test_context_action_variants() {
693 let get_action = ContextAction::Get {
695 chat_id: Some("test_chat".to_string()),
696 context_type: ContextType::Recent,
697 timeframe: None,
698 };
699
700 let create_action = ContextAction::Create {
701 chat_id: "test_chat".to_string(),
702 summary: "Test summary".to_string(),
703 context_type: "recent".to_string(),
704 };
705
706 match get_action {
708 ContextAction::Get { .. } => {}
709 _ => panic!("Expected ContextAction::Get"),
710 }
711
712 match create_action {
713 ContextAction::Create { .. } => {}
714 _ => panic!("Expected ContextAction::Create"),
715 }
716 }
717
718 #[test]
719 fn test_database_action_variants() {
720 let init_action = DatabaseAction::Init;
722 let stats_action = DatabaseAction::Stats {
723 chat_id: Some("test_chat".to_string()),
724 since: Some("2023-01-01".to_string()),
725 };
726 let cleanup_action = DatabaseAction::Cleanup {
727 older_than_days: 30,
728 };
729 let vector_metrics_action = DatabaseAction::VectorMetrics;
730 let vector_maintenance_action = DatabaseAction::VectorMaintenance;
731
732 match init_action {
733 DatabaseAction::Init => {}
734 _ => panic!("Expected DatabaseAction::Init"),
735 }
736
737 match stats_action {
738 DatabaseAction::Stats { .. } => {}
739 _ => panic!("Expected DatabaseAction::Stats"),
740 }
741
742 match cleanup_action {
743 DatabaseAction::Cleanup { older_than_days } => {
744 assert_eq!(older_than_days, 30);
745 }
746 _ => panic!("Expected DatabaseAction::Cleanup"),
747 }
748
749 match vector_metrics_action {
750 DatabaseAction::VectorMetrics => {}
751 _ => panic!("Expected DatabaseAction::VectorMetrics"),
752 }
753
754 match vector_maintenance_action {
755 DatabaseAction::VectorMaintenance => {}
756 _ => panic!("Expected DatabaseAction::VectorMaintenance"),
757 }
758 }
759
760 #[test]
761 fn test_search_action_variants() {
762 let semantic_action = SearchAction::Semantic {
764 query: "test query".to_string(),
765 chat_id: Some("test_chat".to_string()),
766 limit: 5,
767 };
768
769 let text_action = SearchAction::Text {
770 query: "search text".to_string(),
771 chat_id: None,
772 limit: 20,
773 };
774
775 let advanced_action = SearchAction::Advanced {
776 user_id: Some("user123".to_string()),
777 event_type: Some("NewMessage".to_string()),
778 since: Some("2023-01-01".to_string()),
779 until: Some("2023-12-31".to_string()),
780 limit: 50,
781 };
782
783 match semantic_action {
784 SearchAction::Semantic {
785 query,
786 chat_id,
787 limit,
788 } => {
789 assert_eq!(query, "test query");
790 assert_eq!(chat_id, Some("test_chat".to_string()));
791 assert_eq!(limit, 5);
792 }
793 _ => panic!("Expected SearchAction::Semantic"),
794 }
795
796 match text_action {
797 SearchAction::Text {
798 query,
799 chat_id,
800 limit,
801 } => {
802 assert_eq!(query, "search text");
803 assert_eq!(chat_id, None);
804 assert_eq!(limit, 20);
805 }
806 _ => panic!("Expected SearchAction::Text"),
807 }
808
809 match advanced_action {
810 SearchAction::Advanced {
811 user_id,
812 event_type,
813 since,
814 until,
815 limit,
816 } => {
817 assert_eq!(user_id, Some("user123".to_string()));
818 assert_eq!(event_type, Some("NewMessage".to_string()));
819 assert_eq!(since, Some("2023-01-01".to_string()));
820 assert_eq!(until, Some("2023-12-31".to_string()));
821 assert_eq!(limit, 50);
822 }
823 _ => panic!("Expected SearchAction::Advanced"),
824 }
825 }
826
827 #[test]
828 fn test_context_type_enum_values() {
829 let recent = ContextType::Recent;
831 let topic = ContextType::Topic;
832 let user_profile = ContextType::UserProfile;
833
834 match recent {
835 ContextType::Recent => {}
836 _ => panic!("Expected ContextType::Recent"),
837 }
838
839 match topic {
840 ContextType::Topic => {}
841 _ => panic!("Expected ContextType::Topic"),
842 }
843
844 match user_profile {
845 ContextType::UserProfile => {}
846 _ => panic!("Expected ContextType::UserProfile"),
847 }
848 }
849
850 #[test]
851 fn test_storage_commands_validation() {
852 let database_cmd = StorageCommands::Database {
854 action: DatabaseAction::Init,
855 };
856 assert!(database_cmd.validate().is_ok());
857
858 let search_cmd = StorageCommands::Search {
859 action: SearchAction::Text {
860 query: "test".to_string(),
861 chat_id: None,
862 limit: 10,
863 },
864 };
865 assert!(search_cmd.validate().is_ok());
866
867 let context_cmd = StorageCommands::Context {
868 action: ContextAction::Get {
869 chat_id: Some("test".to_string()),
870 context_type: ContextType::Recent,
871 timeframe: None,
872 },
873 };
874 assert!(context_cmd.validate().is_ok());
875 }
876
877 #[test]
878 fn test_storage_commands_debug_and_clone() {
879 let cmd = StorageCommands::Database {
880 action: DatabaseAction::Init,
881 };
882
883 let debug_str = format!("{cmd:?}");
885 assert!(debug_str.contains("Database"));
886 assert!(debug_str.contains("Init"));
887
888 let cloned_cmd = cmd.clone();
890 assert_eq!(cloned_cmd.name(), cmd.name());
891 }
892
893 #[test]
894 fn test_parse_datetime_edge_cases() {
895 assert!(parse_datetime("2023-12-31T23:59:59Z").is_ok());
897 assert!(parse_datetime("2023-01-01T00:00:00+00:00").is_ok());
898 assert!(parse_datetime("2023-06-15T12:30:45").is_ok());
899 assert!(parse_datetime("2023-02-28").is_ok());
900 assert!(parse_datetime("2024-02-29").is_ok()); assert!(parse_datetime("").is_err());
904 assert!(parse_datetime("not-a-date").is_err());
905 assert!(parse_datetime("2023-13-01").is_err()); assert!(parse_datetime("2023-02-30").is_err()); }
908
909 #[tokio::test]
910 async fn test_storage_config_fallback() {
911 let storage_cmd = StorageCommands::Database {
912 action: DatabaseAction::Init,
913 };
914
915 let config_result = storage_cmd.load_storage_config().await;
917 assert!(config_result.is_ok());
918
919 let config = config_result.unwrap();
920 assert_eq!(config.database.max_connections, 20);
921 assert_eq!(config.database.connection_timeout, 30);
922 assert!(config.database.auto_migrate);
923 assert_eq!(config.settings.event_retention_days, 365);
924 assert_eq!(config.settings.cleanup_interval_hours, 24);
925 assert_eq!(config.settings.batch_size, 100);
926 assert_eq!(config.settings.max_memory_events, 10000);
927 }
928
929 #[test]
930 fn test_context_action_get_with_different_types() {
931 let recent_action = ContextAction::Get {
932 chat_id: Some("chat1".to_string()),
933 context_type: ContextType::Recent,
934 timeframe: Some("1d".to_string()),
935 };
936
937 let topic_action = ContextAction::Get {
938 chat_id: Some("chat2".to_string()),
939 context_type: ContextType::Topic,
940 timeframe: Some("1w".to_string()),
941 };
942
943 let user_profile_action = ContextAction::Get {
944 chat_id: Some("chat3".to_string()),
945 context_type: ContextType::UserProfile,
946 timeframe: None,
947 };
948
949 match recent_action {
950 ContextAction::Get {
951 context_type: ContextType::Recent,
952 ..
953 } => {}
954 _ => panic!("Expected Recent context type"),
955 }
956
957 match topic_action {
958 ContextAction::Get {
959 context_type: ContextType::Topic,
960 ..
961 } => {}
962 _ => panic!("Expected Topic context type"),
963 }
964
965 match user_profile_action {
966 ContextAction::Get {
967 context_type: ContextType::UserProfile,
968 ..
969 } => {}
970 _ => panic!("Expected UserProfile context type"),
971 }
972 }
973
974 #[test]
975 fn test_storage_commands_name_variants() {
976 let database_cmd = StorageCommands::Database {
977 action: DatabaseAction::Stats {
978 chat_id: None,
979 since: None,
980 },
981 };
982 assert_eq!(database_cmd.name(), "database");
983
984 let search_cmd = StorageCommands::Search {
985 action: SearchAction::Semantic {
986 query: "test".to_string(),
987 chat_id: None,
988 limit: 10,
989 },
990 };
991 assert_eq!(search_cmd.name(), "search");
992
993 let context_cmd = StorageCommands::Context {
994 action: ContextAction::Create {
995 chat_id: "test".to_string(),
996 summary: "summary".to_string(),
997 context_type: "topic".to_string(),
998 },
999 };
1000 assert_eq!(context_cmd.name(), "context");
1001 }
1002}