ricecoder_cli/commands/
sessions.rs

1//! Sessions command - Manage ricecoder sessions
2
3use crate::commands::Command;
4use crate::error::{CliError, CliResult};
5use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::PathBuf;
8
9/// Sessions command action
10#[derive(Debug, Clone)]
11pub enum SessionsAction {
12    /// List all sessions
13    List,
14    /// Create a new session
15    Create { name: String },
16    /// Delete a session
17    Delete { id: String },
18    /// Rename a session
19    Rename { id: String, name: String },
20    /// Switch to a session
21    Switch { id: String },
22    /// Show session info
23    Info { id: String },
24    /// Share a session with a shareable link
25    Share {
26        expires_in: Option<u64>,
27        no_history: bool,
28        no_context: bool,
29    },
30    /// List all active shares
31    ShareList,
32    /// Revoke a share
33    ShareRevoke { share_id: String },
34    /// Show share information
35    ShareInfo { share_id: String },
36    /// View a shared session
37    ShareView { share_id: String },
38}
39
40/// Session data for persistence
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SessionInfo {
43    /// Session ID
44    pub id: String,
45    /// Session name
46    pub name: String,
47    /// Creation timestamp
48    pub created_at: u64,
49    /// Last modified timestamp
50    pub modified_at: u64,
51    /// Number of messages
52    pub message_count: usize,
53}
54
55/// Sessions command handler
56pub struct SessionsCommand {
57    action: SessionsAction,
58}
59
60impl SessionsCommand {
61    /// Create a new sessions command
62    pub fn new(action: SessionsAction) -> Self {
63        Self { action }
64    }
65
66    /// Get the sessions directory
67    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        // Create directory if it doesn't exist
73        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    /// Get the sessions index file
81    fn sessions_index() -> CliResult<PathBuf> {
82        let sessions_dir = Self::sessions_dir()?;
83        Ok(sessions_dir.join("index.json"))
84    }
85
86    /// Load all sessions from index
87    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        // Handle empty file
98        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    /// Save sessions to index
109    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
144/// List all sessions
145fn 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
167/// Create a new session
168fn 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
193/// Delete a session
194fn 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
209/// Rename a session
210fn 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
230/// Switch to a session
231fn 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    // Store current session in config
240    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
252/// Show session info
253fn 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
269/// Format timestamp as human-readable string
270fn 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    // Simple formatting - just show seconds since epoch for now
277    // In production, use chrono or similar
278    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
287/// Handle share command - generate a shareable link
288fn 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    // Create share service
293    let share_service = ShareService::new();
294
295    // Build permission flags
296    let include_history = !no_history;
297    let include_context = !no_context;
298
299    // Get current session ID (for now, use a placeholder)
300    let session_id = "current-session";
301
302    // Create permissions
303    let permissions = SharePermissions {
304        read_only: true,
305        include_history,
306        include_context,
307    };
308
309    // Convert expires_in to Duration
310    let expires_in_duration = expires_in.map(|secs| Duration::seconds(secs as i64));
311
312    // Generate share link
313    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    // Display share information
318    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
336/// Handle share list command - list all active shares
337fn handle_share_list() -> CliResult<()> {
338    use ricecoder_sessions::ShareService;
339
340    // Create share service
341    let share_service = ShareService::new();
342
343    // List all active shares
344    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
388/// Handle share revoke command - revoke a share
389fn handle_share_revoke(share_id: &str) -> CliResult<()> {
390    use ricecoder_sessions::ShareService;
391
392    // Create share service
393    let share_service = ShareService::new();
394
395    // Revoke the share
396    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
404/// Handle share info command - show share details
405fn handle_share_info(share_id: &str) -> CliResult<()> {
406    use ricecoder_sessions::ShareService;
407
408    // Create share service
409    let share_service = ShareService::new();
410
411    // Get share details
412    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
435/// Handle share view command - view a shared session
436fn handle_share_view(share_id: &str) -> CliResult<()> {
437    use ricecoder_sessions::{ShareService, Session, SessionContext, SessionMode};
438
439    // Create share service
440    let share_service = ShareService::new();
441
442    // Validate share exists and is not expired
443    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    // For now, create a mock session to display
456    // In a real implementation, this would retrieve the actual session from storage
457    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    // Create filtered session view based on permissions
463    let shared_session = share_service.create_shared_session_view(&mock_session, &share.permissions);
464
465    // Display shared session with read-only mode enforced
466    display_shared_session(&shared_session, &share)
467}
468
469/// Display a shared session with read-only mode enforced
470fn display_shared_session(
471    session: &ricecoder_sessions::Session,
472    share: &ricecoder_sessions::SessionShare,
473) -> CliResult<()> {
474    use ricecoder_sessions::MessageRole;
475
476    // Display header with permission indicators
477    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    // Display session metadata
488    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    // Display messages if history is included
499    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            // Pagination: show first 10 messages
507            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            // Display pagination info
527            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    // Display context if included
546    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        // Generate a share
921        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        // Verify we can retrieve it
932        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        // Try to get a share that doesn't exist
948        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        // Generate a share
960        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        // Revoke it
971        service
972            .revoke_share(&share.id)
973            .expect("Failed to revoke share");
974
975        // Verify it's gone
976        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        // Generate multiple shares
987        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        // List all shares
1002        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        // Create a session with messages
1016        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        // Create a view with history included
1032        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        // Create a session with messages
1050        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        // Create a view with history excluded
1066        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        // Create a session with context
1084        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        // Create a view with context excluded
1093        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        // Generate shares for different sessions
1111        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        // List shares for session-1
1130        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        // Generate shares for a session
1147        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        // Invalidate all shares for session-1
1162        let invalidated = service
1163            .invalidate_session_shares("session-1")
1164            .expect("Failed to invalidate shares");
1165
1166        assert_eq!(invalidated, 2);
1167
1168        // Verify shares are gone
1169        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        // Generate a share with read_only=true
1183        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        // Verify read_only is enforced
1194        assert!(share.permissions.read_only);
1195    }
1196}