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