1use chrono::{DateTime, Local};
16use std::fs::{self, File, OpenOptions};
17use std::io::{BufWriter, Write};
18use std::path::{Path, PathBuf};
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_file(&self, log_dir: &Path) -> std::io::Result<PathBuf> {
171 let trace_path = log_dir.join(format!(
172 "attempt_{:03}_parsing_trace.log",
173 self.attempt_number
174 ));
175
176 let file = File::create(&trace_path)?;
177 let mut writer = BufWriter::new(file);
178
179 Self::write_header(&mut writer, self)?;
180 Self::write_raw_output(&mut writer, self)?;
181 Self::write_parsing_steps(&mut writer, self)?;
182 Self::write_final_message(&mut writer, self)?;
183 Self::write_footer(&mut writer)?;
184
185 writer.flush()?;
186 Ok(trace_path)
187 }
188
189 fn write_header(w: &mut impl Write, trace: &Self) -> std::io::Result<()> {
190 writeln!(
191 w,
192 "================================================================================"
193 )?;
194 writeln!(
195 w,
196 "PARSING TRACE LOG - Attempt #{:03}",
197 trace.attempt_number
198 )?;
199 writeln!(
200 w,
201 "================================================================================"
202 )?;
203 writeln!(w)?;
204 writeln!(w, "Agent: {}", trace.agent)?;
205 writeln!(w, "Strategy: {}", trace.strategy)?;
206 writeln!(
207 w,
208 "Timestamp: {}",
209 trace.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
210 )?;
211 writeln!(w)?;
212 Ok(())
213 }
214
215 fn write_raw_output(w: &mut impl Write, trace: &Self) -> std::io::Result<()> {
216 writeln!(
217 w,
218 "--------------------------------------------------------------------------------"
219 )?;
220 writeln!(w, "RAW AGENT OUTPUT")?;
221 writeln!(
222 w,
223 "--------------------------------------------------------------------------------"
224 )?;
225 writeln!(w)?;
226 match &trace.raw_output {
227 Some(output) => {
228 writeln!(w, "{output}")?;
229 }
230 None => {
231 writeln!(w, "[No raw output captured]")?;
232 }
233 }
234 writeln!(w)?;
235 Ok(())
236 }
237
238 fn write_parsing_steps(w: &mut impl Write, trace: &Self) -> std::io::Result<()> {
239 writeln!(
240 w,
241 "--------------------------------------------------------------------------------"
242 )?;
243 writeln!(w, "PARSING STEPS")?;
244 writeln!(
245 w,
246 "--------------------------------------------------------------------------------"
247 )?;
248 writeln!(w)?;
249
250 if trace.steps.is_empty() {
251 writeln!(w, "[No parsing steps recorded]")?;
252 } else {
253 for step in &trace.steps {
254 let status = if step.success {
255 "✓ SUCCESS"
256 } else {
257 "✗ FAILED"
258 };
259 writeln!(w, "{}. {} [{}]", step.step_number, step.description, status)?;
260 writeln!(w)?;
261
262 if let Some(input) = &step.input {
263 writeln!(w, " INPUT:")?;
264 for line in input.lines() {
265 writeln!(w, " {line}")?;
266 }
267 writeln!(w)?;
268 }
269
270 if let Some(result) = &step.result {
271 writeln!(w, " RESULT:")?;
272 for line in result.lines() {
273 writeln!(w, " {line}")?;
274 }
275 writeln!(w)?;
276 }
277
278 if !step.details.is_empty() {
279 writeln!(w, " DETAILS: {}", step.details)?;
280 writeln!(w)?;
281 }
282 }
283 }
284 writeln!(w)?;
285 Ok(())
286 }
287
288 fn write_final_message(w: &mut impl Write, trace: &Self) -> std::io::Result<()> {
289 writeln!(
290 w,
291 "--------------------------------------------------------------------------------"
292 )?;
293 writeln!(w, "FINAL EXTRACTED MESSAGE")?;
294 writeln!(
295 w,
296 "--------------------------------------------------------------------------------"
297 )?;
298 writeln!(w)?;
299 match &trace.final_message {
300 Some(message) => {
301 writeln!(w, "{message}")?;
302 }
303 None => {
304 writeln!(w, "[No message extracted]")?;
305 }
306 }
307 writeln!(w)?;
308 Ok(())
309 }
310
311 fn write_footer(w: &mut impl Write) -> std::io::Result<()> {
312 writeln!(
313 w,
314 "================================================================================"
315 )?;
316 Ok(())
317 }
318}
319
320#[derive(Debug, Clone)]
322pub struct ExtractionAttempt {
323 pub method: &'static str,
325 pub success: bool,
327 pub detail: String,
329}
330
331impl ExtractionAttempt {
332 pub const fn success(method: &'static str, detail: String) -> Self {
334 Self {
335 method,
336 success: true,
337 detail,
338 }
339 }
340
341 pub const fn failure(method: &'static str, detail: String) -> Self {
343 Self {
344 method,
345 success: false,
346 detail,
347 }
348 }
349}
350
351#[derive(Debug, Clone)]
353pub struct ValidationCheck {
354 pub name: &'static str,
356 pub passed: bool,
358 pub error: Option<String>,
360}
361
362impl ValidationCheck {
363 pub const fn pass(name: &'static str) -> Self {
365 Self {
366 name,
367 passed: true,
368 error: None,
369 }
370 }
371
372 pub const fn fail(name: &'static str, error: String) -> Self {
374 Self {
375 name,
376 passed: false,
377 error: Some(error),
378 }
379 }
380}
381
382#[derive(Debug, Clone)]
384pub enum AttemptOutcome {
385 Success(String),
387 Fallback(String),
389 AgentError(String),
391 ExtractionFailed(String),
393}
394
395impl std::fmt::Display for AttemptOutcome {
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 match self {
398 Self::Success(msg) => write!(f, "SUCCESS: {}", preview_message(msg)),
399 Self::Fallback(msg) => write!(f, "FALLBACK: {}", preview_message(msg)),
400 Self::AgentError(err) => write!(f, "AGENT_ERROR: {err}"),
401 Self::ExtractionFailed(err) => write!(f, "EXTRACTION_FAILED: {err}"),
402 }
403 }
404}
405
406fn preview_message(msg: &str) -> String {
408 let first_line = msg.lines().next().unwrap_or(msg);
409 if first_line.len() > 60 {
410 format!("{}...", &first_line[..60])
411 } else {
412 first_line.to_string()
413 }
414}
415
416#[derive(Debug, Clone)]
421pub struct CommitAttemptLog {
422 pub attempt_number: usize,
424 pub agent: String,
426 pub strategy: String,
428 pub timestamp: DateTime<Local>,
430 pub prompt_size_bytes: usize,
432 pub diff_size_bytes: usize,
434 pub diff_was_truncated: bool,
436 pub raw_output: Option<String>,
438 pub extraction_attempts: Vec<ExtractionAttempt>,
440 pub validation_checks: Vec<ValidationCheck>,
442 pub outcome: Option<AttemptOutcome>,
444}
445
446impl CommitAttemptLog {
447 pub fn new(attempt_number: usize, agent: &str, strategy: &str) -> Self {
449 Self {
450 attempt_number,
451 agent: agent.to_string(),
452 strategy: strategy.to_string(),
453 timestamp: Local::now(),
454 prompt_size_bytes: 0,
455 diff_size_bytes: 0,
456 diff_was_truncated: false,
457 raw_output: None,
458 extraction_attempts: Vec::new(),
459 validation_checks: Vec::new(),
460 outcome: None,
461 }
462 }
463
464 pub const fn set_prompt_size(&mut self, size: usize) {
466 self.prompt_size_bytes = size;
467 }
468
469 pub const fn set_diff_info(&mut self, size: usize, was_truncated: bool) {
471 self.diff_size_bytes = size;
472 self.diff_was_truncated = was_truncated;
473 }
474
475 pub fn set_raw_output(&mut self, output: &str) {
479 const MAX_OUTPUT_SIZE: usize = 50_000;
480 if output.len() > MAX_OUTPUT_SIZE {
481 self.raw_output = Some(format!(
482 "{}\n\n[... truncated {} bytes ...]\n\n{}",
483 &output[..MAX_OUTPUT_SIZE / 2],
484 output.len() - MAX_OUTPUT_SIZE,
485 &output[output.len() - MAX_OUTPUT_SIZE / 2..]
486 ));
487 } else {
488 self.raw_output = Some(output.to_string());
489 }
490 }
491
492 pub fn add_extraction_attempt(&mut self, attempt: ExtractionAttempt) {
494 self.extraction_attempts.push(attempt);
495 }
496
497 pub fn set_validation_checks(&mut self, checks: Vec<ValidationCheck>) {
499 self.validation_checks = checks;
500 }
501
502 pub fn set_outcome(&mut self, outcome: AttemptOutcome) {
504 self.outcome = Some(outcome);
505 }
506
507 pub fn write_to_file(&self, log_dir: &Path) -> std::io::Result<PathBuf> {
517 fs::create_dir_all(log_dir)?;
519
520 let filename = format!(
522 "attempt_{:03}_{}_{}_{}.log",
523 self.attempt_number,
524 sanitize_agent_name(&self.agent),
525 self.strategy.replace(' ', "_"),
526 self.timestamp.format("%Y%m%dT%H%M%S")
527 );
528 let log_path = log_dir.join(filename);
529
530 let file = OpenOptions::new()
532 .create(true)
533 .write(true)
534 .truncate(true)
535 .open(&log_path)?;
536 let mut writer = BufWriter::new(file);
537
538 self.write_header(&mut writer)?;
539 self.write_context(&mut writer)?;
540 self.write_raw_output(&mut writer)?;
541 self.write_extraction_attempts(&mut writer)?;
542 self.write_validation(&mut writer)?;
543 self.write_outcome(&mut writer)?;
544
545 writer.flush()?;
546 Ok(log_path)
547 }
548
549 fn write_header(&self, w: &mut impl Write) -> std::io::Result<()> {
550 writeln!(
551 w,
552 "========================================================================"
553 )?;
554 writeln!(w, "COMMIT GENERATION ATTEMPT LOG")?;
555 writeln!(
556 w,
557 "========================================================================"
558 )?;
559 writeln!(w)?;
560 writeln!(w, "Attempt: #{}", self.attempt_number)?;
561 writeln!(w, "Agent: {}", self.agent)?;
562 writeln!(w, "Strategy: {}", self.strategy)?;
563 writeln!(
564 w,
565 "Timestamp: {}",
566 self.timestamp.format("%Y-%m-%d %H:%M:%S %Z")
567 )?;
568 writeln!(w)?;
569 Ok(())
570 }
571
572 fn write_context(&self, w: &mut impl Write) -> std::io::Result<()> {
573 writeln!(
574 w,
575 "------------------------------------------------------------------------"
576 )?;
577 writeln!(w, "CONTEXT")?;
578 writeln!(
579 w,
580 "------------------------------------------------------------------------"
581 )?;
582 writeln!(w)?;
583 writeln!(
584 w,
585 "Prompt size: {} bytes ({} KB)",
586 self.prompt_size_bytes,
587 self.prompt_size_bytes / 1024
588 )?;
589 writeln!(
590 w,
591 "Diff size: {} bytes ({} KB)",
592 self.diff_size_bytes,
593 self.diff_size_bytes / 1024
594 )?;
595 writeln!(
596 w,
597 "Diff truncated: {}",
598 if self.diff_was_truncated { "YES" } else { "NO" }
599 )?;
600 writeln!(w)?;
601 Ok(())
602 }
603
604 fn write_raw_output(&self, w: &mut impl Write) -> std::io::Result<()> {
605 writeln!(
606 w,
607 "------------------------------------------------------------------------"
608 )?;
609 writeln!(w, "RAW AGENT OUTPUT")?;
610 writeln!(
611 w,
612 "------------------------------------------------------------------------"
613 )?;
614 writeln!(w)?;
615 match &self.raw_output {
616 Some(output) => {
617 writeln!(w, "{output}")?;
618 }
619 None => {
620 writeln!(w, "[No output captured]")?;
621 }
622 }
623 writeln!(w)?;
624 Ok(())
625 }
626
627 fn write_extraction_attempts(&self, w: &mut impl Write) -> std::io::Result<()> {
628 writeln!(
629 w,
630 "------------------------------------------------------------------------"
631 )?;
632 writeln!(w, "EXTRACTION ATTEMPTS")?;
633 writeln!(
634 w,
635 "------------------------------------------------------------------------"
636 )?;
637 writeln!(w)?;
638
639 if self.extraction_attempts.is_empty() {
640 writeln!(w, "[No extraction attempts recorded]")?;
641 } else {
642 for (i, attempt) in self.extraction_attempts.iter().enumerate() {
643 let status = if attempt.success {
644 "✓ SUCCESS"
645 } else {
646 "✗ FAILED"
647 };
648 writeln!(w, "{}. {} [{}]", i + 1, attempt.method, status)?;
649 writeln!(w, " Detail: {}", attempt.detail)?;
650 writeln!(w)?;
651 }
652 }
653 writeln!(w)?;
654 Ok(())
655 }
656
657 fn write_validation(&self, w: &mut impl Write) -> std::io::Result<()> {
658 writeln!(
659 w,
660 "------------------------------------------------------------------------"
661 )?;
662 writeln!(w, "VALIDATION RESULTS")?;
663 writeln!(
664 w,
665 "------------------------------------------------------------------------"
666 )?;
667 writeln!(w)?;
668
669 if self.validation_checks.is_empty() {
670 writeln!(w, "[No validation checks recorded]")?;
671 } else {
672 for check in &self.validation_checks {
673 let status = if check.passed { "✓ PASS" } else { "✗ FAIL" };
674 write!(w, " [{status}] {}", check.name)?;
675 if let Some(error) = &check.error {
676 writeln!(w, ": {error}")?;
677 } else {
678 writeln!(w)?;
679 }
680 }
681 }
682 writeln!(w)?;
683 Ok(())
684 }
685
686 fn write_outcome(&self, w: &mut impl Write) -> std::io::Result<()> {
687 writeln!(
688 w,
689 "------------------------------------------------------------------------"
690 )?;
691 writeln!(w, "OUTCOME")?;
692 writeln!(
693 w,
694 "------------------------------------------------------------------------"
695 )?;
696 writeln!(w)?;
697 match &self.outcome {
698 Some(outcome) => {
699 writeln!(w, "{outcome}")?;
700 }
701 None => {
702 writeln!(w, "[Outcome not recorded]")?;
703 }
704 }
705 writeln!(w)?;
706 writeln!(
707 w,
708 "========================================================================"
709 )?;
710 Ok(())
711 }
712}
713
714fn sanitize_agent_name(agent: &str) -> String {
716 agent
717 .chars()
718 .map(|c| if c.is_alphanumeric() { c } else { '_' })
719 .collect::<String>()
720 .chars()
721 .take(20)
722 .collect()
723}
724
725#[derive(Debug)]
730pub struct CommitLogSession {
731 run_dir: PathBuf,
733 attempt_counter: usize,
735}
736
737impl CommitLogSession {
738 pub fn new(base_log_dir: &str) -> std::io::Result<Self> {
746 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
747 let run_dir = PathBuf::from(base_log_dir).join(format!("run_{timestamp}"));
748 fs::create_dir_all(&run_dir)?;
749
750 Ok(Self {
751 run_dir,
752 attempt_counter: 0,
753 })
754 }
755
756 pub fn run_dir(&self) -> &Path {
758 &self.run_dir
759 }
760
761 pub const fn next_attempt_number(&mut self) -> usize {
763 self.attempt_counter += 1;
764 self.attempt_counter
765 }
766
767 pub fn new_attempt(&mut self, agent: &str, strategy: &str) -> CommitAttemptLog {
774 let attempt_number = self.next_attempt_number();
775 CommitAttemptLog::new(attempt_number, agent, strategy)
776 }
777
778 pub fn write_summary(&self, total_attempts: usize, final_outcome: &str) -> std::io::Result<()> {
785 let summary_path = self.run_dir.join("SUMMARY.txt");
786 let mut file = File::create(summary_path)?;
787
788 writeln!(file, "COMMIT GENERATION SESSION SUMMARY")?;
789 writeln!(file, "=================================")?;
790 writeln!(file)?;
791 writeln!(file, "Run directory: {}", self.run_dir.display())?;
792 writeln!(file, "Total attempts: {total_attempts}")?;
793 writeln!(file, "Final outcome: {final_outcome}")?;
794 writeln!(file)?;
795 writeln!(file, "Individual attempt logs are in this directory.")?;
796
797 Ok(())
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use tempfile::TempDir;
805
806 #[test]
807 fn test_attempt_log_creation() {
808 let log = CommitAttemptLog::new(1, "claude", "initial");
809 assert_eq!(log.attempt_number, 1);
810 assert_eq!(log.agent, "claude");
811 assert_eq!(log.strategy, "initial");
812 assert!(log.raw_output.is_none());
813 assert!(log.extraction_attempts.is_empty());
814 assert!(log.validation_checks.is_empty());
815 assert!(log.outcome.is_none());
816 }
817
818 #[test]
819 fn test_attempt_log_set_values() {
820 let mut log = CommitAttemptLog::new(2, "glm", "strict_json");
821
822 log.set_prompt_size(10_000);
823 log.set_diff_info(50_000, true);
824 log.set_raw_output("test output");
825
826 assert_eq!(log.prompt_size_bytes, 10_000);
827 assert_eq!(log.diff_size_bytes, 50_000);
828 assert!(log.diff_was_truncated);
829 assert_eq!(log.raw_output.as_deref(), Some("test output"));
830 }
831
832 #[test]
833 fn test_extraction_attempt_creation() {
834 let success =
835 ExtractionAttempt::success("XML", "Found <ralph-commit> at pos 0".to_string());
836 assert!(success.success);
837 assert_eq!(success.method, "XML");
838
839 let failure = ExtractionAttempt::failure("JSON", "No JSON found".to_string());
840 assert!(!failure.success);
841 assert_eq!(failure.method, "JSON");
842 }
843
844 #[test]
845 fn test_validation_check_creation() {
846 let pass = ValidationCheck::pass("basic_length");
847 assert!(pass.passed);
848 assert!(pass.error.is_none());
849
850 let fail = ValidationCheck::fail("no_json_artifacts", "Found JSON in message".to_string());
851 assert!(!fail.passed);
852 assert!(fail.error.is_some());
853 }
854
855 #[test]
856 fn test_outcome_display() {
857 let success = AttemptOutcome::Success("feat: add feature".to_string());
858 assert!(format!("{success}").contains("SUCCESS"));
859
860 let error = AttemptOutcome::AgentError("token limit exceeded".to_string());
861 assert!(format!("{error}").contains("AGENT_ERROR"));
862 }
863
864 #[test]
865 fn test_write_log_to_file() {
866 let temp_dir = TempDir::new().unwrap();
867 let log_dir = temp_dir.path();
868
869 let mut log = CommitAttemptLog::new(1, "claude", "initial");
870 log.set_prompt_size(5000);
871 log.set_diff_info(10000, false);
872 log.set_raw_output("raw agent output here");
873 log.add_extraction_attempt(ExtractionAttempt::failure(
874 "XML",
875 "No <ralph-commit> tag found".to_string(),
876 ));
877 log.add_extraction_attempt(ExtractionAttempt::success(
878 "JSON",
879 "Extracted from JSON".to_string(),
880 ));
881 log.set_validation_checks(vec![
882 ValidationCheck::pass("basic_length"),
883 ValidationCheck::fail("no_bad_patterns", "File list pattern detected".to_string()),
884 ]);
885 log.set_outcome(AttemptOutcome::ExtractionFailed("bad pattern".to_string()));
886
887 let log_path = log.write_to_file(log_dir).unwrap();
888 assert!(log_path.exists());
889
890 let content = fs::read_to_string(&log_path).unwrap();
891 assert!(content.contains("COMMIT GENERATION ATTEMPT LOG"));
892 assert!(content.contains("Attempt: #1"));
893 assert!(content.contains("claude"));
894 assert!(content.contains("EXTRACTION ATTEMPTS"));
895 assert!(content.contains("✗ FAILED"));
896 assert!(content.contains("✓ SUCCESS"));
897 assert!(content.contains("VALIDATION RESULTS"));
898 assert!(content.contains("OUTCOME"));
899 }
900
901 #[test]
902 fn test_session_creates_run_directory() {
903 let temp_dir = TempDir::new().unwrap();
904 let base_dir = temp_dir.path().join("logs");
905
906 let session = CommitLogSession::new(base_dir.to_str().unwrap()).unwrap();
907 assert!(session.run_dir().exists());
908 assert!(session.run_dir().to_string_lossy().contains("run_"));
909 }
910
911 #[test]
912 fn test_session_increments_attempt_number() {
913 let temp_dir = TempDir::new().unwrap();
914 let base_dir = temp_dir.path().join("logs");
915
916 let mut session = CommitLogSession::new(base_dir.to_str().unwrap()).unwrap();
917
918 assert_eq!(session.next_attempt_number(), 1);
919 assert_eq!(session.next_attempt_number(), 2);
920 assert_eq!(session.next_attempt_number(), 3);
921 }
922
923 #[test]
924 fn test_session_new_attempt() {
925 let temp_dir = TempDir::new().unwrap();
926 let base_dir = temp_dir.path().join("logs");
927
928 let mut session = CommitLogSession::new(base_dir.to_str().unwrap()).unwrap();
929
930 let log1 = session.new_attempt("claude", "initial");
931 assert_eq!(log1.attempt_number, 1);
932
933 let log2 = session.new_attempt("glm", "strict_json");
934 assert_eq!(log2.attempt_number, 2);
935 }
936
937 #[test]
938 fn test_session_write_summary() {
939 let temp_dir = TempDir::new().unwrap();
940 let base_dir = temp_dir.path().join("logs");
941
942 let session = CommitLogSession::new(base_dir.to_str().unwrap()).unwrap();
943 session
944 .write_summary(5, "SUCCESS: feat: add feature")
945 .unwrap();
946
947 let summary_path = session.run_dir().join("SUMMARY.txt");
948 assert!(summary_path.exists());
949
950 let content = fs::read_to_string(&summary_path).unwrap();
951 assert!(content.contains("Total attempts: 5"));
952 assert!(content.contains("SUCCESS"));
953 }
954
955 #[test]
956 fn test_sanitize_agent_name() {
957 assert_eq!(sanitize_agent_name("claude"), "claude");
958 assert_eq!(sanitize_agent_name("agent/commit"), "agent_commit");
959 assert_eq!(sanitize_agent_name("my-agent-v2"), "my_agent_v2");
960 let long_name = "a".repeat(50);
962 assert_eq!(sanitize_agent_name(&long_name).len(), 20);
963 }
964
965 #[test]
966 fn test_large_output_truncation() {
967 let mut log = CommitAttemptLog::new(1, "test", "test");
968 let large_output = "x".repeat(100_000);
969 log.set_raw_output(&large_output);
970
971 let output = log.raw_output.unwrap();
972 assert!(output.len() < large_output.len());
973 assert!(output.contains("[... truncated"));
974 }
975
976 #[test]
977 fn test_parsing_trace_step_creation() {
978 let step = ParsingTraceStep::new(1, "XML extraction");
979 assert_eq!(step.step_number, 1);
980 assert_eq!(step.description, "XML extraction");
981 assert!(!step.success);
982 assert!(step.input.is_none());
983 assert!(step.result.is_none());
984 }
985
986 #[test]
987 fn test_parsing_trace_step_builder() {
988 let step = ParsingTraceStep::new(1, "XML extraction")
989 .with_input("input content")
990 .with_result("result content")
991 .with_success(true)
992 .with_details("extraction successful");
993
994 assert!(step.success);
995 assert_eq!(step.input.as_deref(), Some("input content"));
996 assert_eq!(step.result.as_deref(), Some("result content"));
997 assert_eq!(step.details, "extraction successful");
998 }
999
1000 #[test]
1001 fn test_parsing_trace_step_truncation() {
1002 let large_input = "x".repeat(100_000);
1003 let step = ParsingTraceStep::new(1, "test").with_input(&large_input);
1004
1005 assert!(step.input.is_some());
1006 let input = step.input.as_ref().unwrap();
1007 assert!(input.len() < large_input.len());
1008 assert!(input.contains("[... input truncated"));
1009 }
1010
1011 #[test]
1012 fn test_parsing_trace_log_creation() {
1013 let trace = ParsingTraceLog::new(1, "claude", "initial");
1014 assert_eq!(trace.attempt_number, 1);
1015 assert_eq!(trace.agent, "claude");
1016 assert_eq!(trace.strategy, "initial");
1017 assert!(trace.raw_output.is_none());
1018 assert!(trace.steps.is_empty());
1019 assert!(trace.final_message.is_none());
1020 }
1021
1022 #[test]
1023 fn test_parsing_trace_log_set_raw_output() {
1024 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1025 trace.set_raw_output("test output");
1026
1027 assert_eq!(trace.raw_output.as_deref(), Some("test output"));
1028 }
1029
1030 #[test]
1031 fn test_parsing_trace_raw_output_truncation() {
1032 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1033 let large_output = "x".repeat(100_000);
1034 trace.set_raw_output(&large_output);
1035
1036 let output = trace.raw_output.unwrap();
1037 assert!(output.len() < large_output.len());
1038 assert!(output.contains("[... raw output truncated"));
1039 }
1040
1041 #[test]
1042 fn test_parsing_trace_add_step() {
1043 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1044 let step = ParsingTraceStep::new(1, "XML extraction");
1045 trace.add_step(step);
1046
1047 assert_eq!(trace.steps.len(), 1);
1048 assert_eq!(trace.steps[0].description, "XML extraction");
1049 }
1050
1051 #[test]
1052 fn test_parsing_trace_set_final_message() {
1053 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1054 trace.set_final_message("feat: add feature");
1055
1056 assert_eq!(trace.final_message.as_deref(), Some("feat: add feature"));
1057 }
1058
1059 #[test]
1060 fn test_parsing_trace_write_to_file() {
1061 let temp_dir = TempDir::new().unwrap();
1062 let log_dir = temp_dir.path();
1063
1064 let mut trace = ParsingTraceLog::new(1, "claude", "initial");
1065 trace.set_raw_output("raw agent output");
1066 trace.add_step(
1067 ParsingTraceStep::new(1, "XML extraction")
1068 .with_input("input")
1069 .with_result("result")
1070 .with_success(true)
1071 .with_details("success"),
1072 );
1073 trace.add_step(
1074 ParsingTraceStep::new(2, "Validation")
1075 .with_success(false)
1076 .with_details("failed"),
1077 );
1078 trace.set_final_message("feat: add feature");
1079
1080 let trace_path = trace.write_to_file(log_dir).unwrap();
1081 assert!(trace_path.exists());
1082 assert!(trace_path.to_string_lossy().contains("parsing_trace"));
1083
1084 let content = fs::read_to_string(&trace_path).unwrap();
1085 assert!(content.contains("PARSING TRACE LOG"));
1086 assert!(content.contains("Attempt #001"));
1087 assert!(content.contains("RAW AGENT OUTPUT"));
1088 assert!(content.contains("PARSING STEPS"));
1089 assert!(content.contains("✓ SUCCESS"));
1090 assert!(content.contains("✗ FAILED"));
1091 assert!(content.contains("FINAL EXTRACTED MESSAGE"));
1092 }
1093}