foundry_mcp/
errors.rs

1//! Custom error types and error handling for the Project Manager MCP server
2
3use std::fmt;
4use std::path::{Path, PathBuf};
5
6/// Custom error types for different failure categories
7#[derive(Debug)]
8pub enum ProjectManagerError {
9    /// File system related errors
10    FileSystem {
11        operation: String,
12        path: PathBuf,
13        source: std::io::Error,
14    },
15
16    /// JSON serialization/deserialization errors
17    Serialization {
18        operation: String,
19        content: String,
20        source: serde_json::Error,
21    },
22
23    /// Validation errors for user input
24    Validation {
25        field: String,
26        value: String,
27        reason: String,
28    },
29
30    /// Resource not found errors
31    NotFound {
32        resource_type: String,
33        identifier: String,
34        context: Option<String>,
35    },
36
37    /// Resource already exists errors
38    AlreadyExists {
39        resource_type: String,
40        identifier: String,
41        context: Option<String>,
42    },
43
44    /// MCP protocol errors
45    McpProtocol {
46        operation: String,
47        details: String,
48        source: Option<Box<dyn std::error::Error + Send + Sync>>,
49    },
50
51    /// Configuration errors
52    Configuration {
53        setting: String,
54        value: String,
55        reason: String,
56    },
57
58    /// Permission errors
59    Permission {
60        operation: String,
61        path: PathBuf,
62        details: String,
63    },
64
65    /// Internal errors (should not happen in normal operation)
66    Internal {
67        operation: String,
68        details: String,
69        source: Option<Box<dyn std::error::Error + Send + Sync>>,
70    },
71}
72
73impl ProjectManagerError {
74    /// Create a file system error with context
75    pub fn file_system_error(operation: &str, path: &Path, source: std::io::Error) -> Self {
76        Self::FileSystem {
77            operation: operation.to_string(),
78            path: path.to_path_buf(),
79            source,
80        }
81    }
82
83    /// Create a serialization error with context
84    pub fn serialization_error(operation: &str, content: &str, source: serde_json::Error) -> Self {
85        Self::Serialization {
86            operation: operation.to_string(),
87            content: content.to_string(),
88            source,
89        }
90    }
91
92    /// Create a validation error
93    pub fn validation_error(field: &str, value: &str, reason: &str) -> Self {
94        Self::Validation {
95            field: field.to_string(),
96            value: value.to_string(),
97            reason: reason.to_string(),
98        }
99    }
100
101    /// Create a not found error
102    pub fn not_found(resource_type: &str, identifier: &str, context: Option<&str>) -> Self {
103        Self::NotFound {
104            resource_type: resource_type.to_string(),
105            identifier: identifier.to_string(),
106            context: context.map(|s| s.to_string()),
107        }
108    }
109
110    /// Create an already exists error
111    pub fn already_exists(resource_type: &str, identifier: &str, context: Option<&str>) -> Self {
112        Self::AlreadyExists {
113            resource_type: resource_type.to_string(),
114            identifier: identifier.to_string(),
115            context: context.map(|s| s.to_string()),
116        }
117    }
118
119    /// Create an MCP protocol error
120    pub fn mcp_protocol_error(
121        operation: &str,
122        details: &str,
123        source: Option<Box<dyn std::error::Error + Send + Sync>>,
124    ) -> Self {
125        Self::McpProtocol {
126            operation: operation.to_string(),
127            details: details.to_string(),
128            source,
129        }
130    }
131
132    /// Create a configuration error
133    pub fn configuration_error(setting: &str, value: &str, reason: &str) -> Self {
134        Self::Configuration {
135            setting: setting.to_string(),
136            value: value.to_string(),
137            reason: reason.to_string(),
138        }
139    }
140
141    /// Create a permission error
142    pub fn permission_error(operation: &str, path: &Path, details: &str) -> Self {
143        Self::Permission {
144            operation: operation.to_string(),
145            path: path.to_path_buf(),
146            details: details.to_string(),
147        }
148    }
149
150    /// Create an internal error
151    pub fn internal_error(
152        operation: &str,
153        details: &str,
154        source: Option<Box<dyn std::error::Error + Send + Sync>>,
155    ) -> Self {
156        Self::Internal {
157            operation: operation.to_string(),
158            details: details.to_string(),
159            source,
160        }
161    }
162
163    /// Get a user-friendly error message with context
164    pub fn user_message(&self) -> String {
165        match self {
166            Self::FileSystem {
167                operation, path, source
168            } => {
169                let base_msg = format!("Failed to {} at path '{}'", operation, path.display());
170                if let Some(suggestion) = self.get_suggestion() {
171                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
172                } else {
173                    format!("{}\nReason: {}", base_msg, source)
174                }
175            }
176            Self::Serialization { operation, .. } => {
177                let base_msg = format!("Failed to {} data", operation);
178                if let Some(suggestion) = self.get_suggestion() {
179                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
180                } else {
181                    base_msg
182                }
183            }
184            Self::Validation {
185                field,
186                value,
187                reason,
188            } => {
189                let base_msg = format!(
190                    "Invalid value '{}' for field '{}': {}",
191                    value, field, reason
192                );
193                if let Some(suggestion) = self.get_suggestion() {
194                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
195                } else {
196                    base_msg
197                }
198            }
199            Self::NotFound {
200                resource_type,
201                identifier,
202                context,
203            } => {
204                let base_msg = if let Some(ctx) = context {
205                    format!("{} '{}' not found in {}", resource_type, identifier, ctx)
206                } else {
207                    format!("{} '{}' not found", resource_type, identifier)
208                };
209                if let Some(suggestion) = self.get_suggestion() {
210                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
211                } else {
212                    base_msg
213                }
214            }
215            Self::AlreadyExists {
216                resource_type,
217                identifier,
218                context,
219            } => {
220                let base_msg = if let Some(ctx) = context {
221                    format!(
222                        "{} '{}' already exists in {}",
223                        resource_type, identifier, ctx
224                    )
225                } else {
226                    format!("{} '{}' already exists", resource_type, identifier)
227                };
228                if let Some(suggestion) = self.get_suggestion() {
229                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
230                } else {
231                    base_msg
232                }
233            }
234            Self::McpProtocol {
235                operation, details, ..
236            } => {
237                let base_msg = format!("MCP protocol error during {}: {}", operation, details);
238                if let Some(suggestion) = self.get_suggestion() {
239                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
240                } else {
241                    base_msg
242                }
243            }
244            Self::Configuration {
245                setting,
246                value,
247                reason,
248            } => {
249                let base_msg = format!(
250                    "Configuration error for '{}' (value: '{}'): {}",
251                    setting, value, reason
252                );
253                if let Some(suggestion) = self.get_suggestion() {
254                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
255                } else {
256                    base_msg
257                }
258            }
259            Self::Permission {
260                operation, path, details
261            } => {
262                let base_msg = format!(
263                    "Permission denied for {} at path '{}': {}",
264                    operation,
265                    path.display(),
266                    details
267                );
268                if let Some(suggestion) = self.get_suggestion() {
269                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
270                } else {
271                    base_msg
272                }
273            }
274            Self::Internal {
275                operation, details, ..
276            } => {
277                let base_msg = format!("Internal error during {}: {}", operation, details);
278                if let Some(suggestion) = self.get_suggestion() {
279                    format!("{}\n\nSuggestion: {}", base_msg, suggestion)
280                } else {
281                    base_msg
282                }
283            }
284        }
285    }
286
287    /// Get actionable suggestions for resolving the error
288    pub fn get_suggestion(&self) -> Option<String> {
289        match self {
290            Self::FileSystem { operation, path, source } => {
291                match source.kind() {
292                    std::io::ErrorKind::NotFound => {
293                        if operation.contains("create") {
294                            Some("Check that the parent directory exists and you have write permissions.".to_string())
295                        } else if operation.contains("read") || operation.contains("open") {
296                            Some(format!("Verify that the file '{}' exists and you have read permissions.", path.display()))
297                        } else {
298                            Some("Check that the file or directory exists and you have the necessary permissions.".to_string())
299                        }
300                    }
301                    std::io::ErrorKind::PermissionDenied => {
302                        Some("Check file/directory permissions or run with appropriate privileges.".to_string())
303                    }
304                    std::io::ErrorKind::AlreadyExists => {
305                        Some("Choose a different name or remove the existing file/directory first.".to_string())
306                    }
307                    std::io::ErrorKind::InvalidData => {
308                        Some("The file may be corrupted or in an unexpected format. Try recreating it.".to_string())
309                    }
310                    _ => Some("Check file system permissions and available disk space.".to_string())
311                }
312            }
313            Self::Serialization { operation, .. } => {
314                if operation.contains("serialize") {
315                    Some("Check that all required fields are properly set and contain valid data.".to_string())
316                } else if operation.contains("parse") || operation.contains("deserialize") {
317                    Some("The file may be corrupted or in an unexpected format. Try regenerating it or check for syntax errors.".to_string())
318                } else {
319                    Some("Verify that the data structure matches the expected format.".to_string())
320                }
321            }
322            Self::Validation { field, .. } => {
323                match field.as_str() {
324                    "project_name" => Some("Project names should be lowercase with hyphens or underscores, e.g., 'my-project' or 'my_project'.".to_string()),
325                    "spec_name" => Some("Specification names should be in snake_case format, e.g., 'user_authentication' or 'api_design'.".to_string()),
326                    "task_id" => Some("Task IDs should be unique strings. Use the generated UUID or a meaningful identifier.".to_string()),
327                    _ => Some("Check the documentation for the expected format and valid values for this field.".to_string())
328                }
329            }
330            Self::NotFound { resource_type, identifier, .. } => {
331                match resource_type.as_str() {
332                    "Project" => Some(format!("Create the project '{}' first using the setup_project tool.", identifier)),
333                    "Specification" => Some(format!("Create the specification '{}' first using the create_spec tool, or check if the ID is correct.", identifier)),
334                    "Task" => Some("Check that the task ID is correct or create the task first.".to_string()),
335                    _ => Some("Verify that the resource exists and the identifier is correct.".to_string())
336                }
337            }
338            Self::AlreadyExists { resource_type, identifier, .. } => {
339                match resource_type.as_str() {
340                    "Project" => Some(format!("Choose a different project name or use the existing project '{}'.", identifier)),
341                    "Specification" => Some(format!("Choose a different specification name or load the existing specification '{}'.", identifier)),
342                    "Task" => Some("Use a different task ID or update the existing task instead.".to_string()),
343                    _ => Some("Choose a different identifier or work with the existing resource.".to_string())
344                }
345            }
346            Self::McpProtocol { operation, .. } => {
347                match operation.as_str() {
348                    "tool_call" => Some("Check that all required parameters are provided and have correct types.".to_string()),
349                    "list_tools" => Some("Ensure the MCP server is properly initialized and running.".to_string()),
350                    _ => Some("Check the MCP client configuration and network connectivity.".to_string())
351                }
352            }
353            Self::Configuration { setting, .. } => {
354                match setting.as_str() {
355                    "home_directory" => Some("Set the HOME environment variable or run from a user directory.".to_string()),
356                    "base_directory" => Some("Ensure the base directory path is valid and accessible.".to_string()),
357                    _ => Some("Check the configuration documentation and verify all required settings.".to_string())
358                }
359            }
360            Self::Permission { operation, path, .. } => {
361                if operation.contains("write") || operation.contains("create") {
362                    Some(format!("Ensure you have write permissions for '{}' or change the target directory.", path.display()))
363                } else if operation.contains("read") {
364                    Some(format!("Ensure you have read permissions for '{}'.", path.display()))
365                } else {
366                    Some("Check that you have the necessary permissions for this operation.".to_string())
367                }
368            }
369            Self::Internal { .. } => {
370                Some("This is an unexpected error. Please report this issue with the error details.".to_string())
371            }
372        }
373    }
374
375    /// Get detailed troubleshooting steps
376    pub fn get_troubleshooting_steps(&self) -> Vec<String> {
377        let mut steps = Vec::new();
378        
379        match self {
380            Self::FileSystem { operation, path, source } => {
381                steps.push(format!("1. Check if path '{}' exists", path.display()));
382                if operation.contains("write") || operation.contains("create") {
383                    steps.push("2. Verify you have write permissions to the directory".to_string());
384                    steps.push("3. Check available disk space".to_string());
385                    steps.push("4. Ensure parent directories exist".to_string());
386                } else if operation.contains("read") {
387                    steps.push("2. Verify you have read permissions to the file".to_string());
388                    steps.push("3. Check that the file is not locked by another process".to_string());
389                }
390                steps.push(format!("5. System error: {}", source));
391            }
392            Self::NotFound { resource_type, identifier, context } => {
393                match resource_type.as_str() {
394                    "Project" => {
395                        steps.push("1. List available projects to verify the name".to_string());
396                        steps.push(format!("2. Create project '{}' using setup_project tool", identifier));
397                        steps.push("3. Check for typos in the project name".to_string());
398                    }
399                    "Specification" => {
400                        steps.push("1. List available specifications for the project".to_string());
401                        steps.push(format!("2. Create specification '{}' using create_spec tool", identifier));
402                        steps.push("3. Verify the specification ID format (YYYYMMDD_name)".to_string());
403                    }
404                    _ => {
405                        steps.push("1. Verify the resource identifier is correct".to_string());
406                        steps.push("2. Check if the resource exists in the expected location".to_string());
407                        if let Some(ctx) = context {
408                            steps.push(format!("3. Context: {}", ctx));
409                        }
410                    }
411                }
412            }
413            Self::Validation { field, value, reason } => {
414                steps.push(format!("1. Current value '{}' is invalid", value));
415                steps.push(format!("2. Reason: {}", reason));
416                if let Some(suggestion) = self.get_suggestion() {
417                    steps.push(format!("3. {}", suggestion));
418                }
419                steps.push(format!("4. Check the documentation for field '{}'", field));
420            }
421            _ => {
422                if let Some(suggestion) = self.get_suggestion() {
423                    steps.push(format!("1. {}", suggestion));
424                }
425                steps.push("2. Check the logs for more detailed error information".to_string());
426                steps.push("3. Verify your environment and configuration".to_string());
427            }
428        }
429        
430        steps
431    }
432
433    /// Get a detailed error message for developers
434    pub fn debug_message(&self) -> String {
435        match self {
436            Self::FileSystem {
437                operation,
438                path,
439                source,
440            } => {
441                format!("FileSystem error: {} at {:?} - {}", operation, path, source)
442            }
443            Self::Serialization {
444                operation,
445                content,
446                source,
447            } => {
448                format!(
449                    "Serialization error: {} - content: {} - {}",
450                    operation, content, source
451                )
452            }
453            Self::Validation {
454                field,
455                value,
456                reason,
457            } => {
458                format!(
459                    "Validation error: field '{}' with value '{}' - {}",
460                    field, value, reason
461                )
462            }
463            Self::NotFound {
464                resource_type,
465                identifier,
466                context,
467            } => {
468                format!(
469                    "NotFound error: {} '{}' in context '{:?}'",
470                    resource_type, identifier, context
471                )
472            }
473            Self::AlreadyExists {
474                resource_type,
475                identifier,
476                context,
477            } => {
478                format!(
479                    "AlreadyExists error: {} '{}' in context '{:?}'",
480                    resource_type, identifier, context
481                )
482            }
483            Self::McpProtocol {
484                operation,
485                details,
486                source,
487            } => {
488                format!(
489                    "MCP Protocol error: {} - {} - source: {:?}",
490                    operation, details, source
491                )
492            }
493            Self::Configuration {
494                setting,
495                value,
496                reason,
497            } => {
498                format!(
499                    "Configuration error: setting '{}' with value '{}' - {}",
500                    setting, value, reason
501                )
502            }
503            Self::Permission {
504                operation,
505                path,
506                details,
507            } => {
508                format!(
509                    "Permission error: {} at {:?} - {}",
510                    operation, path, details
511                )
512            }
513            Self::Internal {
514                operation,
515                details,
516                source,
517            } => {
518                format!(
519                    "Internal error: {} - {} - source: {:?}",
520                    operation, details, source
521                )
522            }
523        }
524    }
525
526    /// Get the error category for logging and monitoring
527    pub fn category(&self) -> &'static str {
528        match self {
529            Self::FileSystem { .. } => "filesystem",
530            Self::Serialization { .. } => "serialization",
531            Self::Validation { .. } => "validation",
532            Self::NotFound { .. } => "not_found",
533            Self::AlreadyExists { .. } => "already_exists",
534            Self::McpProtocol { .. } => "mcp_protocol",
535            Self::Configuration { .. } => "configuration",
536            Self::Permission { .. } => "permission",
537            Self::Internal { .. } => "internal",
538        }
539    }
540
541    /// Check if this is a user-facing error (vs internal error)
542    pub fn is_user_facing(&self) -> bool {
543        !matches!(self, Self::Internal { .. })
544    }
545}
546
547impl fmt::Display for ProjectManagerError {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        write!(f, "{}", self.user_message())
550    }
551}
552
553impl std::error::Error for ProjectManagerError {
554    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
555        match self {
556            Self::FileSystem { source, .. } => Some(source),
557            Self::Serialization { source, .. } => Some(source),
558            Self::McpProtocol { source, .. } => source
559                .as_ref()
560                .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)),
561            Self::Internal { source, .. } => source
562                .as_ref()
563                .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)),
564            _ => None,
565        }
566    }
567}
568
569// Error conversion traits for common error types
570impl From<std::io::Error> for ProjectManagerError {
571    fn from(err: std::io::Error) -> Self {
572        Self::FileSystem {
573            operation: "perform I/O operation".to_string(),
574            path: PathBuf::new(),
575            source: err,
576        }
577    }
578}
579
580impl From<serde_json::Error> for ProjectManagerError {
581    fn from(err: serde_json::Error) -> Self {
582        Self::Serialization {
583            operation: "parse JSON".to_string(),
584            content: "unknown".to_string(),
585            source: err,
586        }
587    }
588}
589
590// Result type alias for the project manager
591pub type Result<T> = std::result::Result<T, ProjectManagerError>;
592
593// Helper functions for common error patterns
594pub mod helpers {
595    use super::*;
596
597    /// Create a validation error for invalid project names
598    pub fn invalid_project_name(name: &str) -> ProjectManagerError {
599        ProjectManagerError::validation_error(
600            "project_name",
601            name,
602            "Project names cannot contain special characters or spaces",
603        )
604    }
605
606    /// Create a validation error for invalid spec names
607    pub fn invalid_spec_name(name: &str) -> ProjectManagerError {
608        ProjectManagerError::validation_error(
609            "spec_name",
610            name,
611            "Spec names must be in snake_case format (lowercase with underscores)",
612        )
613    }
614
615    /// Create a validation error
616    pub fn validation_error(field: &str, value: &str, reason: &str) -> ProjectManagerError {
617        ProjectManagerError::validation_error(field, value, reason)
618    }
619
620    /// Create a not found error for projects
621    pub fn project_not_found(name: &str) -> ProjectManagerError {
622        ProjectManagerError::not_found("Project", name, None)
623    }
624
625    /// Create a not found error for specifications
626    pub fn spec_not_found(spec_id: &str, project_name: &str) -> ProjectManagerError {
627        ProjectManagerError::not_found("Specification", spec_id, Some(project_name))
628    }
629
630    /// Create an already exists error for projects
631    pub fn project_already_exists(name: &str) -> ProjectManagerError {
632        ProjectManagerError::already_exists("Project", name, None)
633    }
634
635    /// Create an already exists error for specifications
636    pub fn spec_already_exists(spec_id: &str, project_name: &str) -> ProjectManagerError {
637        ProjectManagerError::already_exists("Specification", spec_id, Some(project_name))
638    }
639
640    /// Create a file system error with operation context
641    pub fn file_system_error(
642        operation: &str,
643        path: &Path,
644        source: std::io::Error,
645    ) -> ProjectManagerError {
646        ProjectManagerError::file_system_error(operation, path, source)
647    }
648
649    /// Create a serialization error with operation context
650    pub fn serialization_error(
651        operation: &str,
652        content: &str,
653        source: serde_json::Error,
654    ) -> ProjectManagerError {
655        ProjectManagerError::serialization_error(operation, content, source)
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662    use std::path::PathBuf;
663
664    #[test]
665    fn test_file_system_error_creation() {
666        let path = PathBuf::from("/test/path");
667        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
668        let error = ProjectManagerError::file_system_error("read file", &path, io_error);
669        
670        match error {
671            ProjectManagerError::FileSystem { operation, path: error_path, .. } => {
672                assert_eq!(operation, "read file");
673                assert_eq!(error_path, path);
674            }
675            _ => panic!("Expected FileSystem error"),
676        }
677    }
678
679    #[test]
680    fn test_serialization_error_creation() {
681        // Create a real JSON error by trying to parse invalid JSON
682        let json_result: std::result::Result<serde_json::Value, serde_json::Error> = serde_json::from_str("invalid json");
683        let json_error = json_result.unwrap_err();
684        let error = ProjectManagerError::serialization_error("parse JSON", "invalid json", json_error);
685        
686        match error {
687            ProjectManagerError::Serialization { operation, content, .. } => {
688                assert_eq!(operation, "parse JSON");
689                assert_eq!(content, "invalid json");
690            }
691            _ => panic!("Expected Serialization error"),
692        }
693    }
694
695    #[test]
696    fn test_validation_error_creation() {
697        let error = ProjectManagerError::validation_error("project_name", "invalid name", "contains spaces");
698        
699        match error {
700            ProjectManagerError::Validation { field, value, reason } => {
701                assert_eq!(field, "project_name");
702                assert_eq!(value, "invalid name");
703                assert_eq!(reason, "contains spaces");
704            }
705            _ => panic!("Expected Validation error"),
706        }
707    }
708
709    #[test]
710    fn test_not_found_error_creation() {
711        let error = ProjectManagerError::not_found("Project", "test-project", Some("workspace"));
712        
713        match error {
714            ProjectManagerError::NotFound { resource_type, identifier, context } => {
715                assert_eq!(resource_type, "Project");
716                assert_eq!(identifier, "test-project");
717                assert_eq!(context, Some("workspace".to_string()));
718            }
719            _ => panic!("Expected NotFound error"),
720        }
721    }
722
723    #[test]
724    fn test_already_exists_error_creation() {
725        let error = ProjectManagerError::already_exists("Project", "duplicate-project", Some("workspace"));
726        
727        match error {
728            ProjectManagerError::AlreadyExists { resource_type, identifier, context } => {
729                assert_eq!(resource_type, "Project");
730                assert_eq!(identifier, "duplicate-project");
731                assert_eq!(context, Some("workspace".to_string()));
732            }
733            _ => panic!("Expected AlreadyExists error"),
734        }
735    }
736
737    #[test]
738    fn test_user_message_formatting() {
739        let path = PathBuf::from("/test/file.txt");
740        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
741        let error = ProjectManagerError::file_system_error("read file", &path, io_error);
742        
743        let message = error.user_message();
744        assert!(message.contains("Failed to read file"));
745        assert!(message.contains("/test/file.txt"));
746    }
747
748    #[test]
749    fn test_user_message_validation_error() {
750        let error = ProjectManagerError::validation_error("project_name", "bad name", "contains invalid characters");
751        let message = error.user_message();
752        
753        assert!(message.contains("Invalid value 'bad name'"));
754        assert!(message.contains("for field 'project_name'"));
755        assert!(message.contains("contains invalid characters"));
756    }
757
758    #[test]
759    fn test_error_category() {
760        let fs_error = ProjectManagerError::file_system_error("test", &PathBuf::new(), 
761            std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
762        assert_eq!(fs_error.category(), "filesystem");
763        
764        let validation_error = ProjectManagerError::validation_error("field", "value", "reason");
765        assert_eq!(validation_error.category(), "validation");
766    }
767
768    #[test]
769    fn test_is_user_facing() {
770        let validation_error = ProjectManagerError::validation_error("field", "value", "reason");
771        assert!(validation_error.is_user_facing());
772        
773        let internal_error = ProjectManagerError::internal_error("op", "details", None);
774        assert!(!internal_error.is_user_facing());
775    }
776
777    #[test]
778    fn test_from_serde_json_error() {
779        let json_result: std::result::Result<serde_json::Value, serde_json::Error> = serde_json::from_str("invalid json");
780        let json_error = json_result.unwrap_err();
781        let pm_error: ProjectManagerError = json_error.into();
782        
783        match pm_error {
784            ProjectManagerError::Serialization { operation, content, .. } => {
785                assert_eq!(operation, "parse JSON");
786                assert_eq!(content, "unknown");
787            }
788            _ => panic!("Expected Serialization error from serde_json::Error conversion"),
789        }
790    }
791
792    #[test]
793    fn test_helpers_invalid_project_name() {
794        let error = helpers::invalid_project_name("bad project name");
795        
796        match error {
797            ProjectManagerError::Validation { field, value, reason } => {
798                assert_eq!(field, "project_name");
799                assert_eq!(value, "bad project name");
800                assert!(reason.contains("special characters"));
801            }
802            _ => panic!("Expected Validation error"),
803        }
804    }
805
806    #[test]
807    fn test_helpers_serialization_error() {
808        let json_result: std::result::Result<serde_json::Value, serde_json::Error> = serde_json::from_str("invalid json");
809        let json_error = json_result.unwrap_err();
810        let error = helpers::serialization_error("deserialize", "bad json", json_error);
811        
812        match error {
813            ProjectManagerError::Serialization { operation, content, .. } => {
814                assert_eq!(operation, "deserialize");
815                assert_eq!(content, "bad json");
816            }
817            _ => panic!("Expected Serialization error"),
818        }
819    }
820
821    #[test]
822    fn test_helpers_project_already_exists() {
823        let error = helpers::project_already_exists("duplicate-project");
824        
825        match error {
826            ProjectManagerError::AlreadyExists { resource_type, identifier, context } => {
827                assert_eq!(resource_type, "Project");
828                assert_eq!(identifier, "duplicate-project");
829                assert_eq!(context, None);
830            }
831            _ => panic!("Expected AlreadyExists error"),
832        }
833    }
834}