1use crate::commands::Command;
4use crate::error::{CliError, CliResult};
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone)]
11pub enum SessionsAction {
12 List,
14 Create { name: String },
16 Delete { id: String },
18 Rename { id: String, name: String },
20 Switch { id: String },
22 Info { id: String },
24 Share {
26 expires_in: Option<u64>,
27 no_history: bool,
28 no_context: bool,
29 },
30 ShareList,
32 ShareRevoke { share_id: String },
34 ShareInfo { share_id: String },
36 ShareView { share_id: String },
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SessionInfo {
43 pub id: String,
45 pub name: String,
47 pub created_at: u64,
49 pub modified_at: u64,
51 pub message_count: usize,
53}
54
55pub struct SessionsCommand {
57 action: SessionsAction,
58}
59
60impl SessionsCommand {
61 pub fn new(action: SessionsAction) -> Self {
63 Self { action }
64 }
65
66 fn sessions_dir() -> CliResult<PathBuf> {
68 let home = dirs::home_dir()
69 .ok_or_else(|| CliError::Internal("Could not determine home directory".to_string()))?;
70 let sessions_dir = home.join(".ricecoder").join("sessions");
71
72 fs::create_dir_all(&sessions_dir).map_err(|e| {
74 CliError::Internal(format!("Failed to create sessions directory: {}", e))
75 })?;
76
77 Ok(sessions_dir)
78 }
79
80 fn sessions_index() -> CliResult<PathBuf> {
82 let sessions_dir = Self::sessions_dir()?;
83 Ok(sessions_dir.join("index.json"))
84 }
85
86 fn load_sessions() -> CliResult<Vec<SessionInfo>> {
88 let index_path = Self::sessions_index()?;
89
90 if !index_path.exists() {
91 return Ok(Vec::new());
92 }
93
94 let content = fs::read_to_string(&index_path)
95 .map_err(|e| CliError::Internal(format!("Failed to read sessions index: {}", e)))?;
96
97 if content.trim().is_empty() {
99 return Ok(Vec::new());
100 }
101
102 let sessions: Vec<SessionInfo> = serde_json::from_str(&content)
103 .map_err(|e| CliError::Internal(format!("Failed to parse sessions index: {}", e)))?;
104
105 Ok(sessions)
106 }
107
108 fn save_sessions(sessions: &[SessionInfo]) -> CliResult<()> {
110 let index_path = Self::sessions_index()?;
111
112 let content = serde_json::to_string_pretty(sessions)
113 .map_err(|e| CliError::Internal(format!("Failed to serialize sessions: {}", e)))?;
114
115 fs::write(&index_path, content)
116 .map_err(|e| CliError::Internal(format!("Failed to write sessions index: {}", e)))?;
117
118 Ok(())
119 }
120}
121
122impl Command for SessionsCommand {
123 fn execute(&self) -> CliResult<()> {
124 match &self.action {
125 SessionsAction::List => list_sessions(),
126 SessionsAction::Create { name } => create_session(name),
127 SessionsAction::Delete { id } => delete_session(id),
128 SessionsAction::Rename { id, name } => rename_session(id, name),
129 SessionsAction::Switch { id } => switch_session(id),
130 SessionsAction::Info { id } => show_session_info(id),
131 SessionsAction::Share {
132 expires_in,
133 no_history,
134 no_context,
135 } => handle_share(*expires_in, *no_history, *no_context),
136 SessionsAction::ShareList => handle_share_list(),
137 SessionsAction::ShareRevoke { share_id } => handle_share_revoke(share_id),
138 SessionsAction::ShareInfo { share_id } => handle_share_info(share_id),
139 SessionsAction::ShareView { share_id } => handle_share_view(share_id),
140 }
141 }
142}
143
144fn list_sessions() -> CliResult<()> {
146 let sessions = SessionsCommand::load_sessions()?;
147
148 if sessions.is_empty() {
149 println!("No sessions found. Create one with: rice sessions create <name>");
150 return Ok(());
151 }
152
153 println!("Sessions:");
154 println!();
155
156 for session in sessions {
157 println!(" {} - {}", session.id, session.name);
158 println!(" Messages: {}", session.message_count);
159 println!(" Created: {}", format_timestamp(session.created_at));
160 println!(" Modified: {}", format_timestamp(session.modified_at));
161 println!();
162 }
163
164 Ok(())
165}
166
167fn create_session(name: &str) -> CliResult<()> {
169 let mut sessions = SessionsCommand::load_sessions()?;
170
171 let now = std::time::SystemTime::now()
172 .duration_since(std::time::UNIX_EPOCH)
173 .map(|d| d.as_secs())
174 .unwrap_or(0);
175
176 let id = format!("session-{}", now);
177
178 let session = SessionInfo {
179 id: id.clone(),
180 name: name.to_string(),
181 created_at: now,
182 modified_at: now,
183 message_count: 0,
184 };
185
186 sessions.push(session);
187 SessionsCommand::save_sessions(&sessions)?;
188
189 println!("Created session: {} ({})", id, name);
190 Ok(())
191}
192
193fn delete_session(id: &str) -> CliResult<()> {
195 let mut sessions = SessionsCommand::load_sessions()?;
196
197 let initial_len = sessions.len();
198 sessions.retain(|s| s.id != id);
199
200 if sessions.len() == initial_len {
201 return Err(CliError::Internal(format!("Session not found: {}", id)));
202 }
203
204 SessionsCommand::save_sessions(&sessions)?;
205 println!("Deleted session: {}", id);
206 Ok(())
207}
208
209fn rename_session(id: &str, name: &str) -> CliResult<()> {
211 let mut sessions = SessionsCommand::load_sessions()?;
212
213 let session = sessions
214 .iter_mut()
215 .find(|s| s.id == id)
216 .ok_or_else(|| CliError::Internal(format!("Session not found: {}", id)))?;
217
218 let old_name = session.name.clone();
219 session.name = name.to_string();
220 session.modified_at = std::time::SystemTime::now()
221 .duration_since(std::time::UNIX_EPOCH)
222 .map(|d| d.as_secs())
223 .unwrap_or(0);
224
225 SessionsCommand::save_sessions(&sessions)?;
226 println!("Renamed session from '{}' to '{}'", old_name, name);
227 Ok(())
228}
229
230fn switch_session(id: &str) -> CliResult<()> {
232 let sessions = SessionsCommand::load_sessions()?;
233
234 let session = sessions
235 .iter()
236 .find(|s| s.id == id)
237 .ok_or_else(|| CliError::Internal(format!("Session not found: {}", id)))?;
238
239 let config_path = dirs::home_dir()
241 .ok_or_else(|| CliError::Internal("Could not determine home directory".to_string()))?
242 .join(".ricecoder")
243 .join("current_session.txt");
244
245 fs::write(&config_path, &session.id)
246 .map_err(|e| CliError::Internal(format!("Failed to save current session: {}", e)))?;
247
248 println!("Switched to session: {} ({})", session.id, session.name);
249 Ok(())
250}
251
252fn show_session_info(id: &str) -> CliResult<()> {
254 let sessions = SessionsCommand::load_sessions()?;
255
256 let session = sessions
257 .iter()
258 .find(|s| s.id == id)
259 .ok_or_else(|| CliError::Internal(format!("Session not found: {}", id)))?;
260
261 println!("Session: {}", session.id);
262 println!(" Name: {}", session.name);
263 println!(" Messages: {}", session.message_count);
264 println!(" Created: {}", format_timestamp(session.created_at));
265 println!(" Modified: {}", format_timestamp(session.modified_at));
266 Ok(())
267}
268
269fn format_timestamp(secs: u64) -> String {
271 use std::time::UNIX_EPOCH;
272
273 let duration = std::time::Duration::from_secs(secs);
274 let datetime = UNIX_EPOCH + duration;
275
276 format!(
279 "{} seconds ago",
280 std::time::SystemTime::now()
281 .duration_since(datetime)
282 .map(|d| d.as_secs())
283 .unwrap_or(0)
284 )
285}
286
287fn handle_share(expires_in: Option<u64>, no_history: bool, no_context: bool) -> CliResult<()> {
289 use ricecoder_sessions::{ShareService, SharePermissions};
290 use chrono::Duration;
291
292 let share_service = ShareService::new();
294
295 let include_history = !no_history;
297 let include_context = !no_context;
298
299 let session_id = "current-session";
301
302 let permissions = SharePermissions {
304 read_only: true,
305 include_history,
306 include_context,
307 };
308
309 let expires_in_duration = expires_in.map(|secs| Duration::seconds(secs as i64));
311
312 let share = share_service
314 .generate_share_link(session_id, permissions, expires_in_duration)
315 .map_err(|e| CliError::Internal(format!("Failed to generate share link: {}", e)))?;
316
317 println!("Share link: {}", share.id);
319 println!();
320 println!("Permissions:");
321 println!(" History: {}", if include_history { "Yes" } else { "No" });
322 println!(" Context: {}", if include_context { "Yes" } else { "No" });
323
324 if let Some(expiration) = expires_in {
325 println!(" Expires in: {} seconds", expiration);
326 } else {
327 println!(" Expires: Never");
328 }
329
330 println!();
331 println!("Share this link with others to grant access to your session.");
332
333 Ok(())
334}
335
336fn handle_share_list() -> CliResult<()> {
338 use ricecoder_sessions::ShareService;
339
340 let share_service = ShareService::new();
342
343 let shares = share_service
345 .list_shares()
346 .map_err(|e| CliError::Internal(format!("Failed to list shares: {}", e)))?;
347
348 if shares.is_empty() {
349 println!("Active shares:");
350 println!();
351 println!(" No shares found. Create one with: rice sessions share");
352 println!();
353 return Ok(());
354 }
355
356 println!("Active shares:");
357 println!();
358 println!("{:<40} {:<20} {:<20} {:<20} {:<30}", "Share ID", "Session ID", "Created", "Expires", "Permissions");
359 println!("{}", "-".repeat(130));
360
361 for share in shares {
362 let created = share.created_at.format("%Y-%m-%d %H:%M:%S").to_string();
363 let expires = share
364 .expires_at
365 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
366 .unwrap_or_else(|| "Never".to_string());
367 let permissions = format!(
368 "History: {}, Context: {}",
369 if share.permissions.include_history { "Yes" } else { "No" },
370 if share.permissions.include_context { "Yes" } else { "No" }
371 );
372
373 println!(
374 "{:<40} {:<20} {:<20} {:<20} {:<30}",
375 &share.id[..40.min(share.id.len())],
376 &share.session_id[..20.min(share.session_id.len())],
377 created,
378 expires,
379 &permissions[..30.min(permissions.len())]
380 );
381 }
382
383 println!();
384
385 Ok(())
386}
387
388fn handle_share_revoke(share_id: &str) -> CliResult<()> {
390 use ricecoder_sessions::ShareService;
391
392 let share_service = ShareService::new();
394
395 share_service
397 .revoke_share(share_id)
398 .map_err(|e| CliError::Internal(format!("Failed to revoke share: {}", e)))?;
399
400 println!("Share {} revoked successfully", share_id);
401 Ok(())
402}
403
404fn handle_share_info(share_id: &str) -> CliResult<()> {
406 use ricecoder_sessions::ShareService;
407
408 let share_service = ShareService::new();
410
411 let share = share_service
413 .get_share(share_id)
414 .map_err(|e| CliError::Internal(format!("Failed to get share info: {}", e)))?;
415
416 println!("Share: {}", share.id);
417 println!(" Session: {}", share.session_id);
418 println!(" Created: {}", share.created_at.format("%Y-%m-%d %H:%M:%S"));
419 println!(
420 " Expires: {}",
421 share
422 .expires_at
423 .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
424 .unwrap_or_else(|| "Never".to_string())
425 );
426 println!(" Permissions:");
427 println!(" History: {}", if share.permissions.include_history { "Yes" } else { "No" });
428 println!(" Context: {}", if share.permissions.include_context { "Yes" } else { "No" });
429 println!(" Read-Only: {}", if share.permissions.read_only { "Yes" } else { "No" });
430 println!(" Status: Active");
431
432 Ok(())
433}
434
435fn handle_share_view(share_id: &str) -> CliResult<()> {
437 use ricecoder_sessions::{ShareService, Session, SessionContext, SessionMode};
438
439 let share_service = ShareService::new();
441
442 let share = share_service
444 .get_share(share_id)
445 .map_err(|e| match e {
446 ricecoder_sessions::SessionError::ShareNotFound(_) => {
447 CliError::Internal(format!("Share not found: {}", share_id))
448 }
449 ricecoder_sessions::SessionError::ShareExpired(_) => {
450 CliError::Internal(format!("Share has expired: {}", share_id))
451 }
452 _ => CliError::Internal(format!("Failed to access share: {}", e)),
453 })?;
454
455 let mock_session = Session::new(
458 format!("Shared Session ({})", &share.session_id[..8.min(share.session_id.len())]),
459 SessionContext::new("openai".to_string(), "gpt-4".to_string(), SessionMode::Chat),
460 );
461
462 let shared_session = share_service.create_shared_session_view(&mock_session, &share.permissions);
464
465 display_shared_session(&shared_session, &share)
467}
468
469fn display_shared_session(
471 session: &ricecoder_sessions::Session,
472 share: &ricecoder_sessions::SessionShare,
473) -> CliResult<()> {
474 use ricecoder_sessions::MessageRole;
475
476 println!();
478 println!("╔════════════════════════════════════════════════════════════════╗");
479 println!("║ Shared Session: {} [Read-Only]", session.name);
480 println!("║ Permissions: [History: {}] [Context: {}]",
481 if share.permissions.include_history { "Yes" } else { "No" },
482 if share.permissions.include_context { "Yes" } else { "No" }
483 );
484 println!("╚════════════════════════════════════════════════════════════════╝");
485 println!();
486
487 println!("Session Information:");
489 println!(" Created: {}", session.created_at.format("%Y-%m-%d %H:%M:%S"));
490 if let Some(expires_at) = share.expires_at {
491 println!(" Expires: {}", expires_at.format("%Y-%m-%d %H:%M:%S"));
492 } else {
493 println!(" Expires: Never");
494 }
495 println!(" Status: Read-Only");
496 println!();
497
498 if share.permissions.include_history {
500 if session.history.is_empty() {
501 println!("Messages: (empty)");
502 } else {
503 println!("Messages ({} total):", session.history.len());
504 println!();
505
506 let messages_per_page = 10;
508 let total_messages = session.history.len();
509 let pages = (total_messages + messages_per_page - 1) / messages_per_page;
510 let current_page = 1;
511 let start_idx = (current_page - 1) * messages_per_page;
512 let end_idx = (start_idx + messages_per_page).min(total_messages);
513
514 for (idx, msg) in session.history[start_idx..end_idx].iter().enumerate() {
515 let role_str = match msg.role {
516 MessageRole::User => "User",
517 MessageRole::Assistant => "Assistant",
518 MessageRole::System => "System",
519 };
520
521 println!("[{}] {}: {}", start_idx + idx + 1, role_str, msg.content);
522 println!(" Timestamp: {}", msg.timestamp.format("%Y-%m-%d %H:%M:%S"));
523 println!();
524 }
525
526 println!("Message {} - {} of {} (Page {} of {})",
528 start_idx + 1,
529 end_idx,
530 total_messages,
531 current_page,
532 pages
533 );
534
535 if pages > 1 {
536 println!("(Use 'rice sessions share view <share_id> --page <N>' to view other pages)");
537 }
538 }
539 } else {
540 println!("Messages: (history excluded from share)");
541 }
542
543 println!();
544
545 if share.permissions.include_context {
547 println!("Context:");
548 if let Some(project_path) = &session.context.project_path {
549 println!(" Project: {}", project_path);
550 }
551 println!(" Provider: {}", session.context.provider);
552 println!(" Model: {}", session.context.model);
553
554 if !session.context.files.is_empty() {
555 println!(" Files:");
556 for file in &session.context.files {
557 println!(" - {}", file);
558 }
559 } else {
560 println!(" Files: (none)");
561 }
562 } else {
563 println!("Context: (context excluded from share)");
564 }
565
566 println!();
567 println!("This is a read-only view. You cannot modify this shared session.");
568 println!();
569
570 Ok(())
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576
577 #[test]
578 fn test_sessions_command_creation() {
579 let cmd = SessionsCommand::new(SessionsAction::List);
580 assert!(matches!(cmd.action, SessionsAction::List));
581 }
582
583 #[test]
584 fn test_create_session_action() {
585 let cmd = SessionsCommand::new(SessionsAction::Create {
586 name: "test".to_string(),
587 });
588 assert!(matches!(cmd.action, SessionsAction::Create { .. }));
589 }
590
591 #[test]
592 fn test_delete_session_action() {
593 let cmd = SessionsCommand::new(SessionsAction::Delete {
594 id: "session-1".to_string(),
595 });
596 assert!(matches!(cmd.action, SessionsAction::Delete { .. }));
597 }
598
599 #[test]
600 fn test_session_info_serialization() {
601 let session = SessionInfo {
602 id: "session-1".to_string(),
603 name: "Test Session".to_string(),
604 created_at: 1000,
605 modified_at: 2000,
606 message_count: 5,
607 };
608
609 let json = serde_json::to_string(&session).unwrap();
610 let deserialized: SessionInfo = serde_json::from_str(&json).unwrap();
611
612 assert_eq!(session.id, deserialized.id);
613 assert_eq!(session.name, deserialized.name);
614 assert_eq!(session.message_count, deserialized.message_count);
615 }
616
617 #[test]
618 fn test_rename_session_action() {
619 let cmd = SessionsCommand::new(SessionsAction::Rename {
620 id: "session-1".to_string(),
621 name: "New Name".to_string(),
622 });
623 assert!(matches!(cmd.action, SessionsAction::Rename { .. }));
624 }
625
626 #[test]
627 fn test_switch_session_action() {
628 let cmd = SessionsCommand::new(SessionsAction::Switch {
629 id: "session-1".to_string(),
630 });
631 assert!(matches!(cmd.action, SessionsAction::Switch { .. }));
632 }
633
634 #[test]
635 fn test_info_session_action() {
636 let cmd = SessionsCommand::new(SessionsAction::Info {
637 id: "session-1".to_string(),
638 });
639 assert!(matches!(cmd.action, SessionsAction::Info { .. }));
640 }
641
642 #[test]
643 fn test_share_action() {
644 let cmd = SessionsCommand::new(SessionsAction::Share {
645 expires_in: Some(3600),
646 no_history: false,
647 no_context: false,
648 });
649 assert!(matches!(cmd.action, SessionsAction::Share { .. }));
650 }
651
652 #[test]
653 fn test_share_action_with_flags() {
654 let cmd = SessionsCommand::new(SessionsAction::Share {
655 expires_in: None,
656 no_history: true,
657 no_context: true,
658 });
659 assert!(matches!(cmd.action, SessionsAction::Share { .. }));
660 }
661
662 #[test]
663 fn test_share_list_action() {
664 let cmd = SessionsCommand::new(SessionsAction::ShareList);
665 assert!(matches!(cmd.action, SessionsAction::ShareList));
666 }
667
668 #[test]
669 fn test_share_revoke_action() {
670 let cmd = SessionsCommand::new(SessionsAction::ShareRevoke {
671 share_id: "share-123".to_string(),
672 });
673 assert!(matches!(cmd.action, SessionsAction::ShareRevoke { .. }));
674 }
675
676 #[test]
677 fn test_share_info_action() {
678 let cmd = SessionsCommand::new(SessionsAction::ShareInfo {
679 share_id: "share-123".to_string(),
680 });
681 assert!(matches!(cmd.action, SessionsAction::ShareInfo { .. }));
682 }
683
684 #[test]
685 fn test_share_command_with_expiration() {
686 let cmd = SessionsCommand::new(SessionsAction::Share {
687 expires_in: Some(3600),
688 no_history: false,
689 no_context: false,
690 });
691
692 match cmd.action {
693 SessionsAction::Share {
694 expires_in,
695 no_history,
696 no_context,
697 } => {
698 assert_eq!(expires_in, Some(3600));
699 assert!(!no_history);
700 assert!(!no_context);
701 }
702 _ => panic!("Expected Share action"),
703 }
704 }
705
706 #[test]
707 fn test_share_command_without_history() {
708 let cmd = SessionsCommand::new(SessionsAction::Share {
709 expires_in: None,
710 no_history: true,
711 no_context: false,
712 });
713
714 match cmd.action {
715 SessionsAction::Share {
716 expires_in,
717 no_history,
718 no_context,
719 } => {
720 assert_eq!(expires_in, None);
721 assert!(no_history);
722 assert!(!no_context);
723 }
724 _ => panic!("Expected Share action"),
725 }
726 }
727
728 #[test]
729 fn test_share_command_without_context() {
730 let cmd = SessionsCommand::new(SessionsAction::Share {
731 expires_in: None,
732 no_history: false,
733 no_context: true,
734 });
735
736 match cmd.action {
737 SessionsAction::Share {
738 expires_in,
739 no_history,
740 no_context,
741 } => {
742 assert_eq!(expires_in, None);
743 assert!(!no_history);
744 assert!(no_context);
745 }
746 _ => panic!("Expected Share action"),
747 }
748 }
749
750 #[test]
751 fn test_share_command_all_restrictions() {
752 let cmd = SessionsCommand::new(SessionsAction::Share {
753 expires_in: Some(7200),
754 no_history: true,
755 no_context: true,
756 });
757
758 match cmd.action {
759 SessionsAction::Share {
760 expires_in,
761 no_history,
762 no_context,
763 } => {
764 assert_eq!(expires_in, Some(7200));
765 assert!(no_history);
766 assert!(no_context);
767 }
768 _ => panic!("Expected Share action"),
769 }
770 }
771
772 #[test]
773 fn test_share_revoke_action_with_id() {
774 let share_id = "test-share-id-12345";
775 let cmd = SessionsCommand::new(SessionsAction::ShareRevoke {
776 share_id: share_id.to_string(),
777 });
778
779 match cmd.action {
780 SessionsAction::ShareRevoke { share_id: id } => {
781 assert_eq!(id, share_id);
782 }
783 _ => panic!("Expected ShareRevoke action"),
784 }
785 }
786
787 #[test]
788 fn test_share_info_action_with_id() {
789 let share_id = "test-share-id-67890";
790 let cmd = SessionsCommand::new(SessionsAction::ShareInfo {
791 share_id: share_id.to_string(),
792 });
793
794 match cmd.action {
795 SessionsAction::ShareInfo { share_id: id } => {
796 assert_eq!(id, share_id);
797 }
798 _ => panic!("Expected ShareInfo action"),
799 }
800 }
801
802 #[test]
803 fn test_session_info_with_zero_messages() {
804 let session = SessionInfo {
805 id: "session-1".to_string(),
806 name: "Empty Session".to_string(),
807 created_at: 1000,
808 modified_at: 1000,
809 message_count: 0,
810 };
811
812 assert_eq!(session.message_count, 0);
813 assert_eq!(session.name, "Empty Session");
814 }
815
816 #[test]
817 fn test_session_info_with_many_messages() {
818 let session = SessionInfo {
819 id: "session-2".to_string(),
820 name: "Busy Session".to_string(),
821 created_at: 1000,
822 modified_at: 5000,
823 message_count: 100,
824 };
825
826 assert_eq!(session.message_count, 100);
827 assert!(session.modified_at > session.created_at);
828 }
829
830 #[test]
831 fn test_share_permissions_all_enabled() {
832 use ricecoder_sessions::SharePermissions;
833
834 let perms = SharePermissions {
835 read_only: true,
836 include_history: true,
837 include_context: true,
838 };
839
840 assert!(perms.read_only);
841 assert!(perms.include_history);
842 assert!(perms.include_context);
843 }
844
845 #[test]
846 fn test_share_permissions_history_only() {
847 use ricecoder_sessions::SharePermissions;
848
849 let perms = SharePermissions {
850 read_only: true,
851 include_history: true,
852 include_context: false,
853 };
854
855 assert!(perms.read_only);
856 assert!(perms.include_history);
857 assert!(!perms.include_context);
858 }
859
860 #[test]
861 fn test_share_permissions_context_only() {
862 use ricecoder_sessions::SharePermissions;
863
864 let perms = SharePermissions {
865 read_only: true,
866 include_history: false,
867 include_context: true,
868 };
869
870 assert!(perms.read_only);
871 assert!(!perms.include_history);
872 assert!(perms.include_context);
873 }
874
875 #[test]
876 fn test_share_permissions_nothing_included() {
877 use ricecoder_sessions::SharePermissions;
878
879 let perms = SharePermissions {
880 read_only: true,
881 include_history: false,
882 include_context: false,
883 };
884
885 assert!(perms.read_only);
886 assert!(!perms.include_history);
887 assert!(!perms.include_context);
888 }
889
890 #[test]
891 fn test_share_view_action() {
892 let cmd = SessionsCommand::new(SessionsAction::ShareView {
893 share_id: "share-123".to_string(),
894 });
895 assert!(matches!(cmd.action, SessionsAction::ShareView { .. }));
896 }
897
898 #[test]
899 fn test_share_view_action_with_id() {
900 let share_id = "test-share-view-id";
901 let cmd = SessionsCommand::new(SessionsAction::ShareView {
902 share_id: share_id.to_string(),
903 });
904
905 match cmd.action {
906 SessionsAction::ShareView { share_id: id } => {
907 assert_eq!(id, share_id);
908 }
909 _ => panic!("Expected ShareView action"),
910 }
911 }
912
913 #[test]
914 fn test_share_service_get_share() {
915 use ricecoder_sessions::{ShareService, SharePermissions};
916 use chrono::Duration;
917
918 let service = ShareService::new();
919
920 let permissions = SharePermissions {
922 read_only: true,
923 include_history: true,
924 include_context: true,
925 };
926
927 let share = service
928 .generate_share_link("session-1", permissions, None)
929 .expect("Failed to generate share");
930
931 let retrieved = service
933 .get_share(&share.id)
934 .expect("Failed to retrieve share");
935
936 assert_eq!(retrieved.id, share.id);
937 assert_eq!(retrieved.session_id, "session-1");
938 assert!(retrieved.permissions.read_only);
939 }
940
941 #[test]
942 fn test_share_service_get_nonexistent_share() {
943 use ricecoder_sessions::ShareService;
944
945 let service = ShareService::new();
946
947 let result = service.get_share("nonexistent-share");
949
950 assert!(result.is_err());
951 }
952
953 #[test]
954 fn test_share_service_revoke_share() {
955 use ricecoder_sessions::{ShareService, SharePermissions};
956
957 let service = ShareService::new();
958
959 let permissions = SharePermissions {
961 read_only: true,
962 include_history: true,
963 include_context: true,
964 };
965
966 let share = service
967 .generate_share_link("session-1", permissions, None)
968 .expect("Failed to generate share");
969
970 service
972 .revoke_share(&share.id)
973 .expect("Failed to revoke share");
974
975 let result = service.get_share(&share.id);
977 assert!(result.is_err());
978 }
979
980 #[test]
981 fn test_share_service_list_shares() {
982 use ricecoder_sessions::{ShareService, SharePermissions};
983
984 let service = ShareService::new();
985
986 let permissions = SharePermissions {
988 read_only: true,
989 include_history: true,
990 include_context: true,
991 };
992
993 let share1 = service
994 .generate_share_link("session-1", permissions.clone(), None)
995 .expect("Failed to generate share 1");
996
997 let share2 = service
998 .generate_share_link("session-2", permissions.clone(), None)
999 .expect("Failed to generate share 2");
1000
1001 let shares = service.list_shares().expect("Failed to list shares");
1003
1004 assert!(shares.len() >= 2);
1005 assert!(shares.iter().any(|s| s.id == share1.id));
1006 assert!(shares.iter().any(|s| s.id == share2.id));
1007 }
1008
1009 #[test]
1010 fn test_share_service_create_shared_session_view_with_history() {
1011 use ricecoder_sessions::{ShareService, SharePermissions, Session, SessionContext, SessionMode, Message, MessageRole};
1012
1013 let service = ShareService::new();
1014
1015 let mut session = Session::new(
1017 "Test Session".to_string(),
1018 SessionContext::new("openai".to_string(), "gpt-4".to_string(), SessionMode::Chat),
1019 );
1020
1021 session.history.push(Message::new(
1022 MessageRole::User,
1023 "Hello".to_string(),
1024 ));
1025
1026 session.history.push(Message::new(
1027 MessageRole::Assistant,
1028 "Hi there!".to_string(),
1029 ));
1030
1031 let permissions = SharePermissions {
1033 read_only: true,
1034 include_history: true,
1035 include_context: true,
1036 };
1037
1038 let view = service.create_shared_session_view(&session, &permissions);
1039
1040 assert_eq!(view.history.len(), 2);
1041 }
1042
1043 #[test]
1044 fn test_share_service_create_shared_session_view_without_history() {
1045 use ricecoder_sessions::{ShareService, SharePermissions, Session, SessionContext, SessionMode, Message, MessageRole};
1046
1047 let service = ShareService::new();
1048
1049 let mut session = Session::new(
1051 "Test Session".to_string(),
1052 SessionContext::new("openai".to_string(), "gpt-4".to_string(), SessionMode::Chat),
1053 );
1054
1055 session.history.push(Message::new(
1056 MessageRole::User,
1057 "Hello".to_string(),
1058 ));
1059
1060 session.history.push(Message::new(
1061 MessageRole::Assistant,
1062 "Hi there!".to_string(),
1063 ));
1064
1065 let permissions = SharePermissions {
1067 read_only: true,
1068 include_history: false,
1069 include_context: true,
1070 };
1071
1072 let view = service.create_shared_session_view(&session, &permissions);
1073
1074 assert_eq!(view.history.len(), 0);
1075 }
1076
1077 #[test]
1078 fn test_share_service_create_shared_session_view_without_context() {
1079 use ricecoder_sessions::{ShareService, SharePermissions, Session, SessionContext, SessionMode};
1080
1081 let service = ShareService::new();
1082
1083 let mut session = Session::new(
1085 "Test Session".to_string(),
1086 SessionContext::new("openai".to_string(), "gpt-4".to_string(), SessionMode::Chat),
1087 );
1088
1089 session.context.files.push("file1.rs".to_string());
1090 session.context.files.push("file2.rs".to_string());
1091
1092 let permissions = SharePermissions {
1094 read_only: true,
1095 include_history: true,
1096 include_context: false,
1097 };
1098
1099 let view = service.create_shared_session_view(&session, &permissions);
1100
1101 assert_eq!(view.context.files.len(), 0);
1102 }
1103
1104 #[test]
1105 fn test_share_service_list_shares_for_session() {
1106 use ricecoder_sessions::{ShareService, SharePermissions};
1107
1108 let service = ShareService::new();
1109
1110 let permissions = SharePermissions {
1112 read_only: true,
1113 include_history: true,
1114 include_context: true,
1115 };
1116
1117 let share1 = service
1118 .generate_share_link("session-1", permissions.clone(), None)
1119 .expect("Failed to generate share 1");
1120
1121 let share2 = service
1122 .generate_share_link("session-1", permissions.clone(), None)
1123 .expect("Failed to generate share 2");
1124
1125 let share3 = service
1126 .generate_share_link("session-2", permissions.clone(), None)
1127 .expect("Failed to generate share 3");
1128
1129 let session1_shares = service
1131 .list_shares_for_session("session-1")
1132 .expect("Failed to list shares for session-1");
1133
1134 assert_eq!(session1_shares.len(), 2);
1135 assert!(session1_shares.iter().any(|s| s.id == share1.id));
1136 assert!(session1_shares.iter().any(|s| s.id == share2.id));
1137 assert!(!session1_shares.iter().any(|s| s.id == share3.id));
1138 }
1139
1140 #[test]
1141 fn test_share_service_invalidate_session_shares() {
1142 use ricecoder_sessions::{ShareService, SharePermissions};
1143
1144 let service = ShareService::new();
1145
1146 let permissions = SharePermissions {
1148 read_only: true,
1149 include_history: true,
1150 include_context: true,
1151 };
1152
1153 let share1 = service
1154 .generate_share_link("session-1", permissions.clone(), None)
1155 .expect("Failed to generate share 1");
1156
1157 let share2 = service
1158 .generate_share_link("session-1", permissions.clone(), None)
1159 .expect("Failed to generate share 2");
1160
1161 let invalidated = service
1163 .invalidate_session_shares("session-1")
1164 .expect("Failed to invalidate shares");
1165
1166 assert_eq!(invalidated, 2);
1167
1168 let result1 = service.get_share(&share1.id);
1170 let result2 = service.get_share(&share2.id);
1171
1172 assert!(result1.is_err());
1173 assert!(result2.is_err());
1174 }
1175
1176 #[test]
1177 fn test_share_service_read_only_enforcement() {
1178 use ricecoder_sessions::{ShareService, SharePermissions};
1179
1180 let service = ShareService::new();
1181
1182 let permissions = SharePermissions {
1184 read_only: true,
1185 include_history: true,
1186 include_context: true,
1187 };
1188
1189 let share = service
1190 .generate_share_link("session-1", permissions, None)
1191 .expect("Failed to generate share");
1192
1193 assert!(share.permissions.read_only);
1195 }
1196}