Skip to main content

optirs_core/research/
collaboration.rs

1// Collaboration tools for multi-researcher projects
2//
3// This module provides tools for managing collaborative research projects,
4// including real-time editing, version control, communication, and task management.
5
6use crate::error::{OptimError, Result};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::PathBuf;
11
12/// Collaborative workspace for research projects
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct CollaborativeWorkspace {
15    /// Workspace identifier
16    pub id: String,
17    /// Workspace name
18    pub name: String,
19    /// Project members
20    pub members: Vec<ProjectMember>,
21    /// Shared documents
22    pub documents: Vec<SharedDocument>,
23    /// Communication channels
24    pub channels: Vec<CommunicationChannel>,
25    /// Task assignments
26    pub tasks: Vec<Task>,
27    /// Version control information
28    pub version_control: VersionControl,
29    /// Workspace settings
30    pub settings: WorkspaceSettings,
31    /// Access control
32    pub access_control: AccessControl,
33    /// Activity log
34    pub activity_log: Vec<Activity>,
35    /// Creation timestamp
36    pub created_at: DateTime<Utc>,
37    /// Last modified timestamp
38    pub modified_at: DateTime<Utc>,
39}
40
41/// Project member information
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ProjectMember {
44    /// Member identifier
45    pub id: String,
46    /// User information
47    pub user: UserInfo,
48    /// Member role
49    pub role: MemberRole,
50    /// Permissions
51    pub permissions: Vec<Permission>,
52    /// Join date
53    pub joined_at: DateTime<Utc>,
54    /// Last active timestamp
55    pub last_active: DateTime<Utc>,
56    /// Member status
57    pub status: MemberStatus,
58    /// Contribution statistics
59    pub contributions: ContributionStats,
60}
61
62/// User information
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct UserInfo {
65    /// Full name
66    pub name: String,
67    /// Email address
68    pub email: String,
69    /// Institution
70    pub institution: String,
71    /// Profile picture URL
72    pub avatar_url: Option<String>,
73    /// Timezone
74    pub timezone: String,
75    /// Preferred language
76    pub language: String,
77    /// Research interests
78    pub research_interests: Vec<String>,
79}
80
81/// Member roles
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub enum MemberRole {
84    /// Project owner
85    Owner,
86    /// Project administrator
87    Admin,
88    /// Principal investigator
89    PrincipalInvestigator,
90    /// Senior researcher
91    SeniorResearcher,
92    /// Researcher
93    Researcher,
94    /// PhD student
95    PhDStudent,
96    /// Masters student
97    MastersStudent,
98    /// Research assistant
99    ResearchAssistant,
100    /// Collaborator
101    Collaborator,
102    /// Guest
103    Guest,
104    /// Observer (read-only)
105    Observer,
106}
107
108/// Member permissions
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110pub enum Permission {
111    /// Read project data
112    Read,
113    /// Write/edit project data
114    Write,
115    /// Delete project data
116    Delete,
117    /// Manage members
118    ManageMembers,
119    /// Manage permissions
120    ManagePermissions,
121    /// Manage settings
122    ManageSettings,
123    /// Create experiments
124    CreateExperiments,
125    /// Run experiments
126    RunExperiments,
127    /// Publish results
128    PublishResults,
129    /// Access sensitive data
130    AccessSensitiveData,
131}
132
133/// Member status
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135pub enum MemberStatus {
136    /// Active member
137    Active,
138    /// Inactive member
139    Inactive,
140    /// On leave
141    OnLeave,
142    /// Suspended
143    Suspended,
144    /// Former member
145    Former,
146}
147
148/// Contribution statistics
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ContributionStats {
151    /// Number of experiments created
152    pub experiments_created: usize,
153    /// Number of experiments run
154    pub experiments_run: usize,
155    /// Lines of code contributed
156    pub lines_of_code: usize,
157    /// Documents authored
158    pub documents_authored: usize,
159    /// Comments/discussions posted
160    pub comments_posted: usize,
161    /// Reviews conducted
162    pub reviews_conducted: usize,
163    /// Total contribution score
164    pub contribution_score: f64,
165}
166
167/// Shared document in the workspace
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct SharedDocument {
170    /// Document identifier
171    pub id: String,
172    /// Document name
173    pub name: String,
174    /// Document type
175    pub document_type: DocumentType,
176    /// Document content
177    pub content: String,
178    /// Document owner
179    pub owner_id: String,
180    /// Collaborators with edit access
181    pub collaborators: Vec<String>,
182    /// Document version
183    pub version: u32,
184    /// Version history
185    pub version_history: Vec<DocumentVersion>,
186    /// Access permissions
187    pub access_permissions: DocumentPermissions,
188    /// Document metadata
189    pub metadata: DocumentMetadata,
190    /// Creation timestamp
191    pub created_at: DateTime<Utc>,
192    /// Last modified timestamp
193    pub modified_at: DateTime<Utc>,
194}
195
196/// Document types
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
198pub enum DocumentType {
199    /// Research paper/manuscript
200    Manuscript,
201    /// Experiment notes
202    ExperimentNotes,
203    /// Meeting notes
204    MeetingNotes,
205    /// Literature review
206    LiteratureReview,
207    /// Research proposal
208    ResearchProposal,
209    /// Data analysis
210    DataAnalysis,
211    /// Code documentation
212    CodeDocumentation,
213    /// Presentation
214    Presentation,
215    /// Other document
216    Other(String),
217}
218
219/// Document version
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct DocumentVersion {
222    /// Version number
223    pub version: u32,
224    /// Version author
225    pub author_id: String,
226    /// Version content
227    pub content: String,
228    /// Change summary
229    pub change_summary: String,
230    /// Timestamp
231    pub timestamp: DateTime<Utc>,
232    /// Content hash for integrity
233    pub content_hash: String,
234}
235
236/// Document permissions
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct DocumentPermissions {
239    /// Public visibility
240    pub public: bool,
241    /// Read access
242    pub read_access: Vec<String>,
243    /// Write access
244    pub write_access: Vec<String>,
245    /// Admin access
246    pub admin_access: Vec<String>,
247}
248
249/// Document metadata
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct DocumentMetadata {
252    /// Document tags
253    pub tags: Vec<String>,
254    /// Word count
255    pub word_count: usize,
256    /// Character count
257    pub character_count: usize,
258    /// Number of collaborators
259    pub collaborator_count: usize,
260    /// Number of versions
261    pub version_count: usize,
262    /// Last editor
263    pub last_editor_id: String,
264}
265
266/// Communication channel
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct CommunicationChannel {
269    /// Channel identifier
270    pub id: String,
271    /// Channel name
272    pub name: String,
273    /// Channel description
274    pub description: String,
275    /// Channel type
276    pub channel_type: ChannelType,
277    /// Channel members
278    pub members: Vec<String>,
279    /// Messages in the channel
280    pub messages: Vec<Message>,
281    /// Channel settings
282    pub settings: ChannelSettings,
283    /// Member id that created the channel.
284    ///
285    /// Added in 0.3.2: `create_channel` took a `creatorid` and threw it away,
286    /// so a channel carried no record of who had opened it and the creator was
287    /// not even guaranteed to be a member.
288    pub created_by: String,
289    /// Creation timestamp
290    pub created_at: DateTime<Utc>,
291}
292
293/// Channel types
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
295pub enum ChannelType {
296    /// General discussion
297    General,
298    /// Experiment discussion
299    Experiments,
300    /// Paper writing
301    PaperWriting,
302    /// Code review
303    CodeReview,
304    /// Announcements
305    Announcements,
306    /// Random/off-topic
307    Random,
308    /// Private channel
309    Private,
310    /// Direct message
311    DirectMessage,
312}
313
314/// Message in a communication channel
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct Message {
317    /// Message identifier
318    pub id: String,
319    /// Message author
320    pub author_id: String,
321    /// Message content
322    pub content: String,
323    /// Message type
324    pub message_type: MessageType,
325    /// Attachments
326    pub attachments: Vec<Attachment>,
327    /// Replies to this message
328    pub replies: Vec<Message>,
329    /// Reactions
330    pub reactions: Vec<Reaction>,
331    /// Mentions
332    pub mentions: Vec<String>,
333    /// Thread ID (if part of a thread)
334    pub thread_id: Option<String>,
335    /// Timestamp
336    pub timestamp: DateTime<Utc>,
337    /// Edit history
338    pub edit_history: Vec<MessageEdit>,
339}
340
341/// Message types
342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
343pub enum MessageType {
344    /// Regular text message
345    Text,
346    /// Code snippet
347    Code,
348    /// File attachment
349    File,
350    /// System message
351    System,
352    /// Experiment result
353    ExperimentResult,
354    /// Task assignment
355    TaskAssignment,
356    /// Meeting invitation
357    MeetingInvitation,
358}
359
360/// File attachment
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct Attachment {
363    /// File name
364    pub filename: String,
365    /// File size in bytes
366    pub size: usize,
367    /// MIME type
368    pub mime_type: String,
369    /// File path or URL
370    pub file_path: String,
371    /// File hash for integrity
372    pub file_hash: String,
373    /// Upload timestamp
374    pub uploaded_at: DateTime<Utc>,
375}
376
377/// Message reaction
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct Reaction {
380    /// Emoji or reaction type
381    pub emoji: String,
382    /// Users who reacted
383    pub users: Vec<String>,
384    /// Reaction count
385    pub count: usize,
386}
387
388/// Message edit history
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct MessageEdit {
391    /// Original content
392    pub original_content: String,
393    /// Edit timestamp
394    pub edited_at: DateTime<Utc>,
395    /// Edit reason
396    pub edit_reason: Option<String>,
397}
398
399/// Channel settings
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ChannelSettings {
402    /// Notifications enabled
403    pub notifications: bool,
404    /// Archive old messages
405    pub auto_archive: bool,
406    /// Archive threshold (days)
407    pub archive_after_days: u32,
408    /// Allow external invites
409    pub allow_external_invites: bool,
410    /// Moderation settings
411    pub moderation: ModerationSettings,
412}
413
414/// Moderation settings
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ModerationSettings {
417    /// Require approval for new messages
418    pub require_approval: bool,
419    /// Auto-delete inappropriate content
420    pub auto_delete_inappropriate: bool,
421    /// Spam filtering enabled
422    pub spam_filtering: bool,
423    /// Moderators
424    pub moderators: Vec<String>,
425}
426
427/// Task in the collaborative workspace
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct Task {
430    /// Task identifier
431    pub id: String,
432    /// Task title
433    pub title: String,
434    /// Task description
435    pub description: String,
436    /// Task type
437    pub task_type: TaskType,
438    /// Task status
439    pub status: TaskStatus,
440    /// Task priority
441    pub priority: TaskPriority,
442    /// Assigned to
443    pub assigned_to: Vec<String>,
444    /// Created by
445    pub created_by: String,
446    /// Due date
447    pub due_date: Option<DateTime<Utc>>,
448    /// Estimated effort (hours)
449    pub estimated_hours: Option<f64>,
450    /// Actual effort (hours)
451    pub actual_hours: Option<f64>,
452    /// Task dependencies
453    pub dependencies: Vec<String>,
454    /// Subtasks
455    pub subtasks: Vec<Task>,
456    /// Comments
457    pub comments: Vec<TaskComment>,
458    /// Attachments
459    pub attachments: Vec<Attachment>,
460    /// Labels/tags
461    pub labels: Vec<String>,
462    /// Creation timestamp
463    pub created_at: DateTime<Utc>,
464    /// Completion timestamp
465    pub completed_at: Option<DateTime<Utc>>,
466}
467
468/// Task types
469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
470pub enum TaskType {
471    /// Experiment design
472    ExperimentDesign,
473    /// Data collection
474    DataCollection,
475    /// Data analysis
476    DataAnalysis,
477    /// Code development
478    CodeDevelopment,
479    /// Documentation
480    Documentation,
481    /// Literature review
482    LiteratureReview,
483    /// Paper writing
484    PaperWriting,
485    /// Review/feedback
486    Review,
487    /// Meeting
488    Meeting,
489    /// Administrative
490    Administrative,
491    /// Other
492    Other(String),
493}
494
495/// Task status
496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
497pub enum TaskStatus {
498    /// Task not started
499    NotStarted,
500    /// Task in progress
501    InProgress,
502    /// Task on hold
503    OnHold,
504    /// Task completed
505    Completed,
506    /// Task cancelled
507    Cancelled,
508    /// Task needs review
509    NeedsReview,
510    /// Task approved
511    Approved,
512}
513
514/// Task priority
515#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
516pub enum TaskPriority {
517    /// Critical priority
518    Critical,
519    /// High priority
520    High,
521    /// Medium priority
522    Medium,
523    /// Low priority
524    Low,
525}
526
527/// Task comment
528#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct TaskComment {
530    /// Comment ID
531    pub id: String,
532    /// Comment author
533    pub author_id: String,
534    /// Comment content
535    pub content: String,
536    /// Timestamp
537    pub timestamp: DateTime<Utc>,
538}
539
540/// Version control system for the workspace
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct VersionControl {
543    /// Repository URL
544    pub repository_url: Option<String>,
545    /// Current branch
546    pub current_branch: String,
547    /// Available branches
548    pub branches: Vec<String>,
549    /// Commit history
550    pub commits: Vec<Commit>,
551    /// Merge requests
552    pub merge_requests: Vec<MergeRequest>,
553    /// Version control settings
554    pub settings: VersionControlSettings,
555}
556
557/// Git commit information
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct Commit {
560    /// Commit hash
561    pub hash: String,
562    /// Commit author
563    pub author: String,
564    /// Commit message
565    pub message: String,
566    /// Commit timestamp
567    pub timestamp: DateTime<Utc>,
568    /// Modified files
569    pub modified_files: Vec<String>,
570    /// Parent commits
571    pub parents: Vec<String>,
572}
573
574/// Merge/Pull request
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct MergeRequest {
577    /// Request ID
578    pub id: String,
579    /// Request title
580    pub title: String,
581    /// Request description
582    pub description: String,
583    /// Source branch
584    pub source_branch: String,
585    /// Target branch
586    pub target_branch: String,
587    /// Request author
588    pub author: String,
589    /// Reviewers
590    pub reviewers: Vec<String>,
591    /// Request status
592    pub status: MergeRequestStatus,
593    /// Creation timestamp
594    pub created_at: DateTime<Utc>,
595    /// Merge timestamp
596    pub merged_at: Option<DateTime<Utc>>,
597}
598
599/// Merge request status
600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
601pub enum MergeRequestStatus {
602    /// Open for review
603    Open,
604    /// Under review
605    UnderReview,
606    /// Approved
607    Approved,
608    /// Merged
609    Merged,
610    /// Closed without merging
611    Closed,
612    /// Draft
613    Draft,
614}
615
616/// Version control settings
617#[derive(Debug, Clone, Serialize, Deserialize)]
618pub struct VersionControlSettings {
619    /// Auto-commit enabled
620    pub auto_commit: bool,
621    /// Auto-commit frequency (minutes)
622    pub auto_commit_frequency: u32,
623    /// Require review for merges
624    pub require_review: bool,
625    /// Protected branches
626    pub protected_branches: Vec<String>,
627    /// Automatic backups
628    pub automatic_backups: bool,
629}
630
631/// Workspace settings
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct WorkspaceSettings {
634    /// Workspace timezone
635    pub timezone: String,
636    /// Default language
637    pub default_language: String,
638    /// Collaboration settings
639    pub collaboration: CollaborationSettings,
640    /// Notification settings
641    pub notifications: NotificationSettings,
642    /// Integration settings
643    pub integrations: IntegrationSettings,
644}
645
646/// Collaboration settings
647#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct CollaborationSettings {
649    /// Real-time editing enabled
650    pub real_time_editing: bool,
651    /// Auto-save frequency (seconds)
652    pub auto_save_frequency: u32,
653    /// Conflict resolution strategy
654    pub conflict_resolution: ConflictResolution,
655    /// Maximum simultaneous editors
656    pub max_simultaneous_editors: u32,
657}
658
659/// Conflict resolution strategies
660#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
661pub enum ConflictResolution {
662    /// Manual resolution required
663    Manual,
664    /// Last writer wins
665    LastWriterWins,
666    /// First writer wins
667    FirstWriterWins,
668    /// Merge changes
669    Merge,
670}
671
672/// Notification settings
673#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct NotificationSettings {
675    /// Email notifications
676    pub email: bool,
677    /// In-app notifications
678    pub in_app: bool,
679    /// Desktop notifications
680    pub desktop: bool,
681    /// Mobile push notifications
682    pub mobile_push: bool,
683    /// Notification frequency
684    pub frequency: NotificationFrequency,
685}
686
687/// Notification frequency
688#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
689pub enum NotificationFrequency {
690    /// Immediate notifications
691    Immediate,
692    /// Digest every hour
693    Hourly,
694    /// Daily digest
695    Daily,
696    /// Weekly digest
697    Weekly,
698    /// No notifications
699    None,
700}
701
702/// Integration settings
703#[derive(Debug, Clone, Serialize, Deserialize, Default)]
704pub struct IntegrationSettings {
705    /// Slack integration
706    pub slack: Option<SlackIntegration>,
707    /// Email integration
708    pub email: Option<EmailIntegration>,
709    /// Calendar integration
710    pub calendar: Option<CalendarIntegration>,
711    /// Cloud storage integration
712    pub cloud_storage: Option<CloudStorageIntegration>,
713}
714
715/// Slack integration
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct SlackIntegration {
718    /// Webhook URL
719    pub webhook_url: String,
720    /// Default channel
721    pub default_channel: String,
722    /// Enable experiment notifications
723    pub experiment_notifications: bool,
724    /// Enable task notifications
725    pub task_notifications: bool,
726}
727
728/// Email integration
729#[derive(Debug, Clone, Serialize, Deserialize)]
730pub struct EmailIntegration {
731    /// SMTP server
732    pub smtp_server: String,
733    /// SMTP port
734    pub smtp_port: u16,
735    /// Email address
736    pub email_address: String,
737    /// Authentication credentials
738    pub auth_credentials: Option<String>,
739}
740
741/// Calendar integration
742#[derive(Debug, Clone, Serialize, Deserialize)]
743pub struct CalendarIntegration {
744    /// Calendar provider
745    pub provider: CalendarProvider,
746    /// Calendar ID
747    pub calendar_id: String,
748    /// Sync meetings
749    pub sync_meetings: bool,
750    /// Sync deadlines
751    pub sync_deadlines: bool,
752}
753
754/// Calendar providers
755#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
756pub enum CalendarProvider {
757    /// Google Calendar
758    Google,
759    /// Outlook Calendar
760    Outlook,
761    /// Apple Calendar
762    Apple,
763    /// CalDAV
764    CalDAV,
765}
766
767/// Cloud storage integration
768#[derive(Debug, Clone, Serialize, Deserialize)]
769pub struct CloudStorageIntegration {
770    /// Storage provider
771    pub provider: CloudStorageProvider,
772    /// Storage path
773    pub storage_path: String,
774    /// Auto-sync enabled
775    pub auto_sync: bool,
776    /// Sync frequency (minutes)
777    pub sync_frequency: u32,
778}
779
780/// Cloud storage providers
781#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
782pub enum CloudStorageProvider {
783    /// Google Drive
784    GoogleDrive,
785    /// Dropbox
786    Dropbox,
787    /// OneDrive
788    OneDrive,
789    /// Amazon S3
790    AmazonS3,
791    /// Custom provider
792    Custom(String),
793}
794
795/// Access control for the workspace
796#[derive(Debug, Clone, Serialize, Deserialize)]
797pub struct AccessControl {
798    /// Access control lists
799    pub acls: Vec<AccessControlEntry>,
800    /// Default permissions for new members
801    pub default_permissions: Vec<Permission>,
802    /// Guest access allowed
803    pub guest_access: bool,
804    /// Public visibility
805    pub public_visibility: bool,
806    /// Invitation settings
807    pub invitation_settings: InvitationSettings,
808}
809
810/// Access control entry
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct AccessControlEntry {
813    /// Principal (user, group, or role)
814    pub principal: Principal,
815    /// Granted permissions
816    pub permissions: Vec<Permission>,
817    /// Access expiration
818    pub expires_at: Option<DateTime<Utc>>,
819}
820
821/// Principal (user, group, or role)
822#[derive(Debug, Clone, Serialize, Deserialize)]
823pub enum Principal {
824    /// Individual user
825    User(String),
826    /// User group
827    Group(String),
828    /// Member role
829    Role(MemberRole),
830    /// Everyone
831    Everyone,
832}
833
834/// Invitation settings
835#[derive(Debug, Clone, Serialize, Deserialize)]
836pub struct InvitationSettings {
837    /// Require approval for invitations
838    pub require_approval: bool,
839    /// Allow external invitations
840    pub allow_external: bool,
841    /// Invitation expiration (days)
842    pub expiration_days: u32,
843    /// Maximum invitations per user
844    pub max_invitations_per_user: u32,
845}
846
847/// Activity log entry
848#[derive(Debug, Clone, Serialize, Deserialize)]
849pub struct Activity {
850    /// Activity ID
851    pub id: String,
852    /// User who performed the activity
853    pub user_id: String,
854    /// Activity type
855    pub activity_type: ActivityType,
856    /// Activity description
857    pub description: String,
858    /// Affected resources
859    pub resources: Vec<String>,
860    /// Activity metadata
861    pub metadata: HashMap<String, serde_json::Value>,
862    /// Timestamp
863    pub timestamp: DateTime<Utc>,
864}
865
866/// Activity types
867#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
868pub enum ActivityType {
869    /// User joined workspace
870    UserJoined,
871    /// User left workspace
872    UserLeft,
873    /// Document created
874    DocumentCreated,
875    /// Document edited
876    DocumentEdited,
877    /// Document deleted
878    DocumentDeleted,
879    /// Experiment created
880    ExperimentCreated,
881    /// Experiment started
882    ExperimentStarted,
883    /// Experiment completed
884    ExperimentCompleted,
885    /// Task created
886    TaskCreated,
887    /// Task assigned
888    TaskAssigned,
889    /// Task completed
890    TaskCompleted,
891    /// Message posted
892    MessagePosted,
893    /// File uploaded
894    FileUploaded,
895    /// Merge request created
896    MergeRequestCreated,
897    /// Settings changed
898    SettingsChanged,
899}
900
901/// Collaboration manager
902#[derive(Debug)]
903pub struct CollaborationManager {
904    /// Active workspaces
905    workspaces: HashMap<String, CollaborativeWorkspace>,
906    /// Storage directory
907    storage_dir: PathBuf,
908    /// Manager settings
909    settings: CollaborationManagerSettings,
910}
911
912/// Manager settings
913#[derive(Debug, Clone, Serialize, Deserialize)]
914pub struct CollaborationManagerSettings {
915    /// Maximum workspaces per user
916    pub max_workspaces_per_user: u32,
917    /// Default workspace settings
918    pub default_workspace_settings: WorkspaceSettings,
919    /// Backup settings
920    pub backup_settings: BackupSettings,
921}
922
923/// Backup settings
924#[derive(Debug, Clone, Serialize, Deserialize)]
925pub struct BackupSettings {
926    /// Backup enabled
927    pub enabled: bool,
928    /// Backup frequency (hours)
929    pub frequency_hours: u32,
930    /// Backup retention (days)
931    pub retention_days: u32,
932    /// Backup location
933    pub backup_location: PathBuf,
934}
935
936impl CollaborativeWorkspace {
937    /// Create a new collaborative workspace
938    pub fn new(name: &str, owner: UserInfo) -> Self {
939        let now = Utc::now();
940        let workspace_id = uuid::Uuid::new_v4().to_string();
941        let owner_id = uuid::Uuid::new_v4().to_string();
942
943        let owner_member = ProjectMember {
944            id: owner_id.clone(),
945            user: owner,
946            role: MemberRole::Owner,
947            permissions: vec![
948                Permission::Read,
949                Permission::Write,
950                Permission::Delete,
951                Permission::ManageMembers,
952                Permission::ManagePermissions,
953                Permission::ManageSettings,
954                Permission::CreateExperiments,
955                Permission::RunExperiments,
956                Permission::PublishResults,
957                Permission::AccessSensitiveData,
958            ],
959            joined_at: now,
960            last_active: now,
961            status: MemberStatus::Active,
962            contributions: ContributionStats::default(),
963        };
964
965        Self {
966            id: workspace_id,
967            name: name.to_string(),
968            members: vec![owner_member],
969            documents: Vec::new(),
970            channels: Vec::new(),
971            tasks: Vec::new(),
972            version_control: VersionControl::default(),
973            settings: WorkspaceSettings::default(),
974            access_control: AccessControl::default(),
975            activity_log: Vec::new(),
976            created_at: now,
977            modified_at: now,
978        }
979    }
980
981    /// Add a member to the workspace
982    pub fn add_member(&mut self, user: UserInfo, role: MemberRole) -> Result<()> {
983        let member_id = uuid::Uuid::new_v4().to_string();
984        let permissions = self.get_default_permissions_for_role(&role);
985
986        let member = ProjectMember {
987            id: member_id.clone(),
988            user,
989            role,
990            permissions,
991            joined_at: Utc::now(),
992            last_active: Utc::now(),
993            status: MemberStatus::Active,
994            contributions: ContributionStats::default(),
995        };
996
997        self.members.push(member);
998        self.log_activity(
999            &member_id,
1000            ActivityType::UserJoined,
1001            "User joined the workspace".to_string(),
1002            vec![],
1003        );
1004
1005        Ok(())
1006    }
1007
1008    /// Create a shared document
1009    pub fn create_document(
1010        &mut self,
1011        name: &str,
1012        document_type: DocumentType,
1013        ownerid: &str,
1014    ) -> Result<String> {
1015        if !self.has_permission(ownerid, &Permission::Write) {
1016            return Err(OptimError::InvalidConfig(
1017                "Insufficient permissions to create document".to_string(),
1018            ));
1019        }
1020
1021        let document_id = uuid::Uuid::new_v4().to_string();
1022        let document = SharedDocument {
1023            id: document_id.clone(),
1024            name: name.to_string(),
1025            document_type,
1026            content: String::new(),
1027            owner_id: ownerid.to_string(),
1028            collaborators: Vec::new(),
1029            version: 1,
1030            version_history: Vec::new(),
1031            access_permissions: DocumentPermissions {
1032                public: false,
1033                read_access: self.members.iter().map(|m| m.id.clone()).collect(),
1034                write_access: vec![ownerid.to_string()],
1035                admin_access: vec![ownerid.to_string()],
1036            },
1037            metadata: DocumentMetadata {
1038                tags: Vec::new(),
1039                word_count: 0,
1040                character_count: 0,
1041                collaborator_count: 1,
1042                version_count: 1,
1043                last_editor_id: ownerid.to_string(),
1044            },
1045            created_at: Utc::now(),
1046            modified_at: Utc::now(),
1047        };
1048
1049        self.documents.push(document);
1050        self.log_activity(
1051            ownerid,
1052            ActivityType::DocumentCreated,
1053            format!("Created document: {name}"),
1054            vec![document_id.clone()],
1055        );
1056
1057        Ok(document_id)
1058    }
1059
1060    /// Create a communication channel
1061    pub fn create_channel(
1062        &mut self,
1063        name: &str,
1064        channel_type: ChannelType,
1065        creatorid: &str,
1066    ) -> Result<String> {
1067        let channel_id = uuid::Uuid::new_v4().to_string();
1068        let mut members: Vec<String> = self.members.iter().map(|m| m.id.clone()).collect();
1069        if !members.iter().any(|id| id == creatorid) {
1070            members.push(creatorid.to_string());
1071        }
1072        let channel = CommunicationChannel {
1073            id: channel_id.clone(),
1074            name: name.to_string(),
1075            description: String::new(),
1076            channel_type,
1077            members,
1078            messages: Vec::new(),
1079            settings: ChannelSettings::default(),
1080            created_by: creatorid.to_string(),
1081            created_at: Utc::now(),
1082        };
1083
1084        self.channels.push(channel);
1085        Ok(channel_id)
1086    }
1087
1088    /// Create a task
1089    pub fn create_task(
1090        &mut self,
1091        title: &str,
1092        task_type: TaskType,
1093        creatorid: &str,
1094    ) -> Result<String> {
1095        let task_id = uuid::Uuid::new_v4().to_string();
1096        let task = Task {
1097            id: task_id.clone(),
1098            title: title.to_string(),
1099            description: String::new(),
1100            task_type,
1101            status: TaskStatus::NotStarted,
1102            priority: TaskPriority::Medium,
1103            assigned_to: Vec::new(),
1104            created_by: creatorid.to_string(),
1105            due_date: None,
1106            estimated_hours: None,
1107            actual_hours: None,
1108            dependencies: Vec::new(),
1109            subtasks: Vec::new(),
1110            comments: Vec::new(),
1111            attachments: Vec::new(),
1112            labels: Vec::new(),
1113            created_at: Utc::now(),
1114            completed_at: None,
1115        };
1116
1117        self.tasks.push(task);
1118        self.log_activity(
1119            creatorid,
1120            ActivityType::TaskCreated,
1121            format!("Created task: {title}"),
1122            vec![task_id.clone()],
1123        );
1124
1125        Ok(task_id)
1126    }
1127
1128    /// Check if a user has a specific permission
1129    pub fn has_permission(&self, userid: &str, permission: &Permission) -> bool {
1130        if let Some(member) = self.members.iter().find(|m| m.id == userid) {
1131            member.permissions.contains(permission)
1132        } else {
1133            false
1134        }
1135    }
1136
1137    /// Log an activity
1138    pub fn log_activity(
1139        &mut self,
1140        user_id: &str,
1141        activitytype: ActivityType,
1142        description: String,
1143        resources: Vec<String>,
1144    ) {
1145        let activity = Activity {
1146            id: uuid::Uuid::new_v4().to_string(),
1147            user_id: user_id.to_string(),
1148            activity_type: activitytype,
1149            description,
1150            resources,
1151            metadata: HashMap::new(),
1152            timestamp: Utc::now(),
1153        };
1154
1155        self.activity_log.push(activity);
1156        self.modified_at = Utc::now();
1157    }
1158
1159    fn get_default_permissions_for_role(&self, role: &MemberRole) -> Vec<Permission> {
1160        match role {
1161            MemberRole::Owner | MemberRole::Admin => vec![
1162                Permission::Read,
1163                Permission::Write,
1164                Permission::Delete,
1165                Permission::ManageMembers,
1166                Permission::ManagePermissions,
1167                Permission::ManageSettings,
1168                Permission::CreateExperiments,
1169                Permission::RunExperiments,
1170                Permission::PublishResults,
1171                Permission::AccessSensitiveData,
1172            ],
1173            MemberRole::PrincipalInvestigator | MemberRole::SeniorResearcher => vec![
1174                Permission::Read,
1175                Permission::Write,
1176                Permission::CreateExperiments,
1177                Permission::RunExperiments,
1178                Permission::PublishResults,
1179                Permission::AccessSensitiveData,
1180            ],
1181            MemberRole::Researcher | MemberRole::PhDStudent => vec![
1182                Permission::Read,
1183                Permission::Write,
1184                Permission::CreateExperiments,
1185                Permission::RunExperiments,
1186            ],
1187            MemberRole::MastersStudent | MemberRole::ResearchAssistant => vec![
1188                Permission::Read,
1189                Permission::Write,
1190                Permission::CreateExperiments,
1191            ],
1192            MemberRole::Collaborator => vec![Permission::Read, Permission::Write],
1193            MemberRole::Guest | MemberRole::Observer => vec![Permission::Read],
1194        }
1195    }
1196
1197    /// Generate workspace statistics
1198    pub fn generate_statistics(&self) -> WorkspaceStatistics {
1199        let active_members = self
1200            .members
1201            .iter()
1202            .filter(|m| m.status == MemberStatus::Active)
1203            .count();
1204
1205        let total_documents = self.documents.len();
1206        let total_tasks = self.tasks.len();
1207        let completed_tasks = self
1208            .tasks
1209            .iter()
1210            .filter(|t| t.status == TaskStatus::Completed)
1211            .count();
1212
1213        let total_messages = self.channels.iter().map(|c| c.messages.len()).sum();
1214
1215        let activity_last_30_days = self
1216            .activity_log
1217            .iter()
1218            .filter(|a| {
1219                let thirty_days_ago = Utc::now() - chrono::Duration::days(30);
1220                a.timestamp > thirty_days_ago
1221            })
1222            .count();
1223
1224        WorkspaceStatistics {
1225            total_members: self.members.len(),
1226            active_members,
1227            total_documents,
1228            total_tasks,
1229            completed_tasks,
1230            task_completion_rate: if total_tasks > 0 {
1231                completed_tasks as f64 / total_tasks as f64
1232            } else {
1233                0.0
1234            },
1235            total_messages,
1236            total_channels: self.channels.len(),
1237            activity_last_30_days,
1238            creation_date: self.created_at,
1239            last_activity: self.modified_at,
1240        }
1241    }
1242}
1243
1244/// Workspace statistics
1245#[derive(Debug, Clone, Serialize, Deserialize)]
1246pub struct WorkspaceStatistics {
1247    /// Total number of members
1248    pub total_members: usize,
1249    /// Number of active members
1250    pub active_members: usize,
1251    /// Total number of documents
1252    pub total_documents: usize,
1253    /// Total number of tasks
1254    pub total_tasks: usize,
1255    /// Number of completed tasks
1256    pub completed_tasks: usize,
1257    /// Task completion rate (0.0 to 1.0)
1258    pub task_completion_rate: f64,
1259    /// Total number of messages
1260    pub total_messages: usize,
1261    /// Total number of channels
1262    pub total_channels: usize,
1263    /// Activity count in last 30 days
1264    pub activity_last_30_days: usize,
1265    /// Workspace creation date
1266    pub creation_date: DateTime<Utc>,
1267    /// Last activity timestamp
1268    pub last_activity: DateTime<Utc>,
1269}
1270
1271impl Default for ContributionStats {
1272    fn default() -> Self {
1273        Self {
1274            experiments_created: 0,
1275            experiments_run: 0,
1276            lines_of_code: 0,
1277            documents_authored: 0,
1278            comments_posted: 0,
1279            reviews_conducted: 0,
1280            contribution_score: 0.0,
1281        }
1282    }
1283}
1284
1285impl Default for VersionControl {
1286    fn default() -> Self {
1287        Self {
1288            repository_url: None,
1289            current_branch: "main".to_string(),
1290            branches: vec!["main".to_string()],
1291            commits: Vec::new(),
1292            merge_requests: Vec::new(),
1293            settings: VersionControlSettings::default(),
1294        }
1295    }
1296}
1297
1298impl Default for VersionControlSettings {
1299    fn default() -> Self {
1300        Self {
1301            auto_commit: false,
1302            auto_commit_frequency: 60, // 1 hour
1303            require_review: true,
1304            protected_branches: vec!["main".to_string(), "master".to_string()],
1305            automatic_backups: true,
1306        }
1307    }
1308}
1309
1310impl Default for WorkspaceSettings {
1311    fn default() -> Self {
1312        Self {
1313            timezone: "UTC".to_string(),
1314            default_language: "en".to_string(),
1315            collaboration: CollaborationSettings::default(),
1316            notifications: NotificationSettings::default(),
1317            integrations: IntegrationSettings::default(),
1318        }
1319    }
1320}
1321
1322impl Default for CollaborationSettings {
1323    fn default() -> Self {
1324        Self {
1325            real_time_editing: true,
1326            auto_save_frequency: 30, // 30 seconds
1327            conflict_resolution: ConflictResolution::Manual,
1328            max_simultaneous_editors: 10,
1329        }
1330    }
1331}
1332
1333impl Default for NotificationSettings {
1334    fn default() -> Self {
1335        Self {
1336            email: true,
1337            in_app: true,
1338            desktop: false,
1339            mobile_push: false,
1340            frequency: NotificationFrequency::Daily,
1341        }
1342    }
1343}
1344
1345impl Default for AccessControl {
1346    fn default() -> Self {
1347        Self {
1348            acls: Vec::new(),
1349            default_permissions: vec![Permission::Read],
1350            guest_access: false,
1351            public_visibility: false,
1352            invitation_settings: InvitationSettings::default(),
1353        }
1354    }
1355}
1356
1357impl Default for InvitationSettings {
1358    fn default() -> Self {
1359        Self {
1360            require_approval: true,
1361            allow_external: false,
1362            expiration_days: 7,
1363            max_invitations_per_user: 10,
1364        }
1365    }
1366}
1367
1368impl Default for ChannelSettings {
1369    fn default() -> Self {
1370        Self {
1371            notifications: true,
1372            auto_archive: false,
1373            archive_after_days: 365,
1374            allow_external_invites: false,
1375            moderation: ModerationSettings::default(),
1376        }
1377    }
1378}
1379
1380impl Default for ModerationSettings {
1381    fn default() -> Self {
1382        Self {
1383            require_approval: false,
1384            auto_delete_inappropriate: false,
1385            spam_filtering: true,
1386            moderators: Vec::new(),
1387        }
1388    }
1389}
1390
1391impl Default for BackupSettings {
1392    fn default() -> Self {
1393        Self {
1394            enabled: false,
1395            frequency_hours: 24,
1396            retention_days: 30,
1397            backup_location: PathBuf::from("./backups"),
1398        }
1399    }
1400}
1401
1402impl Default for CollaborationManagerSettings {
1403    fn default() -> Self {
1404        Self {
1405            max_workspaces_per_user: 10,
1406            default_workspace_settings: WorkspaceSettings::default(),
1407            backup_settings: BackupSettings::default(),
1408        }
1409    }
1410}
1411
1412impl CollaborationManager {
1413    /// Create a new collaboration manager rooted at `storage_dir`, creating the
1414    /// directory on disk if it does not already exist.
1415    pub fn new(storage_dir: PathBuf, settings: CollaborationManagerSettings) -> Result<Self> {
1416        std::fs::create_dir_all(&storage_dir).map_err(|e| {
1417            OptimError::InvalidConfig(format!(
1418                "failed to create collaboration storage directory {}: {e}",
1419                storage_dir.display()
1420            ))
1421        })?;
1422
1423        Ok(Self {
1424            workspaces: HashMap::new(),
1425            storage_dir,
1426            settings,
1427        })
1428    }
1429
1430    /// Create and register a new workspace owned by `owner`, enforcing the
1431    /// configured per-user workspace limit.
1432    pub fn create_workspace(&mut self, name: &str, owner: UserInfo) -> Result<String> {
1433        let owned_count = self
1434            .workspaces
1435            .values()
1436            .filter(|ws| {
1437                ws.members
1438                    .iter()
1439                    .any(|m| m.role == MemberRole::Owner && m.user.email == owner.email)
1440            })
1441            .count();
1442
1443        if owned_count as u32 >= self.settings.max_workspaces_per_user {
1444            return Err(OptimError::InvalidConfig(format!(
1445                "user '{}' already owns the maximum of {} workspaces",
1446                owner.email, self.settings.max_workspaces_per_user
1447            )));
1448        }
1449
1450        let workspace = CollaborativeWorkspace::new(name, owner);
1451        let id = workspace.id.clone();
1452        self.workspaces.insert(id.clone(), workspace);
1453        Ok(id)
1454    }
1455
1456    /// Look up a workspace by ID.
1457    pub fn get_workspace(&self, id: &str) -> Option<&CollaborativeWorkspace> {
1458        self.workspaces.get(id)
1459    }
1460
1461    /// Look up a workspace by ID, mutably.
1462    pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut CollaborativeWorkspace> {
1463        self.workspaces.get_mut(id)
1464    }
1465
1466    /// Iterate over all registered workspaces.
1467    pub fn list_workspaces(&self) -> impl Iterator<Item = &CollaborativeWorkspace> {
1468        self.workspaces.values()
1469    }
1470
1471    /// Remove and return a workspace from the manager (does not touch disk).
1472    pub fn remove_workspace(&mut self, id: &str) -> Option<CollaborativeWorkspace> {
1473        self.workspaces.remove(id)
1474    }
1475
1476    /// Persist a registered workspace to `<storage_dir>/<id>.json`.
1477    pub fn save_workspace(&self, id: &str) -> Result<PathBuf> {
1478        let workspace = self
1479            .workspaces
1480            .get(id)
1481            .ok_or_else(|| OptimError::InvalidConfig(format!("workspace '{id}' not found")))?;
1482
1483        let path = self.storage_dir.join(format!("{id}.json"));
1484        let json = serde_json::to_string_pretty(workspace).map_err(|e| {
1485            OptimError::InvalidConfig(format!("failed to serialize workspace '{id}': {e}"))
1486        })?;
1487        std::fs::write(&path, json).map_err(|e| {
1488            OptimError::InvalidConfig(format!("failed to write {}: {e}", path.display()))
1489        })?;
1490
1491        Ok(path)
1492    }
1493
1494    /// Load a workspace previously saved with [`Self::save_workspace`] back
1495    /// into the manager, overwriting any in-memory copy with the same ID.
1496    pub fn load_workspace(&mut self, id: &str) -> Result<()> {
1497        let path = self.storage_dir.join(format!("{id}.json"));
1498        let json = std::fs::read_to_string(&path).map_err(|e| {
1499            OptimError::InvalidConfig(format!("failed to read {}: {e}", path.display()))
1500        })?;
1501        let workspace: CollaborativeWorkspace = serde_json::from_str(&json).map_err(|e| {
1502            OptimError::InvalidConfig(format!("failed to parse workspace '{id}': {e}"))
1503        })?;
1504
1505        self.workspaces.insert(id.to_string(), workspace);
1506        Ok(())
1507    }
1508
1509    /// Manager-wide settings.
1510    pub fn settings(&self) -> &CollaborationManagerSettings {
1511        &self.settings
1512    }
1513
1514    /// Storage directory backing [`Self::save_workspace`] / [`Self::load_workspace`].
1515    pub fn storage_dir(&self) -> &std::path::Path {
1516        &self.storage_dir
1517    }
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522    use super::*;
1523
1524    #[test]
1525    fn test_workspace_creation() {
1526        let owner = UserInfo {
1527            name: "Dr. Test".to_string(),
1528            email: "test@example.com".to_string(),
1529            institution: "Test University".to_string(),
1530            avatar_url: None,
1531            timezone: "UTC".to_string(),
1532            language: "en".to_string(),
1533            research_interests: vec!["machine learning".to_string()],
1534        };
1535
1536        let workspace = CollaborativeWorkspace::new("Test Workspace", owner);
1537
1538        assert_eq!(workspace.name, "Test Workspace");
1539        assert_eq!(workspace.members.len(), 1);
1540        assert_eq!(workspace.members[0].role, MemberRole::Owner);
1541        assert!(workspace.members[0]
1542            .permissions
1543            .contains(&Permission::ManageMembers));
1544    }
1545
1546    #[test]
1547    fn test_document_creation() {
1548        let owner = UserInfo {
1549            name: "Dr. Test".to_string(),
1550            email: "test@example.com".to_string(),
1551            institution: "Test University".to_string(),
1552            avatar_url: None,
1553            timezone: "UTC".to_string(),
1554            language: "en".to_string(),
1555            research_interests: vec![],
1556        };
1557
1558        let mut workspace = CollaborativeWorkspace::new("Test Workspace", owner);
1559        let owner_id = workspace.members[0].id.clone();
1560
1561        let doc_id = workspace
1562            .create_document("Test Document", DocumentType::Manuscript, &owner_id)
1563            .expect("create_document");
1564
1565        assert_eq!(workspace.documents.len(), 1);
1566        assert_eq!(workspace.documents[0].id, doc_id);
1567        assert_eq!(workspace.documents[0].name, "Test Document");
1568        assert_eq!(
1569            workspace.documents[0].document_type,
1570            DocumentType::Manuscript
1571        );
1572        assert_eq!(workspace.activity_log.len(), 1);
1573        assert_eq!(
1574            workspace.activity_log[0].activity_type,
1575            ActivityType::DocumentCreated
1576        );
1577    }
1578
1579    #[test]
1580    fn test_permission_checking() {
1581        let owner = UserInfo {
1582            name: "Dr. Test".to_string(),
1583            email: "test@example.com".to_string(),
1584            institution: "Test University".to_string(),
1585            avatar_url: None,
1586            timezone: "UTC".to_string(),
1587            language: "en".to_string(),
1588            research_interests: vec![],
1589        };
1590
1591        let workspace = CollaborativeWorkspace::new("Test Workspace", owner);
1592        let owner_id = &workspace.members[0].id;
1593
1594        assert!(workspace.has_permission(owner_id, &Permission::Read));
1595        assert!(workspace.has_permission(owner_id, &Permission::Write));
1596        assert!(workspace.has_permission(owner_id, &Permission::ManageMembers));
1597
1598        // Test non-existent user
1599        assert!(!workspace.has_permission("non-existent", &Permission::Read));
1600    }
1601
1602    fn test_owner(email: &str) -> UserInfo {
1603        UserInfo {
1604            name: "Dr. Test".to_string(),
1605            email: email.to_string(),
1606            institution: "Test University".to_string(),
1607            avatar_url: None,
1608            timezone: "UTC".to_string(),
1609            language: "en".to_string(),
1610            research_interests: vec![],
1611        }
1612    }
1613
1614    // Regression test for F24: `CollaborationManager` was exported with fully
1615    // private fields and zero methods (not even a constructor), making it
1616    // impossible for any caller to construct or use despite being part of the
1617    // public API surface.
1618    #[test]
1619    fn test_collaboration_manager_is_constructible_and_functional() {
1620        let dir = std::env::temp_dir().join(format!("optirs_collab_test_{}", uuid::Uuid::new_v4()));
1621
1622        let mut manager =
1623            CollaborationManager::new(dir.clone(), CollaborationManagerSettings::default())
1624                .expect("manager creation should succeed");
1625        assert!(dir.exists());
1626
1627        let workspace_id = manager
1628            .create_workspace("Test Workspace", test_owner("owner@example.com"))
1629            .expect("workspace creation should succeed");
1630
1631        assert!(manager.get_workspace(&workspace_id).is_some());
1632        assert_eq!(manager.list_workspaces().count(), 1);
1633
1634        let saved_path = manager
1635            .save_workspace(&workspace_id)
1636            .expect("save should succeed");
1637        assert!(saved_path.exists());
1638
1639        manager.remove_workspace(&workspace_id);
1640        assert!(manager.get_workspace(&workspace_id).is_none());
1641
1642        manager
1643            .load_workspace(&workspace_id)
1644            .expect("load should succeed");
1645        assert!(manager.get_workspace(&workspace_id).is_some());
1646        assert_eq!(
1647            manager.get_workspace(&workspace_id).unwrap().name,
1648            "Test Workspace"
1649        );
1650
1651        let _ = std::fs::remove_dir_all(&dir);
1652    }
1653
1654    #[test]
1655    fn test_collaboration_manager_enforces_workspace_limit() {
1656        let dir =
1657            std::env::temp_dir().join(format!("optirs_collab_limit_test_{}", uuid::Uuid::new_v4()));
1658        let settings = CollaborationManagerSettings {
1659            max_workspaces_per_user: 1,
1660            ..CollaborationManagerSettings::default()
1661        };
1662
1663        let mut manager =
1664            CollaborationManager::new(dir.clone(), settings).expect("manager creation");
1665
1666        manager
1667            .create_workspace("First", test_owner("limited@example.com"))
1668            .expect("first workspace should succeed");
1669
1670        let second = manager.create_workspace("Second", test_owner("limited@example.com"));
1671        assert!(second.is_err(), "second workspace should exceed the limit");
1672
1673        let _ = std::fs::remove_dir_all(&dir);
1674    }
1675}