diff --git a/src/assertions/mod.rs b/src/assertions/mod.rs index caa7859..68f5ec5 100644 --- a/src/assertions/mod.rs +++ b/src/assertions/mod.rs @@ -28,12 +28,26 @@ pub struct AssertionChecker { impl AssertionChecker { /// Create a new assertion checker - pub fn new(log_file: &Path, work_dir: &Path, log_output_dir: Option<&Path>) -> Self { + pub fn new( + log_file: &Path, + work_dir: &Path, + log_output_dir: Option<&Path>, + output_subdir: Option<&str>, + ) -> Self { let log_data = Self::load_log_file(log_file); + let log_output_dir = log_output_dir.map(|p| { + let dir = if let Some(sub) = output_subdir { + p.join(sub) + } else { + p.to_path_buf() + }; + std::fs::create_dir_all(&dir).ok(); + dir + }); Self { log_data, work_dir: work_dir.to_path_buf(), - log_output_dir: log_output_dir.map(|p| p.to_path_buf()), + log_output_dir, } } diff --git a/src/assertions/tests.rs b/src/assertions/tests.rs index d7638e8..4a67dca 100644 --- a/src/assertions/tests.rs +++ b/src/assertions/tests.rs @@ -9,7 +9,7 @@ mod tests { fn create_checker(log_file: &str) -> AssertionChecker { let log_path = Path::new(log_file); let work_dir = tempfile::tempdir().unwrap(); - AssertionChecker::new(log_path, work_dir.path(), None) + AssertionChecker::new(log_path, work_dir.path(), None, None) } #[test] @@ -198,7 +198,7 @@ mod tests { let empty_log = work_dir.path().join("empty.log"); std::fs::write(&empty_log, "").unwrap(); - let checker = AssertionChecker::new(&empty_log, work_dir.path(), None); + let checker = AssertionChecker::new(&empty_log, work_dir.path(), None, None); let init = checker.init_message(); assert!(init.is_none(), "empty log should not have init message"); } @@ -254,7 +254,7 @@ mod tests { let nonexistent_log = work_dir.path().join("nonexistent.log"); // Should not panic, just return empty checker - let checker = AssertionChecker::new(&nonexistent_log, work_dir.path(), None); + let checker = AssertionChecker::new(&nonexistent_log, work_dir.path(), None, None); assert_eq!( checker.log_data.len(), 0, @@ -271,7 +271,7 @@ mod tests { let empty_log = work_dir.path().join("empty.log"); std::fs::write(&empty_log, "").unwrap(); - let checker = AssertionChecker::new(&empty_log, work_dir.path(), None); + let checker = AssertionChecker::new(&empty_log, work_dir.path(), None, None); let check = crate::models::CheckStep { name: "contains_patent_data".to_string(), @@ -308,4 +308,43 @@ mod tests { "file-contains should fail when string is absent" ); } + + #[test] + fn test_copy_to_output_creates_subdirectory() { + let work_dir = tempfile::tempdir().unwrap(); + let output_dir = tempfile::tempdir().unwrap(); + let test_file = work_dir.path().join("output.txt"); + std::fs::write(&test_file, "test content").unwrap(); + + let empty_log = work_dir.path().join("empty.log"); + std::fs::write(&empty_log, "").unwrap(); + + let checker = AssertionChecker::new( + &empty_log, + work_dir.path(), + Some(output_dir.path()), + Some("skill_test_20260404_050943"), + ); + + let check = crate::models::CheckStep { + name: "copy_file".to_string(), + command: CheckData { + command: "workspace-file".to_string(), + path: Some("output.txt".to_string()), + copy_to_output: Some(true), + ..Default::default() + }, + deny: false, + }; + + let result = checker.evaluate_check(&check); + assert!(result.is_ok(), "workspace-file with copy should pass"); + + let copied = output_dir + .path() + .join("skill_test_20260404_050943/output.txt"); + assert!(copied.exists(), "file should be copied to subdirectory"); + let content = std::fs::read_to_string(&copied).unwrap(); + assert_eq!(content, "test content", "copied content should match"); + } } diff --git a/src/runtime/executor.rs b/src/runtime/executor.rs index 8ab9e49..04e0ea7 100644 --- a/src/runtime/executor.rs +++ b/src/runtime/executor.rs @@ -155,8 +155,16 @@ impl TestExecutor { } // Run assertions - let checker = - AssertionChecker::new(&log_path, workspace.path(), self.log_output_dir.as_deref()); + let output_subdir = self.log_output_dir.as_ref().map(|_| { + let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S"); + format!("{}_{}_{timestamp}", desc.skill_name, desc.test_name) + }); + let checker = AssertionChecker::new( + &log_path, + workspace.path(), + self.log_output_dir.as_deref(), + output_subdir.as_deref(), + ); let check_results: Vec = desc .test .checks