1use chrono::{DateTime, Local};
16use std::path::{Path, PathBuf};
17
18use crate::common::truncate_text;
19use crate::workspace::Workspace;
20
21#[derive(Debug, Clone)]
23pub struct ParsingTraceStep {
24 pub step_number: usize,
26 pub description: String,
28 pub input: Option<String>,
30 pub result: Option<String>,
32 pub success: bool,
34 pub details: String,
36}
37
38impl ParsingTraceStep {
39 pub fn new(step_number: usize, description: &str) -> Self {
41 Self {
42 step_number,
43 description: description.to_string(),
44 input: None,
45 result: None,
46 success: false,
47 details: String::new(),
48 }
49 }
50
51 pub fn with_input(mut self, input: &str) -> Self {
53 const MAX_INPUT_SIZE: usize = 10_000;
55 self.input = if input.len() > MAX_INPUT_SIZE {
56 Some(format!(
57 "{}\n\n[... input truncated {} bytes ...]",
58 &input[..MAX_INPUT_SIZE / 2],
59 input.len() - MAX_INPUT_SIZE
60 ))
61 } else {
62 Some(input.to_string())
63 };
64 self
65 }
66
67 pub fn with_result(mut self, result: &str) -> Self {
69 const MAX_RESULT_SIZE: usize = 10_000;
71 self.result = if result.len() > MAX_RESULT_SIZE {
72 Some(format!(
73 "{}\n\n[... result truncated {} bytes ...]",
74 &result[..MAX_RESULT_SIZE / 2],
75 result.len() - MAX_RESULT_SIZE
76 ))
77 } else {
78 Some(result.to_string())
79 };
80 self
81 }
82
83 pub const fn with_success(mut self, success: bool) -> Self {
85 self.success = success;
86 self
87 }
88
89 pub fn with_details(mut self, details: &str) -> Self {
91 self.details = details.to_string();
92 self
93 }
94}
95
96#[derive(Debug, Clone)]
106pub struct ParsingTraceLog {
107 pub attempt_number: usize,
109 pub agent: String,
111 pub strategy: String,
113 pub raw_output: Option<String>,
115 pub steps: Vec<ParsingTraceStep>,
117 pub final_message: Option<String>,
119 pub timestamp: DateTime<Local>,
121}
122
123impl ParsingTraceLog {
124 pub fn new(attempt_number: usize, agent: &str, strategy: &str) -> Self {
126 Self {
127 attempt_number,
128 agent: agent.to_string(),
129 strategy: strategy.to_string(),
130 raw_output: None,
131 steps: Vec::new(),
132 final_message: None,
133 timestamp: Local::now(),
134 }
135 }
136
137 pub fn set_raw_output(&mut self, output: &str) {
139 const MAX_OUTPUT_SIZE: usize = 50_000;
140 self.raw_output = if output.len() > MAX_OUTPUT_SIZE {
141 Some(format!(
142 "{}\n\n[... raw output truncated {} bytes ...]\n\n{}",
143 &output[..MAX_OUTPUT_SIZE / 2],
144 output.len() - MAX_OUTPUT_SIZE,
145 &output[output.len() - MAX_OUTPUT_SIZE / 2..]
146 ))
147 } else {
148 Some(output.to_string())
149 };
150 }
151
152 pub fn add_step(&mut self, step: ParsingTraceStep) {
154 self.steps.push(step);
155 }
156
157 pub fn set_final_message(&mut self, message: &str) {
159 self.final_message = Some(message.to_string());
160 }
161
162 pub fn write_to_workspace(
176 &self,
177 log_dir: &Path,
178 workspace: &dyn Workspace,
179 ) -> std::io::Result<PathBuf> {
180 let trace_path = log_dir.join(format!(
181 "attempt_{:03}_parsing_trace.log",
182 self.attempt_number
183 ));
184
185 let mut content = String::new();
187 Self::write_header_to_string(&mut content, self);
188 Self::write_raw_output_to_string(&mut content, self);
189 Self::write_parsing_steps_to_string(&mut content, self);
190 Self::write_final_message_to_string(&mut content, self);
191 Self::write_footer_to_string(&mut content);
192
193 workspace.create_dir_all(log_dir)?;
195 workspace.write(&trace_path, &content)?;
196
197 Ok(trace_path)
198 }
199
200 fn write_header_to_string(s: &mut String, trace: &Self) {
202 use std::fmt::Write;
203 let _ = writeln!(
204 s,
205 "================================================================================"
206 );
207 let _ = writeln!(
208 s,
209 "PARSING TRACE LOG - Attempt #{:03}",
210 trace.attempt_number
211 );
212 let _ = writeln!(
213 s,
214 "================================================================================"
215 );
216 let _ = writeln!(s);
217 let _ = writeln!(s, "Agent: {}", trace.agent);
218 let _ = writeln!(s, "Strategy: {}", trace.strategy);
219 let _ = writeln!(
220 s,
221 "Timestamp: {}",
222 trace.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
223 );
224 let _ = writeln!(s);
225 }
226
227 fn write_raw_output_to_string(s: &mut String, trace: &Self) {
228 use std::fmt::Write;
229 let _ = writeln!(
230 s,
231 "--------------------------------------------------------------------------------"
232 );
233 let _ = writeln!(s, "RAW AGENT OUTPUT");
234 let _ = writeln!(
235 s,
236 "--------------------------------------------------------------------------------"
237 );
238 let _ = writeln!(s);
239 match &trace.raw_output {
240 Some(output) => {
241 let _ = writeln!(s, "{output}");
242 }
243 None => {
244 let _ = writeln!(s, "[No raw output captured]");
245 }
246 }
247 let _ = writeln!(s);
248 }
249
250 fn write_parsing_steps_to_string(s: &mut String, trace: &Self) {
251 use std::fmt::Write;
252 let _ = writeln!(
253 s,
254 "--------------------------------------------------------------------------------"
255 );
256 let _ = writeln!(s, "PARSING STEPS");
257 let _ = writeln!(
258 s,
259 "--------------------------------------------------------------------------------"
260 );
261 let _ = writeln!(s);
262
263 if trace.steps.is_empty() {
264 let _ = writeln!(s, "[No parsing steps recorded]");
265 } else {
266 for step in &trace.steps {
267 let status = if step.success {
268 "✓ SUCCESS"
269 } else {
270 "✗ FAILED"
271 };
272 let _ = writeln!(s, "{}. {} [{}]", step.step_number, step.description, status);
273 let _ = writeln!(s);
274
275 if let Some(input) = &step.input {
276 let _ = writeln!(s, " INPUT:");
277 for line in input.lines() {
278 let _ = writeln!(s, " {line}");
279 }
280 let _ = writeln!(s);
281 }
282
283 if let Some(result) = &step.result {
284 let _ = writeln!(s, " RESULT:");
285 for line in result.lines() {
286 let _ = writeln!(s, " {line}");
287 }
288 let _ = writeln!(s);
289 }
290
291 if !step.details.is_empty() {
292 let _ = writeln!(s, " DETAILS: {}", step.details);
293 let _ = writeln!(s);
294 }
295 }
296 }
297 let _ = writeln!(s);
298 }
299
300 fn write_final_message_to_string(s: &mut String, trace: &Self) {
301 use std::fmt::Write;
302 let _ = writeln!(
303 s,
304 "--------------------------------------------------------------------------------"
305 );
306 let _ = writeln!(s, "FINAL EXTRACTED MESSAGE");
307 let _ = writeln!(
308 s,
309 "--------------------------------------------------------------------------------"
310 );
311 let _ = writeln!(s);
312 match &trace.final_message {
313 Some(message) => {
314 let _ = writeln!(s, "{message}");
315 }
316 None => {
317 let _ = writeln!(s, "[No message extracted]");
318 }
319 }
320 let _ = writeln!(s);
321 }
322
323 fn write_footer_to_string(s: &mut String) {
324 use std::fmt::Write;
325 let _ = writeln!(
326 s,
327 "================================================================================"
328 );
329 }
330}
331
332#[derive(Debug, Clone)]
334pub struct ExtractionAttempt {
335 pub method: &'static str,
337 pub success: bool,
339 pub detail: String,
341}
342
343impl ExtractionAttempt {
344 pub const fn success(method: &'static str, detail: String) -> Self {
346 Self {
347 method,
348 success: true,
349 detail,
350 }
351 }
352
353 pub const fn failure(method: &'static str, detail: String) -> Self {
355 Self {
356 method,
357 success: false,
358 detail,
359 }
360 }
361}
362
363#[derive(Debug, Clone)]
365pub struct ValidationCheck {
366 pub name: &'static str,
368 pub passed: bool,
370 pub error: Option<String>,
372}
373
374impl ValidationCheck {
375 #[cfg(test)]
377 pub const fn pass(name: &'static str) -> Self {
378 Self {
379 name,
380 passed: true,
381 error: None,
382 }
383 }
384
385 #[cfg(test)]
387 pub const fn fail(name: &'static str, error: String) -> Self {
388 Self {
389 name,
390 passed: false,
391 error: Some(error),
392 }
393 }
394}
395
396#[derive(Debug, Clone)]
398pub enum AttemptOutcome {
399 Success(String),
401 XsdValidationFailed(String),
403 ExtractionFailed(String),
405}
406
407impl std::fmt::Display for AttemptOutcome {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 match self {
410 Self::Success(msg) => write!(f, "SUCCESS: {}", preview_message(msg)),
411 Self::XsdValidationFailed(err) => write!(f, "XSD_VALIDATION_FAILED: {err}"),
412 Self::ExtractionFailed(err) => write!(f, "EXTRACTION_FAILED: {err}"),
413 }
414 }
415}
416
417fn preview_message(msg: &str) -> String {
421 let first_line = msg.lines().next().unwrap_or(msg);
422 truncate_text(first_line, 63)
424}
425
426#[derive(Debug, Clone)]
431pub struct CommitAttemptLog {
432 pub attempt_number: usize,
434 pub agent: String,
436 pub strategy: String,
438 pub timestamp: DateTime<Local>,
440 pub prompt_size_bytes: usize,
442 pub diff_size_bytes: usize,
444 pub diff_was_truncated: bool,
446 pub raw_output: Option<String>,
448 pub extraction_attempts: Vec<ExtractionAttempt>,
450 pub validation_checks: Vec<ValidationCheck>,
452 pub outcome: Option<AttemptOutcome>,
454}
455
456impl CommitAttemptLog {
457 pub fn new(attempt_number: usize, agent: &str, strategy: &str) -> Self {
459 Self {
460 attempt_number,
461 agent: agent.to_string(),
462 strategy: strategy.to_string(),
463 timestamp: Local::now(),
464 prompt_size_bytes: 0,
465 diff_size_bytes: 0,
466 diff_was_truncated: false,
467 raw_output: None,
468 extraction_attempts: Vec::new(),
469 validation_checks: Vec::new(),
470 outcome: None,
471 }
472 }
473
474 pub const fn set_prompt_size(&mut self, size: usize) {
476 self.prompt_size_bytes = size;
477 }
478
479 pub const fn set_diff_info(&mut self, size: usize, was_truncated: bool) {
481 self.diff_size_bytes = size;
482 self.diff_was_truncated = was_truncated;
483 }
484
485 pub fn set_raw_output(&mut self, output: &str) {
489 const MAX_OUTPUT_SIZE: usize = 50_000;
490 if output.len() > MAX_OUTPUT_SIZE {
491 self.raw_output = Some(format!(
492 "{}\n\n[... truncated {} bytes ...]\n\n{}",
493 &output[..MAX_OUTPUT_SIZE / 2],
494 output.len() - MAX_OUTPUT_SIZE,
495 &output[output.len() - MAX_OUTPUT_SIZE / 2..]
496 ));
497 } else {
498 self.raw_output = Some(output.to_string());
499 }
500 }
501
502 pub fn add_extraction_attempt(&mut self, attempt: ExtractionAttempt) {
504 self.extraction_attempts.push(attempt);
505 }
506
507 #[cfg(test)]
509 pub fn set_validation_checks(&mut self, checks: Vec<ValidationCheck>) {
510 self.validation_checks = checks;
511 }
512
513 pub fn set_outcome(&mut self, outcome: AttemptOutcome) {
515 self.outcome = Some(outcome);
516 }
517
518 pub fn write_to_workspace(
532 &self,
533 log_dir: &Path,
534 workspace: &dyn Workspace,
535 ) -> std::io::Result<PathBuf> {
536 workspace.create_dir_all(log_dir)?;
538
539 let filename = format!(
541 "attempt_{:03}_{}_{}_{}.log",
542 self.attempt_number,
543 sanitize_agent_name(&self.agent),
544 self.strategy.replace(' ', "_"),
545 self.timestamp.format("%Y%m%dT%H%M%S")
546 );
547 let log_path = log_dir.join(filename);
548
549 let mut content = String::new();
551 self.write_header_to_string(&mut content);
552 self.write_context_to_string(&mut content);
553 self.write_raw_output_to_string(&mut content);
554 self.write_extraction_attempts_to_string(&mut content);
555 self.write_validation_to_string(&mut content);
556 self.write_outcome_to_string(&mut content);
557
558 workspace.write(&log_path, &content)?;
560 Ok(log_path)
561 }
562
563 fn write_header_to_string(&self, s: &mut String) {
565 use std::fmt::Write;
566 let _ = writeln!(
567 s,
568 "========================================================================"
569 );
570 let _ = writeln!(s, "COMMIT GENERATION ATTEMPT LOG");
571 let _ = writeln!(
572 s,
573 "========================================================================"
574 );
575 let _ = writeln!(s);
576 let _ = writeln!(s, "Attempt: #{}", self.attempt_number);
577 let _ = writeln!(s, "Agent: {}", self.agent);
578 let _ = writeln!(s, "Strategy: {}", self.strategy);
579 let _ = writeln!(
580 s,
581 "Timestamp: {}",
582 self.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
583 );
584 let _ = writeln!(s);
585 }
586
587 fn write_context_to_string(&self, s: &mut String) {
588 use std::fmt::Write;
589 let _ = writeln!(
590 s,
591 "------------------------------------------------------------------------"
592 );
593 let _ = writeln!(s, "CONTEXT");
594 let _ = writeln!(
595 s,
596 "------------------------------------------------------------------------"
597 );
598 let _ = writeln!(s);
599 let _ = writeln!(
600 s,
601 "Prompt size: {} bytes ({} KB)",
602 self.prompt_size_bytes,
603 self.prompt_size_bytes / 1024
604 );
605 let _ = writeln!(
606 s,
607 "Diff size: {} bytes ({} KB)",
608 self.diff_size_bytes,
609 self.diff_size_bytes / 1024
610 );
611 let _ = writeln!(
612 s,
613 "Diff truncated: {}",
614 if self.diff_was_truncated { "YES" } else { "NO" }
615 );
616 let _ = writeln!(s);
617 }
618
619 fn write_raw_output_to_string(&self, s: &mut String) {
620 use std::fmt::Write;
621 let _ = writeln!(
622 s,
623 "------------------------------------------------------------------------"
624 );
625 let _ = writeln!(s, "RAW AGENT OUTPUT");
626 let _ = writeln!(
627 s,
628 "------------------------------------------------------------------------"
629 );
630 let _ = writeln!(s);
631 match &self.raw_output {
632 Some(output) => {
633 let _ = writeln!(s, "{output}");
634 }
635 None => {
636 let _ = writeln!(s, "[No output captured]");
637 }
638 }
639 let _ = writeln!(s);
640 }
641
642 fn write_extraction_attempts_to_string(&self, s: &mut String) {
643 use std::fmt::Write;
644 let _ = writeln!(
645 s,
646 "------------------------------------------------------------------------"
647 );
648 let _ = writeln!(s, "EXTRACTION ATTEMPTS");
649 let _ = writeln!(
650 s,
651 "------------------------------------------------------------------------"
652 );
653 let _ = writeln!(s);
654
655 if self.extraction_attempts.is_empty() {
656 let _ = writeln!(s, "[No extraction attempts recorded]");
657 } else {
658 for (i, attempt) in self.extraction_attempts.iter().enumerate() {
659 let status = if attempt.success {
660 "✓ SUCCESS"
661 } else {
662 "✗ FAILED"
663 };
664 let _ = writeln!(s, "{}. {} [{}]", i + 1, attempt.method, status);
665 let _ = writeln!(s, " Detail: {}", attempt.detail);
666 let _ = writeln!(s);
667 }
668 }
669 let _ = writeln!(s);
670 }
671
672 fn write_validation_to_string(&self, s: &mut String) {
673 use std::fmt::Write;
674 let _ = writeln!(
675 s,
676 "------------------------------------------------------------------------"
677 );
678 let _ = writeln!(s, "VALIDATION RESULTS");
679 let _ = writeln!(
680 s,
681 "------------------------------------------------------------------------"
682 );
683 let _ = writeln!(s);
684
685 if self.validation_checks.is_empty() {
686 let _ = writeln!(s, "[No validation checks recorded]");
687 } else {
688 for check in &self.validation_checks {
689 let status = if check.passed { "✓ PASS" } else { "✗ FAIL" };
690 let _ = write!(s, " [{status}] {}", check.name);
691 if let Some(error) = &check.error {
692 let _ = writeln!(s, ": {error}");
693 } else {
694 let _ = writeln!(s);
695 }
696 }
697 }
698 let _ = writeln!(s);
699 }
700
701 fn write_outcome_to_string(&self, s: &mut String) {
702 use std::fmt::Write;
703 let _ = writeln!(
704 s,
705 "------------------------------------------------------------------------"
706 );
707 let _ = writeln!(s, "OUTCOME");
708 let _ = writeln!(
709 s,
710 "------------------------------------------------------------------------"
711 );
712 let _ = writeln!(s);
713 match &self.outcome {
714 Some(outcome) => {
715 let _ = writeln!(s, "{outcome}");
716 }
717 None => {
718 let _ = writeln!(s, "[Outcome not recorded]");
719 }
720 }
721 let _ = writeln!(s);
722 let _ = writeln!(
723 s,
724 "========================================================================"
725 );
726 }
727}
728
729fn sanitize_agent_name(agent: &str) -> String {
731 agent
732 .chars()
733 .map(|c| if c.is_alphanumeric() { c } else { '_' })
734 .collect::<String>()
735 .chars()
736 .take(20)
737 .collect()
738}
739
740#[derive(Debug)]
745pub struct CommitLogSession {
746 run_dir: PathBuf,
748 attempt_counter: usize,
750}
751
752impl CommitLogSession {
753 pub fn new(base_log_dir: &str, workspace: &dyn Workspace) -> std::io::Result<Self> {
762 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
763 let run_dir = PathBuf::from(base_log_dir).join(format!("run_{timestamp}"));
764 workspace.create_dir_all(&run_dir)?;
765
766 Ok(Self {
767 run_dir,
768 attempt_counter: 0,
769 })
770 }
771
772 pub fn run_dir(&self) -> &Path {
774 &self.run_dir
775 }
776
777 pub const fn next_attempt_number(&mut self) -> usize {
779 self.attempt_counter += 1;
780 self.attempt_counter
781 }
782
783 pub fn new_attempt(&mut self, agent: &str, strategy: &str) -> CommitAttemptLog {
790 let attempt_number = self.next_attempt_number();
791 CommitAttemptLog::new(attempt_number, agent, strategy)
792 }
793
794 pub fn write_summary(
802 &self,
803 total_attempts: usize,
804 final_outcome: &str,
805 workspace: &dyn Workspace,
806 ) -> std::io::Result<()> {
807 use std::fmt::Write;
808
809 let summary_path = self.run_dir.join("SUMMARY.txt");
810 let mut content = String::new();
811
812 let _ = writeln!(content, "COMMIT GENERATION SESSION SUMMARY");
813 let _ = writeln!(content, "=================================");
814 let _ = writeln!(content);
815 let _ = writeln!(content, "Run directory: {}", self.run_dir.display());
816 let _ = writeln!(content, "Total attempts: {total_attempts}");
817 let _ = writeln!(content, "Final outcome: {final_outcome}");
818 let _ = writeln!(content);
819 let _ = writeln!(content, "Individual attempt logs are in this directory.");
820
821 workspace.write(&summary_path, &content)?;
822 Ok(())
823 }
824}
825
826#[cfg(test)]
827mod tests {
828 use super::*;
829 use crate::workspace::MemoryWorkspace;
830
831 #[test]
836 fn test_attempt_log_write_to_workspace() {
837 let workspace = MemoryWorkspace::new_test();
838 let log_dir = Path::new(".agent/logs/commit_generation/run_test");
839
840 let mut log = CommitAttemptLog::new(1, "claude", "initial");
841 log.set_prompt_size(5000);
842 log.set_diff_info(10000, false);
843 log.set_raw_output("raw agent output here");
844 log.add_extraction_attempt(ExtractionAttempt::failure(
845 "XML",
846 "No <ralph-commit> tag found".to_string(),
847 ));
848 log.set_outcome(AttemptOutcome::Success("feat: add feature".to_string()));
849
850 let log_path = log.write_to_workspace(log_dir, &workspace).unwrap();
851 assert!(workspace.exists(&log_path));
852
853 let content = workspace.read(&log_path).unwrap();
854 assert!(content.contains("COMMIT GENERATION ATTEMPT LOG"));
855 assert!(content.contains("Attempt: #1"));
856 assert!(content.contains("claude"));
857 }
858
859 #[test]
860 fn test_attempt_log_write_with_all_fields() {
861 let workspace = MemoryWorkspace::new_test();
862 let log_dir = Path::new(".agent/logs/commit_generation/run_test");
863
864 let mut log = CommitAttemptLog::new(1, "claude", "initial");
865 log.set_prompt_size(5000);
866 log.set_diff_info(10000, false);
867 log.set_raw_output("raw agent output here");
868 log.add_extraction_attempt(ExtractionAttempt::failure(
869 "XML",
870 "No <ralph-commit> tag found".to_string(),
871 ));
872 log.add_extraction_attempt(ExtractionAttempt::success(
873 "JSON",
874 "Extracted from JSON".to_string(),
875 ));
876 log.set_validation_checks(vec![
877 ValidationCheck::pass("basic_length"),
878 ValidationCheck::fail("no_bad_patterns", "File list pattern detected".to_string()),
879 ]);
880 log.set_outcome(AttemptOutcome::ExtractionFailed("bad pattern".to_string()));
881
882 let log_path = log.write_to_workspace(log_dir, &workspace).unwrap();
883 assert!(workspace.exists(&log_path));
884
885 let content = workspace.read(&log_path).unwrap();
886 assert!(content.contains("COMMIT GENERATION ATTEMPT LOG"));
887 assert!(content.contains("Attempt: #1"));
888 assert!(content.contains("claude"));
889 assert!(content.contains("EXTRACTION ATTEMPTS"));
890 assert!(content.contains("VALIDATION RESULTS"));
891 assert!(content.contains("OUTCOME"));
892 }
893
894 #[test]
895 fn test_parsing_trace_write_to_workspace() {
896 let workspace = MemoryWorkspace::new_test();
897 let log_dir = Path::new(".agent/logs/commit_generation/run_test");
898
899 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
900 trace.set_raw_output("raw agent output");
901 trace.add_step(
902 ParsingTraceStep::new(1, "XML extraction")
903 .with_input("input")
904 .with_success(true),
905 );
906 trace.set_final_message("feat: add feature");
907
908 let trace_path = trace.write_to_workspace(log_dir, &workspace).unwrap();
909 assert!(workspace.exists(&trace_path));
910
911 let content = workspace.read(&trace_path).unwrap();
912 assert!(content.contains("PARSING TRACE LOG"));
913 assert!(content.contains("Attempt #001"));
914 }
915
916 #[test]
917 fn test_parsing_trace_write_with_steps() {
918 let workspace = MemoryWorkspace::new_test();
919 let log_dir = Path::new(".agent/logs/commit_generation/run_test");
920
921 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
922 trace.set_raw_output("raw agent output");
923 trace.add_step(
924 ParsingTraceStep::new(1, "XML extraction")
925 .with_input("input")
926 .with_result("result")
927 .with_success(true)
928 .with_details("success"),
929 );
930 trace.add_step(
931 ParsingTraceStep::new(2, "Validation")
932 .with_success(false)
933 .with_details("failed"),
934 );
935 trace.set_final_message("feat: add feature");
936
937 let trace_path = trace.write_to_workspace(log_dir, &workspace).unwrap();
938 assert!(workspace.exists(&trace_path));
939 assert!(trace_path.to_string_lossy().contains("parsing_trace"));
940
941 let content = workspace.read(&trace_path).unwrap();
942 assert!(content.contains("PARSING TRACE LOG"));
943 assert!(content.contains("Attempt #001"));
944 assert!(content.contains("RAW AGENT OUTPUT"));
945 assert!(content.contains("PARSING STEPS"));
946 assert!(content.contains("FINAL EXTRACTED MESSAGE"));
947 }
948
949 #[test]
950 fn test_session_creates_run_directory() {
951 let workspace = MemoryWorkspace::new_test();
952
953 let session = CommitLogSession::new(".agent/logs/commit_generation", &workspace).unwrap();
954 assert!(workspace.exists(session.run_dir()));
955 assert!(session.run_dir().to_string_lossy().contains("run_"));
956 }
957
958 #[test]
959 fn test_session_increments_attempt_number() {
960 let workspace = MemoryWorkspace::new_test();
961
962 let mut session =
963 CommitLogSession::new(".agent/logs/commit_generation", &workspace).unwrap();
964
965 assert_eq!(session.next_attempt_number(), 1);
966 assert_eq!(session.next_attempt_number(), 2);
967 assert_eq!(session.next_attempt_number(), 3);
968 }
969
970 #[test]
971 fn test_session_new_attempt() {
972 let workspace = MemoryWorkspace::new_test();
973
974 let mut session =
975 CommitLogSession::new(".agent/logs/commit_generation", &workspace).unwrap();
976
977 let log1 = session.new_attempt("claude", "initial");
978 assert_eq!(log1.attempt_number, 1);
979
980 let log2 = session.new_attempt("glm", "strict_json");
981 assert_eq!(log2.attempt_number, 2);
982 }
983
984 #[test]
985 fn test_session_write_summary() {
986 let workspace = MemoryWorkspace::new_test();
987
988 let session = CommitLogSession::new(".agent/logs/commit_generation", &workspace).unwrap();
989 session
990 .write_summary(5, "SUCCESS: feat: add feature", &workspace)
991 .unwrap();
992
993 let summary_path = session.run_dir().join("SUMMARY.txt");
994 assert!(workspace.exists(&summary_path));
995
996 let content = workspace.read(&summary_path).unwrap();
997 assert!(content.contains("Total attempts: 5"));
998 assert!(content.contains("SUCCESS"));
999 }
1000
1001 #[test]
1002 fn test_sanitize_agent_name() {
1003 assert_eq!(sanitize_agent_name("claude"), "claude");
1004 assert_eq!(sanitize_agent_name("agent/commit"), "agent_commit");
1005 assert_eq!(sanitize_agent_name("my-agent-v2"), "my_agent_v2");
1006 let long_name = "a".repeat(50);
1008 assert_eq!(sanitize_agent_name(&long_name).len(), 20);
1009 }
1010
1011 #[test]
1012 fn test_large_output_truncation() {
1013 let mut log = CommitAttemptLog::new(1, "test", "test");
1014 let large_output = "x".repeat(100_000);
1015 log.set_raw_output(&large_output);
1016
1017 let output = log.raw_output.unwrap();
1018 assert!(output.len() < large_output.len());
1019 assert!(output.contains("[... truncated"));
1020 }
1021
1022 #[test]
1023 fn test_parsing_trace_step_creation() {
1024 let step = ParsingTraceStep::new(1, "XML extraction");
1025 assert_eq!(step.step_number, 1);
1026 assert_eq!(step.description, "XML extraction");
1027 assert!(!step.success);
1028 assert!(step.input.is_none());
1029 assert!(step.result.is_none());
1030 }
1031
1032 #[test]
1033 fn test_parsing_trace_step_builder() {
1034 let step = ParsingTraceStep::new(1, "XML extraction")
1035 .with_input("input content")
1036 .with_result("result content")
1037 .with_success(true)
1038 .with_details("extraction successful");
1039
1040 assert!(step.success);
1041 assert_eq!(step.input.as_deref(), Some("input content"));
1042 assert_eq!(step.result.as_deref(), Some("result content"));
1043 assert_eq!(step.details, "extraction successful");
1044 }
1045
1046 #[test]
1047 fn test_parsing_trace_step_truncation() {
1048 let large_input = "x".repeat(100_000);
1049 let step = ParsingTraceStep::new(1, "test").with_input(&large_input);
1050
1051 assert!(step.input.is_some());
1052 let input = step.input.as_ref().unwrap();
1053 assert!(input.len() < large_input.len());
1054 assert!(input.contains("[... input truncated"));
1055 }
1056
1057 #[test]
1058 fn test_parsing_trace_log_creation() {
1059 let trace = ParsingTraceLog::new(1, "claude", "initial");
1060 assert_eq!(trace.attempt_number, 1);
1061 assert_eq!(trace.agent, "claude");
1062 assert_eq!(trace.strategy, "initial");
1063 assert!(trace.raw_output.is_none());
1064 assert!(trace.steps.is_empty());
1065 assert!(trace.final_message.is_none());
1066 }
1067
1068 #[test]
1069 fn test_parsing_trace_log_set_raw_output() {
1070 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1071 trace.set_raw_output("test output");
1072
1073 assert_eq!(trace.raw_output.as_deref(), Some("test output"));
1074 }
1075
1076 #[test]
1077 fn test_parsing_trace_raw_output_truncation() {
1078 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1079 let large_output = "x".repeat(100_000);
1080 trace.set_raw_output(&large_output);
1081
1082 let output = trace.raw_output.unwrap();
1083 assert!(output.len() < large_output.len());
1084 assert!(output.contains("[... raw output truncated"));
1085 }
1086
1087 #[test]
1088 fn test_parsing_trace_add_step() {
1089 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1090 let step = ParsingTraceStep::new(1, "XML extraction");
1091 trace.add_step(step);
1092
1093 assert_eq!(trace.steps.len(), 1);
1094 assert_eq!(trace.steps[0].description, "XML extraction");
1095 }
1096
1097 #[test]
1098 fn test_parsing_trace_set_final_message() {
1099 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1100 trace.set_final_message("feat: add feature");
1101
1102 assert_eq!(trace.final_message.as_deref(), Some("feat: add feature"));
1103 }
1104
1105 #[test]
1106 fn test_attempt_log_creation() {
1107 let log = CommitAttemptLog::new(1, "claude", "initial");
1108 assert_eq!(log.attempt_number, 1);
1109 assert_eq!(log.agent, "claude");
1110 assert_eq!(log.strategy, "initial");
1111 assert!(log.raw_output.is_none());
1112 assert!(log.extraction_attempts.is_empty());
1113 assert!(log.validation_checks.is_empty());
1114 assert!(log.outcome.is_none());
1115 }
1116
1117 #[test]
1118 fn test_attempt_log_set_values() {
1119 let mut log = CommitAttemptLog::new(2, "glm", "strict_json");
1120
1121 log.set_prompt_size(10_000);
1122 log.set_diff_info(50_000, true);
1123 log.set_raw_output("test output");
1124
1125 assert_eq!(log.prompt_size_bytes, 10_000);
1126 assert_eq!(log.diff_size_bytes, 50_000);
1127 assert!(log.diff_was_truncated);
1128 assert_eq!(log.raw_output.as_deref(), Some("test output"));
1129 }
1130
1131 #[test]
1132 fn test_extraction_attempt_creation() {
1133 let success =
1134 ExtractionAttempt::success("XML", "Found <ralph-commit> at pos 0".to_string());
1135 assert!(success.success);
1136 assert_eq!(success.method, "XML");
1137
1138 let failure = ExtractionAttempt::failure("JSON", "No JSON found".to_string());
1139 assert!(!failure.success);
1140 assert_eq!(failure.method, "JSON");
1141 }
1142
1143 #[test]
1144 fn test_validation_check_creation() {
1145 let pass = ValidationCheck::pass("basic_length");
1146 assert!(pass.passed);
1147 assert!(pass.error.is_none());
1148
1149 let fail = ValidationCheck::fail("no_json_artifacts", "Found JSON in message".to_string());
1150 assert!(!fail.passed);
1151 assert!(fail.error.is_some());
1152 }
1153
1154 #[test]
1155 fn test_outcome_display() {
1156 let success = AttemptOutcome::Success("feat: add feature".to_string());
1157 assert!(format!("{success}").contains("SUCCESS"));
1158
1159 let error = AttemptOutcome::ExtractionFailed("extraction failed".to_string());
1160 assert!(format!("{error}").contains("EXTRACTION_FAILED"));
1161 }
1162}