agpm_cli/core/
error_helpers.rs

1//! Error handling helper functions and utilities
2//!
3//! This module provides common error handling patterns used throughout AGPM,
4//! reducing boilerplate and ensuring consistent error messages.
5
6use anyhow::{Context, Result};
7use std::path::Path;
8
9use crate::manifest::Manifest;
10use crate::markdown::MarkdownFile;
11
12/// Common file operations with consistent error handling
13pub trait FileOperations {
14    /// Read a file with appropriate error context
15    fn read_file_with_context(path: impl AsRef<Path>) -> Result<String> {
16        let path = path.as_ref();
17        std::fs::read_to_string(path)
18            .with_context(|| format!("Failed to read file: {}", path.display()))
19    }
20
21    /// Write to a file with appropriate error context
22    fn write_file_with_context(path: impl AsRef<Path>, content: impl AsRef<str>) -> Result<()> {
23        let path = path.as_ref();
24        std::fs::write(path, content.as_ref())
25            .with_context(|| format!("Failed to write file: {}", path.display()))
26    }
27
28    /// Create a directory with appropriate error context
29    fn create_dir_with_context(path: impl AsRef<Path>) -> Result<()> {
30        let path = path.as_ref();
31        std::fs::create_dir_all(path)
32            .with_context(|| format!("Failed to create directory: {}", path.display()))
33    }
34
35    /// Read a file as bytes with appropriate error context
36    fn read_bytes_with_context(path: impl AsRef<Path>) -> Result<Vec<u8>> {
37        let path = path.as_ref();
38        std::fs::read(path).with_context(|| format!("Failed to read file: {}", path.display()))
39    }
40
41    /// Write bytes to a file with appropriate error context
42    fn write_bytes_with_context(path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> Result<()> {
43        let path = path.as_ref();
44        std::fs::write(path, content.as_ref())
45            .with_context(|| format!("Failed to write file: {}", path.display()))
46    }
47
48    /// Copy a file with appropriate error context
49    fn copy_file_with_context(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<u64> {
50        let from = from.as_ref();
51        let to = to.as_ref();
52        std::fs::copy(from, to).with_context(|| {
53            format!("Failed to copy file from {} to {}", from.display(), to.display())
54        })
55    }
56
57    /// Remove a file with appropriate error context
58    fn remove_file_with_context(path: impl AsRef<Path>) -> Result<()> {
59        let path = path.as_ref();
60        std::fs::remove_file(path)
61            .with_context(|| format!("Failed to remove file: {}", path.display()))
62    }
63
64    /// Remove a directory recursively with appropriate error context
65    fn remove_dir_all_with_context(path: impl AsRef<Path>) -> Result<()> {
66        let path = path.as_ref();
67        std::fs::remove_dir_all(path)
68            .with_context(|| format!("Failed to remove directory: {}", path.display()))
69    }
70
71    /// Check if a path exists, returning error if checking fails
72    fn check_exists_with_context(path: impl AsRef<Path>) -> Result<bool> {
73        let path = path.as_ref();
74        path.try_exists()
75            .with_context(|| format!("Failed to check if path exists: {}", path.display()))
76    }
77}
78
79/// Implement `FileOperations` for a unit struct to enable trait usage
80pub struct FileOps;
81impl FileOperations for FileOps {}
82
83/// Common manifest operations with consistent error handling
84pub trait ManifestOperations {
85    /// Load a manifest with appropriate error context
86    fn load_manifest_with_context(path: impl AsRef<Path>) -> Result<Manifest> {
87        let path = path.as_ref();
88        Manifest::load(path)
89            .with_context(|| format!("Failed to parse manifest file: {}", path.display()))
90    }
91
92    /// Save a manifest with appropriate error context
93    fn save_manifest_with_context(manifest: &Manifest, path: impl AsRef<Path>) -> Result<()> {
94        let path = path.as_ref();
95        let content =
96            toml::to_string_pretty(manifest).with_context(|| "Failed to serialize manifest")?;
97        FileOps::write_file_with_context(path, content)
98    }
99}
100
101/// Implement `ManifestOperations` for a unit struct to enable trait usage
102pub struct ManifestOps;
103impl ManifestOperations for ManifestOps {}
104
105/// Common markdown operations with consistent error handling
106pub trait MarkdownOperations {
107    /// Parse markdown content with appropriate error context
108    fn parse_markdown_with_context(
109        content: impl AsRef<str>,
110        path: impl AsRef<Path>,
111    ) -> Result<MarkdownFile> {
112        let path = path.as_ref();
113        MarkdownFile::parse(content.as_ref())
114            .with_context(|| format!("Invalid markdown file: {}", path.display()))
115    }
116
117    /// Read and parse a markdown file with appropriate error context
118    fn read_markdown_with_context(path: impl AsRef<Path>) -> Result<MarkdownFile> {
119        let path = path.as_ref();
120        let content = FileOps::read_file_with_context(path)?;
121        Self::parse_markdown_with_context(content, path)
122    }
123}
124
125/// Implement `MarkdownOperations` for a unit struct to enable trait usage
126pub struct MarkdownOps;
127impl MarkdownOperations for MarkdownOps {}
128
129/// Common lockfile operations with consistent error handling
130pub trait LockfileOperations {
131    /// Load a lockfile with appropriate error context
132    fn load_lockfile_with_context(path: impl AsRef<Path>) -> Result<crate::lockfile::LockFile> {
133        let path = path.as_ref();
134        crate::lockfile::LockFile::load(path)
135            .with_context(|| format!("Failed to load lockfile: {}", path.display()))
136    }
137
138    /// Save a lockfile with appropriate error context
139    fn save_lockfile_with_context(
140        lockfile: &crate::lockfile::LockFile,
141        path: impl AsRef<Path>,
142    ) -> Result<()> {
143        let path = path.as_ref();
144        lockfile.save(path).with_context(|| format!("Failed to save lockfile: {}", path.display()))
145    }
146}
147
148/// Implement `LockfileOperations` for a unit struct to enable trait usage
149pub struct LockfileOps;
150impl LockfileOperations for LockfileOps {}
151
152/// Common JSON operations with consistent error handling
153pub trait JsonOperations {
154    /// Read and parse a JSON file with appropriate error context
155    fn read_json_with_context<T: serde::de::DeserializeOwned>(path: impl AsRef<Path>) -> Result<T> {
156        let path = path.as_ref();
157        let content = FileOps::read_file_with_context(path)?;
158        serde_json::from_str(&content)
159            .with_context(|| format!("Failed to parse JSON file: {}", path.display()))
160    }
161
162    /// Serialize and write a JSON file with appropriate error context
163    fn write_json_with_context<T: serde::Serialize>(
164        value: &T,
165        path: impl AsRef<Path>,
166    ) -> Result<()> {
167        let path = path.as_ref();
168        let content =
169            serde_json::to_string_pretty(value).with_context(|| "Failed to serialize to JSON")?;
170        FileOps::write_file_with_context(path, content)
171    }
172}
173
174/// Implement `JsonOperations` for a unit struct to enable trait usage
175pub struct JsonOps;
176impl JsonOperations for JsonOps {}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use std::fs;
182    use tempfile::TempDir;
183
184    #[test]
185    fn test_file_operations() {
186        let temp = TempDir::new().unwrap();
187        let file_path = temp.path().join("test.txt");
188
189        // Test write and read
190        FileOps::write_file_with_context(&file_path, "test content").unwrap();
191        let content = FileOps::read_file_with_context(&file_path).unwrap();
192        assert_eq!(content, "test content");
193
194        // Test exists check
195        assert!(FileOps::check_exists_with_context(&file_path).unwrap());
196
197        // Test remove
198        FileOps::remove_file_with_context(&file_path).unwrap();
199        assert!(!FileOps::check_exists_with_context(&file_path).unwrap());
200    }
201
202    #[test]
203    fn test_directory_operations() {
204        let temp = TempDir::new().unwrap();
205        let dir_path = temp.path().join("test_dir").join("nested");
206
207        // Test create nested directories
208        FileOps::create_dir_with_context(&dir_path).unwrap();
209        assert!(dir_path.exists());
210
211        // Test remove directory tree
212        let parent = temp.path().join("test_dir");
213        FileOps::remove_dir_all_with_context(&parent).unwrap();
214        assert!(!parent.exists());
215    }
216
217    #[test]
218    fn test_json_operations() {
219        let temp = TempDir::new().unwrap();
220        let json_path = temp.path().join("test.json");
221
222        #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
223        struct TestStruct {
224            field: String,
225            number: i32,
226        }
227
228        let test_data = TestStruct {
229            field: "test".to_string(),
230            number: 42,
231        };
232
233        // Test write and read
234        JsonOps::write_json_with_context(&test_data, &json_path).unwrap();
235        let loaded: TestStruct = JsonOps::read_json_with_context(&json_path).unwrap();
236        assert_eq!(loaded, test_data);
237    }
238
239    #[test]
240    fn test_read_bytes_with_context() {
241        let temp = TempDir::new().unwrap();
242        let file_path = temp.path().join("test_bytes.bin");
243        let test_bytes = b"binary\x00\x01\x02\x03data";
244
245        // Write bytes directly using std::fs
246        fs::write(&file_path, test_bytes).unwrap();
247
248        // Test reading bytes with context
249        let read_bytes = FileOps::read_bytes_with_context(&file_path).unwrap();
250        assert_eq!(read_bytes, test_bytes);
251
252        // Test error case - non-existent file
253        let missing_path = temp.path().join("missing.bin");
254        let result = FileOps::read_bytes_with_context(&missing_path);
255        assert!(result.is_err());
256        let error_msg = result.unwrap_err().to_string();
257        assert!(error_msg.contains("Failed to read file"));
258        assert!(error_msg.contains("missing.bin"));
259    }
260
261    #[test]
262    fn test_write_bytes_with_context() {
263        let temp = TempDir::new().unwrap();
264        let file_path = temp.path().join("test_write_bytes.bin");
265        let test_bytes = b"binary\x00\x01\x02\x03data";
266
267        // Test writing bytes with context
268        FileOps::write_bytes_with_context(&file_path, test_bytes).unwrap();
269
270        // Verify content
271        let read_bytes = fs::read(&file_path).unwrap();
272        assert_eq!(read_bytes, test_bytes);
273
274        // Test error case - invalid path (readonly parent)
275        let readonly_dir = temp.path().join("readonly");
276        fs::create_dir(&readonly_dir).unwrap();
277        let mut perms = fs::metadata(&readonly_dir).unwrap().permissions();
278        perms.set_readonly(true);
279        fs::set_permissions(&readonly_dir, perms).unwrap();
280
281        let readonly_file = readonly_dir.join("test.bin");
282        let result = FileOps::write_bytes_with_context(&readonly_file, test_bytes);
283
284        // Reset permissions for cleanup
285        #[cfg(unix)]
286        {
287            use std::os::unix::fs::PermissionsExt;
288            let perms = fs::Permissions::from_mode(0o755);
289            fs::set_permissions(&readonly_dir, perms).unwrap();
290        }
291        #[cfg(not(unix))]
292        {
293            let mut perms = fs::metadata(&readonly_dir).unwrap().permissions();
294            #[allow(clippy::permissions_set_readonly_false)]
295            perms.set_readonly(false);
296            fs::set_permissions(&readonly_dir, perms).unwrap();
297        }
298
299        // On some systems, writing to readonly directories might still work,
300        // so we just check that the function doesn't panic
301        if let Err(err) = result {
302            let error_msg = err.to_string();
303            assert!(error_msg.contains("Failed to write file"));
304        }
305    }
306
307    #[test]
308    fn test_copy_file_with_context() {
309        let temp = TempDir::new().unwrap();
310        let source_path = temp.path().join("source.txt");
311        let dest_path = temp.path().join("destination.txt");
312        let test_content = "file copy test content";
313
314        // Create source file
315        fs::write(&source_path, test_content).unwrap();
316
317        // Test copying file with context
318        let bytes_copied = FileOps::copy_file_with_context(&source_path, &dest_path).unwrap();
319        assert_eq!(bytes_copied, test_content.len() as u64);
320
321        // Verify destination content
322        let copied_content = fs::read_to_string(&dest_path).unwrap();
323        assert_eq!(copied_content, test_content);
324
325        // Test error case - source file doesn't exist
326        let missing_source = temp.path().join("missing_source.txt");
327        let another_dest = temp.path().join("another_dest.txt");
328        let result = FileOps::copy_file_with_context(&missing_source, &another_dest);
329        assert!(result.is_err());
330        let error_msg = result.unwrap_err().to_string();
331        assert!(error_msg.contains("Failed to copy file"));
332        assert!(error_msg.contains("missing_source.txt"));
333
334        // Test error case - destination directory doesn't exist
335        let nonexistent_dest = temp.path().join("nonexistent").join("dest.txt");
336        let result = FileOps::copy_file_with_context(&source_path, &nonexistent_dest);
337        assert!(result.is_err());
338        let error_msg = result.unwrap_err().to_string();
339        assert!(error_msg.contains("Failed to copy file"));
340    }
341
342    #[test]
343    fn test_manifest_operations_load() {
344        let temp = TempDir::new().unwrap();
345        let manifest_path = temp.path().join("agpm.toml");
346
347        // Create a valid manifest file
348        let manifest_content = r#"
349[sources]
350test = "https://github.com/test/test.git"
351
352[agents]
353test-agent = { source = "test", path = "agents/test.md", version = "v1.0.0" }
354"#;
355        fs::write(&manifest_path, manifest_content).unwrap();
356
357        // Test loading manifest with context
358        let manifest = ManifestOps::load_manifest_with_context(&manifest_path).unwrap();
359        assert!(manifest.sources.contains_key("test"));
360        assert!(manifest.agents.contains_key("test-agent"));
361
362        // Test error case - non-existent file
363        let missing_path = temp.path().join("missing.toml");
364        let result = ManifestOps::load_manifest_with_context(&missing_path);
365        assert!(result.is_err());
366        let error_msg = result.unwrap_err().to_string();
367        assert!(error_msg.contains("Failed to parse manifest file"));
368        assert!(error_msg.contains("missing.toml"));
369
370        // Test error case - invalid TOML
371        let invalid_manifest_path = temp.path().join("invalid.toml");
372        let invalid_content = "this is not valid toml [[[";
373        fs::write(&invalid_manifest_path, invalid_content).unwrap();
374        let result = ManifestOps::load_manifest_with_context(&invalid_manifest_path);
375        assert!(result.is_err());
376        let error_msg = result.unwrap_err().to_string();
377        assert!(error_msg.contains("Failed to parse manifest file"));
378    }
379
380    #[test]
381    fn test_manifest_operations_save() {
382        let temp = TempDir::new().unwrap();
383        let manifest_path = temp.path().join("test_save.toml");
384
385        // Create a manifest to save
386        let manifest = crate::manifest::Manifest::new();
387
388        // Test saving manifest with context
389        ManifestOps::save_manifest_with_context(&manifest, &manifest_path).unwrap();
390        assert!(manifest_path.exists());
391
392        // Verify the saved content can be loaded back
393        let loaded_manifest = ManifestOps::load_manifest_with_context(&manifest_path).unwrap();
394        assert_eq!(manifest.sources.len(), loaded_manifest.sources.len());
395        assert_eq!(manifest.agents.len(), loaded_manifest.agents.len());
396    }
397
398    #[test]
399    fn test_markdown_operations_parse() -> anyhow::Result<()> {
400        let temp = TempDir::new().unwrap();
401        let md_path = temp.path().join("test.md");
402
403        // Test parsing valid markdown with frontmatter
404        let markdown_content = r#"---
405title: "Test Agent"
406version: "1.0.0"
407---
408
409# Test Agent
410
411This is a test agent.
412"#;
413
414        let markdown =
415            MarkdownOps::parse_markdown_with_context(markdown_content, &md_path).unwrap();
416        assert_eq!(markdown.content.trim(), "# Test Agent\n\nThis is a test agent.");
417        assert!(markdown.get_title().is_some());
418        assert_eq!(markdown.get_title().unwrap(), "Test Agent");
419
420        // Test parsing markdown without frontmatter
421        let simple_content = "# Simple Agent\n\nThis is simple.";
422        let simple_markdown =
423            MarkdownOps::parse_markdown_with_context(simple_content, &md_path).unwrap();
424        assert_eq!(simple_markdown.content.trim(), "# Simple Agent\n\nThis is simple.");
425        // get_title() should extract title from the # heading
426        assert_eq!(simple_markdown.get_title().unwrap(), "Simple Agent");
427
428        // Test parsing markdown without frontmatter or headings
429        let plain_content = "This is plain content without headings.";
430        let plain_markdown =
431            MarkdownOps::parse_markdown_with_context(plain_content, &md_path).unwrap();
432        assert_eq!(plain_markdown.content.trim(), "This is plain content without headings.");
433        assert!(plain_markdown.get_title().is_none());
434
435        // Test case - invalid YAML frontmatter now succeeds but without metadata
436        let invalid_content = r#"---
437title: "Test Agent
438invalid yaml here
439---
440
441# Test Agent
442"#;
443        // This should now succeed (with a warning printed to stderr) but treat entire doc as content
444        let markdown = MarkdownOps::parse_markdown_with_context(invalid_content, &md_path)?;
445        // Invalid frontmatter means the entire document becomes content
446        assert!(markdown.metadata.is_none());
447        assert!(markdown.content.contains("---"));
448        assert!(markdown.content.contains("title: \"Test Agent"));
449        assert!(markdown.content.contains("# Test Agent"));
450        Ok(())
451    }
452
453    #[test]
454    fn test_markdown_operations_read() {
455        let temp = TempDir::new().unwrap();
456        let md_path = temp.path().join("test_read.md");
457
458        // Create a markdown file
459        let markdown_content = r#"---
460title: "Test Agent"
461version: "1.0.0"
462---
463
464# Test Agent
465
466This is a test agent for reading.
467"#;
468        fs::write(&md_path, markdown_content).unwrap();
469
470        // Test reading markdown with context
471        let markdown = MarkdownOps::read_markdown_with_context(&md_path).unwrap();
472        assert_eq!(markdown.get_title().unwrap(), "Test Agent");
473        assert!(markdown.content.contains("This is a test agent for reading"));
474
475        // Test error case - non-existent file
476        let missing_path = temp.path().join("missing.md");
477        let result = MarkdownOps::read_markdown_with_context(&missing_path);
478        assert!(result.is_err());
479        let error_msg = result.unwrap_err().to_string();
480        assert!(error_msg.contains("Failed to read file"));
481        assert!(error_msg.contains("missing.md"));
482    }
483
484    #[test]
485    fn test_lockfile_operations_load() {
486        let temp = TempDir::new().unwrap();
487        let lockfile_path = temp.path().join("agpm.lock");
488
489        // Test loading non-existent lockfile (should create new)
490        let lockfile = LockfileOps::load_lockfile_with_context(&lockfile_path).unwrap();
491        assert_eq!(lockfile.version, 1);
492        assert!(lockfile.sources.is_empty());
493
494        // Create a valid lockfile
495        let lockfile_content = r#"# Auto-generated lockfile - DO NOT EDIT
496version = 1
497
498[[sources]]
499name = "test"
500url = "https://github.com/test/test.git"
501commit = "abc123"
502fetched_at = "2024-01-01T00:00:00Z"
503"#;
504        fs::write(&lockfile_path, lockfile_content).unwrap();
505
506        // Test loading existing lockfile
507        let loaded_lockfile = LockfileOps::load_lockfile_with_context(&lockfile_path).unwrap();
508        assert_eq!(loaded_lockfile.version, 1);
509        assert!(!loaded_lockfile.sources.is_empty());
510
511        // Test error case - invalid lockfile format
512        let invalid_lockfile_path = temp.path().join("invalid.lock");
513        let invalid_content = "this is not valid toml [[[";
514        fs::write(&invalid_lockfile_path, invalid_content).unwrap();
515        let result = LockfileOps::load_lockfile_with_context(&invalid_lockfile_path);
516        assert!(result.is_err());
517        let error_msg = result.unwrap_err().to_string();
518        assert!(error_msg.contains("Failed to load lockfile"));
519        assert!(error_msg.contains("invalid.lock"));
520    }
521
522    #[test]
523    fn test_lockfile_operations_save() {
524        let temp = TempDir::new().unwrap();
525        let lockfile_path = temp.path().join("test_save.lock");
526
527        // Create a lockfile to save
528        let lockfile = crate::lockfile::LockFile::new();
529
530        // Test saving lockfile with context
531        LockfileOps::save_lockfile_with_context(&lockfile, &lockfile_path).unwrap();
532        assert!(lockfile_path.exists());
533
534        // Verify the saved content
535        let content = fs::read_to_string(&lockfile_path).unwrap();
536        assert!(content.contains("Auto-generated lockfile"));
537        assert!(content.contains("version = 1"));
538
539        // Verify it can be loaded back
540        let loaded_lockfile = LockfileOps::load_lockfile_with_context(&lockfile_path).unwrap();
541        assert_eq!(lockfile.version, loaded_lockfile.version);
542    }
543
544    #[test]
545    fn test_json_operations_error_cases() {
546        let temp = TempDir::new().unwrap();
547
548        // Test read error - non-existent file
549        let missing_json = temp.path().join("missing.json");
550        let result: Result<serde_json::Value> = JsonOps::read_json_with_context(&missing_json);
551        assert!(result.is_err());
552        let error_msg = result.unwrap_err().to_string();
553        assert!(error_msg.contains("Failed to read file"));
554        assert!(error_msg.contains("missing.json"));
555
556        // Test parse error - invalid JSON
557        let invalid_json_path = temp.path().join("invalid.json");
558        let invalid_json = r#"{ "field": "value" invalid json }"#;
559        fs::write(&invalid_json_path, invalid_json).unwrap();
560
561        let result: Result<serde_json::Value> = JsonOps::read_json_with_context(&invalid_json_path);
562        assert!(result.is_err());
563        let error_msg = result.unwrap_err().to_string();
564        assert!(error_msg.contains("Failed to parse JSON file"));
565        assert!(error_msg.contains("invalid.json"));
566
567        // Test write error with unserializable data
568        // Note: Most standard types are serializable, so this test verifies the
569        // error context path is working correctly by testing the success case
570        let json_path = temp.path().join("test_write_error.json");
571        let test_data = serde_json::json!({"test": "value"});
572
573        JsonOps::write_json_with_context(&test_data, &json_path).unwrap();
574        assert!(json_path.exists());
575
576        let loaded: serde_json::Value = JsonOps::read_json_with_context(&json_path).unwrap();
577        assert_eq!(loaded, test_data);
578    }
579
580    #[test]
581    fn test_file_operations_error_contexts() {
582        let temp = TempDir::new().unwrap();
583
584        // Test read_file_with_context error
585        let missing_file = temp.path().join("missing.txt");
586        let result = FileOps::read_file_with_context(&missing_file);
587        assert!(result.is_err());
588        let error_msg = result.unwrap_err().to_string();
589        assert!(error_msg.contains("Failed to read file"));
590        assert!(error_msg.contains("missing.txt"));
591
592        // Test write_file_with_context error
593        let readonly_dir = temp.path().join("readonly");
594        fs::create_dir(&readonly_dir).unwrap();
595        let mut perms = fs::metadata(&readonly_dir).unwrap().permissions();
596        perms.set_readonly(true);
597        fs::set_permissions(&readonly_dir, perms).unwrap();
598
599        let readonly_file = readonly_dir.join("test.txt");
600        let result = FileOps::write_file_with_context(&readonly_file, "test");
601
602        // Reset permissions for cleanup
603        #[cfg(unix)]
604        {
605            use std::os::unix::fs::PermissionsExt;
606            let perms = fs::Permissions::from_mode(0o755);
607            fs::set_permissions(&readonly_dir, perms).unwrap();
608        }
609        #[cfg(not(unix))]
610        {
611            let mut perms = fs::metadata(&readonly_dir).unwrap().permissions();
612            #[allow(clippy::permissions_set_readonly_false)]
613            perms.set_readonly(false);
614            fs::set_permissions(&readonly_dir, perms).unwrap();
615        }
616
617        if let Err(err) = result {
618            let error_msg = err.to_string();
619            assert!(error_msg.contains("Failed to write file"));
620        }
621
622        // Test create_dir_with_context error (trying to create in non-existent parent)
623        // This should work on most systems, so we test the success case
624        let nested_dir = temp.path().join("nested").join("deep");
625        FileOps::create_dir_with_context(&nested_dir).unwrap();
626        assert!(nested_dir.exists());
627
628        // Test remove_file_with_context error
629        let nonexistent_file = temp.path().join("nonexistent.txt");
630        let result = FileOps::remove_file_with_context(&nonexistent_file);
631        assert!(result.is_err());
632        let error_msg = result.unwrap_err().to_string();
633        assert!(error_msg.contains("Failed to remove file"));
634        assert!(error_msg.contains("nonexistent.txt"));
635
636        // Test check_exists_with_context success and error cases
637        let existing_file = temp.path().join("existing.txt");
638        fs::write(&existing_file, "test").unwrap();
639        assert!(FileOps::check_exists_with_context(&existing_file).unwrap());
640        assert!(!FileOps::check_exists_with_context(&nonexistent_file).unwrap());
641    }
642}