1use chrono::{DateTime, Local};
16use std::path::{Path, PathBuf};
17
18use crate::workspace::Workspace;
19
20#[derive(Debug, Clone)]
22pub struct ParsingTraceStep {
23 pub step_number: usize,
25 pub description: String,
27 pub input: Option<String>,
29 pub result: Option<String>,
31 pub success: bool,
33 pub details: String,
35}
36
37impl ParsingTraceStep {
38 pub fn new(step_number: usize, description: &str) -> Self {
40 Self {
41 step_number,
42 description: description.to_string(),
43 input: None,
44 result: None,
45 success: false,
46 details: String::new(),
47 }
48 }
49
50 pub fn with_input(mut self, input: &str) -> Self {
52 const MAX_INPUT_SIZE: usize = 10_000;
54 self.input = if input.len() > MAX_INPUT_SIZE {
55 Some(format!(
56 "{}\n\n[... input truncated {} bytes ...]",
57 &input[..MAX_INPUT_SIZE / 2],
58 input.len() - MAX_INPUT_SIZE
59 ))
60 } else {
61 Some(input.to_string())
62 };
63 self
64 }
65
66 pub fn with_result(mut self, result: &str) -> Self {
68 const MAX_RESULT_SIZE: usize = 10_000;
70 self.result = if result.len() > MAX_RESULT_SIZE {
71 Some(format!(
72 "{}\n\n[... result truncated {} bytes ...]",
73 &result[..MAX_RESULT_SIZE / 2],
74 result.len() - MAX_RESULT_SIZE
75 ))
76 } else {
77 Some(result.to_string())
78 };
79 self
80 }
81
82 pub const fn with_success(mut self, success: bool) -> Self {
84 self.success = success;
85 self
86 }
87
88 pub fn with_details(mut self, details: &str) -> Self {
90 self.details = details.to_string();
91 self
92 }
93}
94
95#[derive(Debug, Clone)]
105pub struct ParsingTraceLog {
106 pub attempt_number: usize,
108 pub agent: String,
110 pub strategy: String,
112 pub raw_output: Option<String>,
114 pub steps: Vec<ParsingTraceStep>,
116 pub final_message: Option<String>,
118 pub timestamp: DateTime<Local>,
120}
121
122impl ParsingTraceLog {
123 pub fn new(attempt_number: usize, agent: &str, strategy: &str) -> Self {
125 Self {
126 attempt_number,
127 agent: agent.to_string(),
128 strategy: strategy.to_string(),
129 raw_output: None,
130 steps: Vec::new(),
131 final_message: None,
132 timestamp: Local::now(),
133 }
134 }
135
136 pub fn set_raw_output(&mut self, output: &str) {
138 const MAX_OUTPUT_SIZE: usize = 50_000;
139 self.raw_output = if output.len() > MAX_OUTPUT_SIZE {
140 Some(format!(
141 "{}\n\n[... raw output truncated {} bytes ...]\n\n{}",
142 &output[..MAX_OUTPUT_SIZE / 2],
143 output.len() - MAX_OUTPUT_SIZE,
144 &output[output.len() - MAX_OUTPUT_SIZE / 2..]
145 ))
146 } else {
147 Some(output.to_string())
148 };
149 }
150
151 pub fn add_step(&mut self, step: ParsingTraceStep) {
153 self.steps.push(step);
154 }
155
156 pub fn set_final_message(&mut self, message: &str) {
158 self.final_message = Some(message.to_string());
159 }
160
161 pub fn write_to_workspace(
175 &self,
176 log_dir: &Path,
177 workspace: &dyn Workspace,
178 ) -> std::io::Result<PathBuf> {
179 let trace_path = log_dir.join(format!(
180 "attempt_{:03}_parsing_trace.log",
181 self.attempt_number
182 ));
183
184 let mut content = String::new();
186 Self::write_header_to_string(&mut content, self);
187 Self::write_raw_output_to_string(&mut content, self);
188 Self::write_parsing_steps_to_string(&mut content, self);
189 Self::write_final_message_to_string(&mut content, self);
190 Self::write_footer_to_string(&mut content);
191
192 workspace.create_dir_all(log_dir)?;
194 workspace.write(&trace_path, &content)?;
195
196 Ok(trace_path)
197 }
198
199 fn write_header_to_string(s: &mut String, trace: &Self) {
201 use std::fmt::Write;
202 let _ = writeln!(
203 s,
204 "================================================================================"
205 );
206 let _ = writeln!(
207 s,
208 "PARSING TRACE LOG - Attempt #{:03}",
209 trace.attempt_number
210 );
211 let _ = writeln!(
212 s,
213 "================================================================================"
214 );
215 let _ = writeln!(s);
216 let _ = writeln!(s, "Agent: {}", trace.agent);
217 let _ = writeln!(s, "Strategy: {}", trace.strategy);
218 let _ = writeln!(
219 s,
220 "Timestamp: {}",
221 trace.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
222 );
223 let _ = writeln!(s);
224 }
225
226 fn write_raw_output_to_string(s: &mut String, trace: &Self) {
227 use std::fmt::Write;
228 let _ = writeln!(
229 s,
230 "--------------------------------------------------------------------------------"
231 );
232 let _ = writeln!(s, "RAW AGENT OUTPUT");
233 let _ = writeln!(
234 s,
235 "--------------------------------------------------------------------------------"
236 );
237 let _ = writeln!(s);
238 match &trace.raw_output {
239 Some(output) => {
240 let _ = writeln!(s, "{output}");
241 }
242 None => {
243 let _ = writeln!(s, "[No raw output captured]");
244 }
245 }
246 let _ = writeln!(s);
247 }
248
249 fn write_parsing_steps_to_string(s: &mut String, trace: &Self) {
250 use std::fmt::Write;
251 let _ = writeln!(
252 s,
253 "--------------------------------------------------------------------------------"
254 );
255 let _ = writeln!(s, "PARSING STEPS");
256 let _ = writeln!(
257 s,
258 "--------------------------------------------------------------------------------"
259 );
260 let _ = writeln!(s);
261
262 if trace.steps.is_empty() {
263 let _ = writeln!(s, "[No parsing steps recorded]");
264 } else {
265 for step in &trace.steps {
266 let status = if step.success {
267 "✓ SUCCESS"
268 } else {
269 "✗ FAILED"
270 };
271 let _ = writeln!(s, "{}. {} [{}]", step.step_number, step.description, status);
272 let _ = writeln!(s);
273
274 if let Some(input) = &step.input {
275 let _ = writeln!(s, " INPUT:");
276 for line in input.lines() {
277 let _ = writeln!(s, " {line}");
278 }
279 let _ = writeln!(s);
280 }
281
282 if let Some(result) = &step.result {
283 let _ = writeln!(s, " RESULT:");
284 for line in result.lines() {
285 let _ = writeln!(s, " {line}");
286 }
287 let _ = writeln!(s);
288 }
289
290 if !step.details.is_empty() {
291 let _ = writeln!(s, " DETAILS: {}", step.details);
292 let _ = writeln!(s);
293 }
294 }
295 }
296 let _ = writeln!(s);
297 }
298
299 fn write_final_message_to_string(s: &mut String, trace: &Self) {
300 use std::fmt::Write;
301 let _ = writeln!(
302 s,
303 "--------------------------------------------------------------------------------"
304 );
305 let _ = writeln!(s, "FINAL EXTRACTED MESSAGE");
306 let _ = writeln!(
307 s,
308 "--------------------------------------------------------------------------------"
309 );
310 let _ = writeln!(s);
311 match &trace.final_message {
312 Some(message) => {
313 let _ = writeln!(s, "{message}");
314 }
315 None => {
316 let _ = writeln!(s, "[No message extracted]");
317 }
318 }
319 let _ = writeln!(s);
320 }
321
322 fn write_footer_to_string(s: &mut String) {
323 use std::fmt::Write;
324 let _ = writeln!(
325 s,
326 "================================================================================"
327 );
328 }
329}
330
331#[derive(Debug, Clone)]
333pub struct ExtractionAttempt {
334 pub method: &'static str,
336 pub success: bool,
338 pub detail: String,
340}
341
342impl ExtractionAttempt {
343 pub const fn success(method: &'static str, detail: String) -> Self {
345 Self {
346 method,
347 success: true,
348 detail,
349 }
350 }
351
352 pub const fn failure(method: &'static str, detail: String) -> Self {
354 Self {
355 method,
356 success: false,
357 detail,
358 }
359 }
360}
361
362#[derive(Debug, Clone)]
364pub struct ValidationCheck {
365 pub name: &'static str,
367 pub passed: bool,
369 pub error: Option<String>,
371}
372
373impl ValidationCheck {
374 #[cfg(test)]
376 pub const fn pass(name: &'static str) -> Self {
377 Self {
378 name,
379 passed: true,
380 error: None,
381 }
382 }
383
384 #[cfg(test)]
386 pub const fn fail(name: &'static str, error: String) -> Self {
387 Self {
388 name,
389 passed: false,
390 error: Some(error),
391 }
392 }
393}
394
395#[derive(Debug, Clone)]
397pub enum AttemptOutcome {
398 Success(String),
400 XsdValidationFailed(String),
402 ExtractionFailed(String),
404}
405
406impl std::fmt::Display for AttemptOutcome {
407 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 match self {
409 Self::Success(msg) => write!(f, "SUCCESS: {}", preview_message(msg)),
410 Self::XsdValidationFailed(err) => write!(f, "XSD_VALIDATION_FAILED: {err}"),
411 Self::ExtractionFailed(err) => write!(f, "EXTRACTION_FAILED: {err}"),
412 }
413 }
414}
415
416fn preview_message(msg: &str) -> String {
418 let first_line = msg.lines().next().unwrap_or(msg);
419 if first_line.len() > 60 {
420 format!("{}...", &first_line[..60])
421 } else {
422 first_line.to_string()
423 }
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}