1use ralph_proto::{Event, HatId};
10
11fn strip_ansi(s: &str) -> String {
16 let bytes = s.as_bytes();
17 let mut result = Vec::with_capacity(bytes.len());
18 let mut i = 0;
19
20 while i < bytes.len() {
21 if bytes[i] == 0x1b {
22 i += 1;
24 if i >= bytes.len() {
25 break;
26 }
27
28 match bytes[i] {
29 b'[' => {
30 i += 1;
32 while i < bytes.len() && !(0x40..=0x7E).contains(&bytes[i]) {
33 i += 1;
34 }
35 if i < bytes.len() {
36 i += 1; }
38 }
39 b']' => {
40 i += 1;
42 while i < bytes.len() {
43 if bytes[i] == 0x07 {
44 i += 1;
45 break;
46 }
47 if bytes[i] == 0x1b && i + 1 < bytes.len() && bytes[i + 1] == b'\\' {
48 i += 2;
49 break;
50 }
51 i += 1;
52 }
53 }
54 _ => {
55 i += 1;
57 }
58 }
59 } else {
60 result.push(bytes[i]);
61 i += 1;
62 }
63 }
64
65 String::from_utf8_lossy(&result).into_owned()
66}
67
68#[derive(Debug, Clone, PartialEq)]
70pub struct BackpressureEvidence {
71 pub tests_passed: bool,
72 pub lint_passed: bool,
73 pub typecheck_passed: bool,
74 pub audit_passed: bool,
75 pub coverage_passed: bool,
76 pub complexity_score: Option<f64>,
77 pub duplication_passed: bool,
78 pub performance_regression: Option<bool>,
79 pub mutants: Option<MutationEvidence>,
80 pub specs_verified: Option<bool>,
86}
87
88impl BackpressureEvidence {
89 pub fn all_passed(&self) -> bool {
95 self.tests_passed
96 && self.lint_passed
97 && self.typecheck_passed
98 && self.audit_passed
99 && self.coverage_passed
100 && self
101 .complexity_score
102 .is_some_and(|value| value <= QualityReport::COMPLEXITY_THRESHOLD)
103 && self.duplication_passed
104 && !matches!(self.performance_regression, Some(true))
105 && !matches!(self.specs_verified, Some(false))
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum MutationStatus {
112 Pass,
113 Warn,
114 Fail,
115 Unknown,
116}
117
118#[derive(Debug, Clone, PartialEq)]
120pub struct MutationEvidence {
121 pub status: MutationStatus,
122 pub score_percent: Option<f64>,
123}
124
125#[derive(Debug, Clone, PartialEq)]
130pub struct ReviewEvidence {
131 pub tests_passed: bool,
132 pub build_passed: bool,
133}
134
135impl ReviewEvidence {
136 pub fn is_verified(&self) -> bool {
140 self.tests_passed && self.build_passed
141 }
142}
143
144#[derive(Debug, Clone, PartialEq)]
146pub struct QualityReport {
147 pub tests_passed: Option<bool>,
148 pub lint_passed: Option<bool>,
149 pub audit_passed: Option<bool>,
150 pub coverage_percent: Option<f64>,
151 pub mutation_percent: Option<f64>,
152 pub complexity_score: Option<f64>,
153 pub specs_verified: Option<bool>,
158}
159
160impl QualityReport {
161 pub const COVERAGE_THRESHOLD: f64 = 80.0;
162 pub const MUTATION_THRESHOLD: f64 = 70.0;
163 pub const COMPLEXITY_THRESHOLD: f64 = 10.0;
164
165 pub fn meets_thresholds(&self) -> bool {
166 self.tests_passed == Some(true)
167 && self.lint_passed == Some(true)
168 && self.audit_passed == Some(true)
169 && self
170 .coverage_percent
171 .is_some_and(|value| value >= Self::COVERAGE_THRESHOLD)
172 && self
173 .mutation_percent
174 .is_some_and(|value| value >= Self::MUTATION_THRESHOLD)
175 && self
176 .complexity_score
177 .is_some_and(|value| value <= Self::COMPLEXITY_THRESHOLD)
178 && !matches!(self.specs_verified, Some(false))
179 }
180
181 pub fn failed_dimensions(&self) -> Vec<&'static str> {
182 let mut failed = Vec::new();
183
184 if self.tests_passed != Some(true) {
185 failed.push("tests");
186 }
187 if self.lint_passed != Some(true) {
188 failed.push("lint");
189 }
190 if self.audit_passed != Some(true) {
191 failed.push("audit");
192 }
193 if self
194 .coverage_percent
195 .is_none_or(|value| value < Self::COVERAGE_THRESHOLD)
196 {
197 failed.push("coverage");
198 }
199 if self
200 .mutation_percent
201 .is_none_or(|value| value < Self::MUTATION_THRESHOLD)
202 {
203 failed.push("mutation");
204 }
205 if self
206 .complexity_score
207 .is_none_or(|value| value > Self::COMPLEXITY_THRESHOLD)
208 {
209 failed.push("complexity");
210 }
211 if matches!(self.specs_verified, Some(false)) {
212 failed.push("specs");
213 }
214
215 failed
216 }
217}
218
219#[derive(Debug, Default)]
221pub struct EventParser {
222 source: Option<HatId>,
224}
225
226impl EventParser {
227 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn with_source(mut self, source: impl Into<HatId>) -> Self {
234 self.source = Some(source.into());
235 self
236 }
237
238 pub fn parse(&self, output: &str) -> Vec<Event> {
242 let mut events = Vec::new();
243 let mut remaining = output;
244
245 while let Some(start_idx) = remaining.find("<event ") {
246 let after_start = &remaining[start_idx..];
247
248 let Some(tag_end) = after_start.find('>') else {
250 remaining = &remaining[start_idx + 7..];
251 continue;
252 };
253
254 let opening_tag = &after_start[..tag_end + 1];
255
256 let topic = Self::extract_attr(opening_tag, "topic");
258 let target = Self::extract_attr(opening_tag, "target");
259
260 let Some(topic) = topic else {
261 remaining = &remaining[start_idx + tag_end + 1..];
262 continue;
263 };
264
265 let content_start = &after_start[tag_end + 1..];
267 let Some(close_idx) = content_start.find("</event>") else {
268 remaining = &remaining[start_idx + tag_end + 1..];
269 continue;
270 };
271
272 let payload = content_start[..close_idx].trim().to_string();
273
274 let mut event = Event::new(topic, payload);
275
276 if let Some(source) = &self.source {
277 event = event.with_source(source.clone());
278 }
279
280 if let Some(target) = target {
281 event = event.with_target(target);
282 }
283
284 events.push(event);
285
286 let total_consumed = start_idx + tag_end + 1 + close_idx + 8; remaining = &remaining[total_consumed..];
289 }
290
291 events
292 }
293
294 fn extract_attr(tag: &str, attr: &str) -> Option<String> {
296 let pattern = format!("{attr}=\"");
297 let start = tag.find(&pattern)?;
298 let value_start = start + pattern.len();
299 let rest = &tag[value_start..];
300 let end = rest.find('"')?;
301 Some(rest[..end].to_string())
302 }
303
304 pub fn parse_backpressure_evidence(payload: &str) -> Option<BackpressureEvidence> {
323 let clean_payload = strip_ansi(payload);
325
326 let tests_passed = clean_payload.contains("tests: pass");
327 let lint_passed = clean_payload.contains("lint: pass");
328 let typecheck_passed = clean_payload.contains("typecheck: pass");
329 let audit_passed = clean_payload.contains("audit: pass");
330 let coverage_passed = clean_payload.contains("coverage: pass");
331 let complexity_score = Self::parse_complexity_evidence(&clean_payload);
332 let duplication_passed = Self::parse_duplication_evidence(&clean_payload).unwrap_or(false);
333 let performance_regression = Self::parse_performance_regression(&clean_payload);
334 let mutants = Self::parse_mutation_evidence(&clean_payload);
335 let specs_verified = Self::parse_specs_evidence(&clean_payload);
336
337 if clean_payload.contains("tests:")
339 || clean_payload.contains("lint:")
340 || clean_payload.contains("typecheck:")
341 || clean_payload.contains("audit:")
342 || clean_payload.contains("coverage:")
343 || clean_payload.contains("complexity:")
344 || clean_payload.contains("duplication:")
345 || clean_payload.contains("performance:")
346 || clean_payload.contains("perf:")
347 || clean_payload.contains("mutants:")
348 || clean_payload.contains("specs:")
349 {
350 Some(BackpressureEvidence {
351 tests_passed,
352 lint_passed,
353 typecheck_passed,
354 audit_passed,
355 coverage_passed,
356 complexity_score,
357 duplication_passed,
358 performance_regression,
359 mutants,
360 specs_verified,
361 })
362 } else {
363 None
364 }
365 }
366
367 fn parse_mutation_evidence(clean_payload: &str) -> Option<MutationEvidence> {
368 let segment = clean_payload
369 .split(|c| c == '\n' || c == ',')
370 .map(str::trim)
371 .find(|segment| segment.contains("mutants:"))?;
372
373 let normalized = segment.to_lowercase();
374 let status = if normalized.contains("mutants: pass") {
375 MutationStatus::Pass
376 } else if normalized.contains("mutants: warn") {
377 MutationStatus::Warn
378 } else if normalized.contains("mutants: fail") {
379 MutationStatus::Fail
380 } else {
381 MutationStatus::Unknown
382 };
383
384 let score_percent = Self::extract_percentage(segment);
385
386 Some(MutationEvidence {
387 status,
388 score_percent,
389 })
390 }
391
392 fn parse_complexity_evidence(clean_payload: &str) -> Option<f64> {
393 let segment = clean_payload
394 .split(|c| c == '\n' || c == ',')
395 .map(str::trim)
396 .find(|segment| segment.to_lowercase().starts_with("complexity:"))?;
397
398 Self::extract_first_number(segment)
399 }
400
401 fn parse_duplication_evidence(clean_payload: &str) -> Option<bool> {
402 let segment = clean_payload
403 .split(|c| c == '\n' || c == ',')
404 .map(str::trim)
405 .find(|segment| segment.to_lowercase().starts_with("duplication:"))?;
406
407 let normalized = segment.to_lowercase();
408 if normalized.contains("duplication: pass") {
409 Some(true)
410 } else if normalized.contains("duplication: fail") {
411 Some(false)
412 } else {
413 None
414 }
415 }
416
417 fn parse_performance_regression(clean_payload: &str) -> Option<bool> {
418 let segment = clean_payload
419 .split(|c| c == '\n' || c == ',')
420 .map(str::trim)
421 .find(|segment| {
422 let normalized = segment.to_lowercase();
423 normalized.starts_with("performance:") || normalized.starts_with("perf:")
424 })?;
425
426 let normalized = segment.to_lowercase();
427 if normalized.contains("regression") || normalized.contains("fail") {
428 Some(true)
429 } else if normalized.contains("pass")
430 || normalized.contains("ok")
431 || normalized.contains("improved")
432 {
433 Some(false)
434 } else {
435 None
436 }
437 }
438
439 fn parse_specs_evidence(clean_payload: &str) -> Option<bool> {
444 let segment = clean_payload
445 .split(|c| c == '\n' || c == ',')
446 .map(str::trim)
447 .find(|segment| segment.to_lowercase().starts_with("specs:"))?;
448
449 let normalized = segment.to_lowercase();
450 if normalized.contains("specs: pass") {
451 Some(true)
452 } else if normalized.contains("specs: fail") {
453 Some(false)
454 } else {
455 None
456 }
457 }
458
459 fn extract_percentage(segment: &str) -> Option<f64> {
460 let percent_idx = segment.find('%')?;
461 let bytes = segment.as_bytes();
462 let mut start = percent_idx;
463
464 while start > 0 {
465 let prev = bytes[start - 1];
466 if prev.is_ascii_digit() || prev == b'.' {
467 start -= 1;
468 } else {
469 break;
470 }
471 }
472
473 if start == percent_idx {
474 return None;
475 }
476
477 segment[start..percent_idx].trim().parse::<f64>().ok()
478 }
479
480 fn extract_first_number(segment: &str) -> Option<f64> {
481 let bytes = segment.as_bytes();
482 let mut start = None;
483 let mut end = None;
484
485 for (idx, &byte) in bytes.iter().enumerate() {
486 if byte.is_ascii_digit() {
487 if start.is_none() {
488 start = Some(idx);
489 }
490 end = Some(idx + 1);
491 } else if byte == b'.' && start.is_some() {
492 end = Some(idx + 1);
493 } else if start.is_some() {
494 break;
495 }
496 }
497
498 let start = start?;
499 let end = end?;
500 segment[start..end].trim().parse::<f64>().ok()
501 }
502
503 fn parse_quality_pass_fail(segment: &str) -> Option<bool> {
504 if segment.contains("pass") {
505 Some(true)
506 } else if segment.contains("fail") {
507 Some(false)
508 } else {
509 None
510 }
511 }
512
513 pub fn parse_review_evidence(payload: &str) -> Option<ReviewEvidence> {
523 let clean_payload = strip_ansi(payload);
524
525 let tests_passed = clean_payload.contains("tests: pass");
526 let build_passed = clean_payload.contains("build: pass");
527
528 if clean_payload.contains("tests:") || clean_payload.contains("build:") {
530 Some(ReviewEvidence {
531 tests_passed,
532 build_passed,
533 })
534 } else {
535 None
536 }
537 }
538
539 pub fn parse_quality_report(payload: &str) -> Option<QualityReport> {
554 let clean_payload = strip_ansi(payload);
555 let mut report = QualityReport {
556 tests_passed: None,
557 lint_passed: None,
558 audit_passed: None,
559 coverage_percent: None,
560 mutation_percent: None,
561 complexity_score: None,
562 specs_verified: None,
563 };
564 let mut seen = false;
565
566 for segment in clean_payload
567 .split(|c| c == '\n' || c == ',')
568 .map(str::trim)
569 {
570 if segment.is_empty() {
571 continue;
572 }
573 let normalized = segment.to_lowercase();
574
575 if normalized.starts_with("quality.tests:") {
576 report.tests_passed = Self::parse_quality_pass_fail(&normalized);
577 seen = true;
578 } else if normalized.starts_with("quality.lint:") {
579 report.lint_passed = Self::parse_quality_pass_fail(&normalized);
580 seen = true;
581 } else if normalized.starts_with("quality.audit:") {
582 report.audit_passed = Self::parse_quality_pass_fail(&normalized);
583 seen = true;
584 } else if normalized.starts_with("quality.coverage:") {
585 report.coverage_percent = Self::extract_percentage(segment)
586 .or_else(|| Self::extract_first_number(segment));
587 seen = true;
588 } else if normalized.starts_with("quality.mutation:") {
589 report.mutation_percent = Self::extract_percentage(segment)
590 .or_else(|| Self::extract_first_number(segment));
591 seen = true;
592 } else if normalized.starts_with("quality.complexity:") {
593 report.complexity_score = Self::extract_first_number(segment);
594 seen = true;
595 } else if normalized.starts_with("quality.specs:") {
596 report.specs_verified = Self::parse_quality_pass_fail(&normalized);
597 seen = true;
598 }
599 }
600
601 if seen { Some(report) } else { None }
602 }
603
604 pub fn contains_promise(output: &str, promise: &str) -> bool {
613 let promise = promise.trim();
614 if promise.is_empty() {
615 return false;
616 }
617
618 if Self::promise_in_event_tags(output, promise) {
620 return false;
621 }
622 let stripped = Self::strip_event_tags(output);
623
624 for line in stripped.lines().rev() {
625 let trimmed = line.trim();
626 if trimmed.is_empty() {
627 continue;
628 }
629 return trimmed == promise;
630 }
631
632 false
633 }
634
635 pub fn promise_in_event_tags(output: &str, promise: &str) -> bool {
637 let mut remaining = output;
638
639 while let Some(start_idx) = remaining.find("<event ") {
640 let after_start = &remaining[start_idx..];
641
642 let Some(tag_end) = after_start.find('>') else {
644 remaining = &remaining[start_idx + 7..];
645 continue;
646 };
647
648 let content_start = &after_start[tag_end + 1..];
650 let Some(close_idx) = content_start.find("</event>") else {
651 remaining = &remaining[start_idx + tag_end + 1..];
652 continue;
653 };
654
655 let payload = &content_start[..close_idx];
656 if payload.contains(promise) {
657 return true;
658 }
659
660 let total_consumed = start_idx + tag_end + 1 + close_idx + 8;
662 remaining = &remaining[total_consumed..];
663 }
664
665 false
666 }
667
668 fn strip_event_tags(output: &str) -> String {
673 let mut result = String::with_capacity(output.len());
674 let mut remaining = output;
675
676 while let Some(start_idx) = remaining.find("<event ") {
677 result.push_str(&remaining[..start_idx]);
679
680 let after_start = &remaining[start_idx..];
681
682 if let Some(close_idx) = after_start.find("</event>") {
684 remaining = &after_start[close_idx + 8..]; } else {
687 result.push_str(after_start);
689 remaining = "";
690 break;
691 }
692 }
693
694 result.push_str(remaining);
696 result
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703
704 #[test]
705 fn test_parse_single_event() {
706 let output = r#"
707Some preamble text.
708<event topic="impl.done">
709Implemented the authentication module.
710</event>
711Some trailing text.
712"#;
713 let parser = EventParser::new();
714 let events = parser.parse(output);
715
716 assert_eq!(events.len(), 1);
717 assert_eq!(events[0].topic.as_str(), "impl.done");
718 assert!(events[0].payload.contains("authentication module"));
719 }
720
721 #[test]
722 fn test_parse_event_with_target() {
723 let output = r#"<event topic="handoff" target="reviewer">Please review</event>"#;
724 let parser = EventParser::new();
725 let events = parser.parse(output);
726
727 assert_eq!(events.len(), 1);
728 assert_eq!(events[0].target.as_ref().unwrap().as_str(), "reviewer");
729 }
730
731 #[test]
732 fn test_parse_multiple_events() {
733 let output = r#"
734<event topic="impl.started">Starting work</event>
735Working on implementation...
736<event topic="impl.done">Finished</event>
737"#;
738 let parser = EventParser::new();
739 let events = parser.parse(output);
740
741 assert_eq!(events.len(), 2);
742 assert_eq!(events[0].topic.as_str(), "impl.started");
743 assert_eq!(events[1].topic.as_str(), "impl.done");
744 }
745
746 #[test]
747 fn test_parse_with_source() {
748 let output = r#"<event topic="impl.done">Done</event>"#;
749 let parser = EventParser::new().with_source("implementer");
750 let events = parser.parse(output);
751
752 assert_eq!(events[0].source.as_ref().unwrap().as_str(), "implementer");
753 }
754
755 #[test]
756 fn test_no_events() {
757 let output = "Just regular output with no events.";
758 let parser = EventParser::new();
759 let events = parser.parse(output);
760
761 assert!(events.is_empty());
762 }
763
764 #[test]
765 fn test_contains_promise_requires_last_line() {
766 assert!(EventParser::contains_promise(
767 "LOOP_COMPLETE",
768 "LOOP_COMPLETE"
769 ));
770 assert!(EventParser::contains_promise(
771 "All done!\nLOOP_COMPLETE",
772 "LOOP_COMPLETE"
773 ));
774 assert!(EventParser::contains_promise(
775 "LOOP_COMPLETE \n\n",
776 "LOOP_COMPLETE"
777 ));
778 assert!(!EventParser::contains_promise(
779 "prefix LOOP_COMPLETE suffix",
780 "LOOP_COMPLETE"
781 ));
782 assert!(!EventParser::contains_promise(
783 "LOOP_COMPLETE\nMore text",
784 "LOOP_COMPLETE"
785 ));
786 assert!(!EventParser::contains_promise("Any output", " "));
787 assert!(!EventParser::contains_promise(
788 "No promise here",
789 "LOOP_COMPLETE"
790 ));
791 }
792
793 #[test]
794 fn test_contains_promise_ignores_event_payloads() {
795 let output = r#"<event topic="build.task">Fix LOOP_COMPLETE detection</event>"#;
797 assert!(!EventParser::contains_promise(output, "LOOP_COMPLETE"));
798
799 let output = r#"<event topic="build.task">
801## Task: Fix completion promise detection
802- Given LOOP_COMPLETE appears inside an event tag
803- Then it should be ignored
804</event>"#;
805 assert!(!EventParser::contains_promise(output, "LOOP_COMPLETE"));
806 }
807
808 #[test]
809 fn test_contains_promise_detects_outside_events() {
810 let output = r#"<event topic="build.done">Task complete</event>
812All done!
813LOOP_COMPLETE"#;
814 assert!(EventParser::contains_promise(output, "LOOP_COMPLETE"));
815
816 let output = r#"LOOP_COMPLETE
818<event topic="summary">Final summary</event>"#;
819 assert!(EventParser::contains_promise(output, "LOOP_COMPLETE"));
820 }
821
822 #[test]
823 fn test_contains_promise_mixed_content() {
824 let output = r#"Working on task...
826<event topic="build.task">Fix LOOP_COMPLETE bug</event>
827Still working..."#;
828 assert!(!EventParser::contains_promise(output, "LOOP_COMPLETE"));
829
830 let output = r#"All tasks done. LOOP_COMPLETE
833<event topic="summary">Completed LOOP_COMPLETE task</event>"#;
834 assert!(!EventParser::contains_promise(output, "LOOP_COMPLETE"));
835 }
836
837 #[test]
838 fn test_promise_in_event_tags() {
839 let output = r#"<event topic="build.task">Fix LOOP_COMPLETE bug</event>"#;
841 assert!(EventParser::promise_in_event_tags(output, "LOOP_COMPLETE"));
842
843 let output = r#"<event topic="build.done">Task complete</event>"#;
845 assert!(!EventParser::promise_in_event_tags(output, "LOOP_COMPLETE"));
846
847 let output = "Just regular text with LOOP_COMPLETE";
849 assert!(!EventParser::promise_in_event_tags(output, "LOOP_COMPLETE"));
850
851 let output = r#"<event topic="a">first</event>
853<event topic="b">contains LOOP_COMPLETE</event>"#;
854 assert!(EventParser::promise_in_event_tags(output, "LOOP_COMPLETE"));
855 }
856
857 #[test]
858 fn test_strip_event_tags() {
859 let output = r#"before <event topic="test">payload</event> after"#;
861 let stripped = EventParser::strip_event_tags(output);
862 assert_eq!(stripped, "before after");
863 assert!(!stripped.contains("payload"));
864
865 let output =
867 r#"start <event topic="a">one</event> middle <event topic="b">two</event> end"#;
868 let stripped = EventParser::strip_event_tags(output);
869 assert_eq!(stripped, "start middle end");
870
871 let output = "just plain text";
873 let stripped = EventParser::strip_event_tags(output);
874 assert_eq!(stripped, "just plain text");
875 }
876
877 #[test]
878 fn test_parse_backpressure_evidence_all_pass() {
879 let payload = "tests: pass\nlint: pass\ntypecheck: pass\naudit: pass\ncoverage: pass\ncomplexity: 7\nduplication: pass\nperformance: pass";
880 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
881 assert!(evidence.tests_passed);
882 assert!(evidence.lint_passed);
883 assert!(evidence.typecheck_passed);
884 assert!(evidence.audit_passed);
885 assert!(evidence.coverage_passed);
886 assert_eq!(evidence.complexity_score, Some(7.0));
887 assert!(evidence.duplication_passed);
888 assert_eq!(evidence.performance_regression, Some(false));
889 assert!(evidence.all_passed());
890 }
891
892 #[test]
893 fn test_parse_backpressure_evidence_some_fail() {
894 let payload = "tests: pass\nlint: fail\ntypecheck: pass\naudit: pass\ncoverage: pass\ncomplexity: 7\nduplication: pass\nperformance: pass";
895 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
896 assert!(evidence.tests_passed);
897 assert!(!evidence.lint_passed);
898 assert!(evidence.typecheck_passed);
899 assert!(evidence.audit_passed);
900 assert!(evidence.coverage_passed);
901 assert_eq!(evidence.complexity_score, Some(7.0));
902 assert!(evidence.duplication_passed);
903 assert_eq!(evidence.performance_regression, Some(false));
904 assert!(!evidence.all_passed());
905 }
906
907 #[test]
908 fn test_parse_backpressure_evidence_missing() {
909 let payload = "Task completed successfully";
910 let evidence = EventParser::parse_backpressure_evidence(payload);
911 assert!(evidence.is_none());
912 }
913
914 #[test]
915 fn test_parse_backpressure_evidence_partial() {
916 let payload = "tests: pass\nSome other text";
917 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
918 assert!(evidence.tests_passed);
919 assert!(!evidence.lint_passed);
920 assert!(!evidence.typecheck_passed);
921 assert!(!evidence.audit_passed);
922 assert!(!evidence.coverage_passed);
923 assert!(evidence.complexity_score.is_none());
924 assert!(!evidence.duplication_passed);
925 assert!(evidence.performance_regression.is_none());
926 assert!(!evidence.all_passed());
927 }
928
929 #[test]
930 fn test_parse_backpressure_evidence_with_ansi_codes() {
931 let payload = "\x1b[0mtests: pass\x1b[0m\n\x1b[32mlint: pass\x1b[0m\ntypecheck: pass\n\x1b[34maudit: pass\x1b[0m\n\x1b[35mcoverage: pass\x1b[0m\n\x1b[36mcomplexity: 7\x1b[0m\n\x1b[31mduplication: pass\x1b[0m\n\x1b[33mperformance: pass\x1b[0m";
932 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
933 assert!(evidence.tests_passed);
934 assert!(evidence.lint_passed);
935 assert!(evidence.typecheck_passed);
936 assert!(evidence.audit_passed);
937 assert!(evidence.coverage_passed);
938 assert_eq!(evidence.complexity_score, Some(7.0));
939 assert!(evidence.duplication_passed);
940 assert_eq!(evidence.performance_regression, Some(false));
941 assert!(evidence.all_passed());
942 }
943
944 #[test]
945 fn test_parse_backpressure_evidence_with_mutants_pass() {
946 let payload = "tests: pass\nlint: pass\ntypecheck: pass\naudit: pass\ncoverage: pass\ncomplexity: 7\nduplication: pass\nperformance: pass\nmutants: pass (82%)";
947 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
948 let mutants = evidence
949 .mutants
950 .as_ref()
951 .expect("mutants evidence should parse");
952 assert_eq!(mutants.status, MutationStatus::Pass);
953 assert_eq!(mutants.score_percent, Some(82.0));
954 assert_eq!(evidence.performance_regression, Some(false));
955 assert!(evidence.all_passed());
956 }
957
958 #[test]
959 fn test_parse_backpressure_evidence_with_mutants_warn() {
960 let payload = "tests: pass, lint: pass, typecheck: pass, audit: pass, coverage: pass, complexity: 7, duplication: pass, performance: pass, mutants: warn (65%)";
961 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
962 let mutants = evidence
963 .mutants
964 .as_ref()
965 .expect("mutants evidence should parse");
966 assert_eq!(mutants.status, MutationStatus::Warn);
967 assert_eq!(mutants.score_percent, Some(65.0));
968 assert_eq!(evidence.performance_regression, Some(false));
969 assert!(evidence.all_passed());
970 }
971
972 #[test]
973 fn test_parse_backpressure_evidence_with_performance_regression() {
974 let payload = "tests: pass\nlint: pass\ntypecheck: pass\naudit: pass\ncoverage: pass\ncomplexity: 7\nduplication: pass\nperformance: regression";
975 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
976 assert_eq!(evidence.performance_regression, Some(true));
977 assert!(!evidence.all_passed());
978 }
979
980 #[test]
981 fn test_parse_review_evidence_all_pass() {
982 let payload = "tests: pass\nbuild: pass";
983 let evidence = EventParser::parse_review_evidence(payload).unwrap();
984 assert!(evidence.tests_passed);
985 assert!(evidence.build_passed);
986 assert!(evidence.is_verified());
987 }
988
989 #[test]
990 fn test_parse_review_evidence_tests_fail() {
991 let payload = "tests: fail\nbuild: pass";
992 let evidence = EventParser::parse_review_evidence(payload).unwrap();
993 assert!(!evidence.tests_passed);
994 assert!(evidence.build_passed);
995 assert!(!evidence.is_verified());
996 }
997
998 #[test]
999 fn test_parse_review_evidence_build_fail() {
1000 let payload = "tests: pass\nbuild: fail";
1001 let evidence = EventParser::parse_review_evidence(payload).unwrap();
1002 assert!(evidence.tests_passed);
1003 assert!(!evidence.build_passed);
1004 assert!(!evidence.is_verified());
1005 }
1006
1007 #[test]
1008 fn test_parse_review_evidence_missing() {
1009 let payload = "Looks good, approved!";
1010 let evidence = EventParser::parse_review_evidence(payload);
1011 assert!(evidence.is_none());
1012 }
1013
1014 #[test]
1015 fn test_parse_review_evidence_partial() {
1016 let payload = "tests: pass\nLGTM";
1017 let evidence = EventParser::parse_review_evidence(payload).unwrap();
1018 assert!(evidence.tests_passed);
1019 assert!(!evidence.build_passed);
1020 assert!(!evidence.is_verified());
1021 }
1022
1023 #[test]
1024 fn test_parse_review_evidence_with_ansi_codes() {
1025 let payload = "\x1b[32mtests: pass\x1b[0m\n\x1b[32mbuild: pass\x1b[0m";
1026 let evidence = EventParser::parse_review_evidence(payload).unwrap();
1027 assert!(evidence.tests_passed);
1028 assert!(evidence.build_passed);
1029 assert!(evidence.is_verified());
1030 }
1031
1032 #[test]
1033 fn test_parse_quality_report_passes_thresholds() {
1034 let payload = "quality.tests: pass\nquality.coverage: 82% (>=80%)\nquality.lint: pass\nquality.audit: pass\nquality.mutation: 71% (>=70%)\nquality.complexity: 7 (<=10)";
1035 let report = EventParser::parse_quality_report(payload).unwrap();
1036 assert_eq!(report.tests_passed, Some(true));
1037 assert_eq!(report.lint_passed, Some(true));
1038 assert_eq!(report.audit_passed, Some(true));
1039 assert_eq!(report.coverage_percent, Some(82.0));
1040 assert_eq!(report.mutation_percent, Some(71.0));
1041 assert_eq!(report.complexity_score, Some(7.0));
1042 assert!(report.meets_thresholds());
1043 }
1044
1045 #[test]
1046 fn test_parse_quality_report_fails_thresholds() {
1047 let payload = "quality.tests: pass\nquality.coverage: 60%\nquality.lint: fail\nquality.audit: pass\nquality.mutation: 50%\nquality.complexity: 12";
1048 let report = EventParser::parse_quality_report(payload).unwrap();
1049 assert!(!report.meets_thresholds());
1050 }
1051
1052 #[test]
1053 fn test_parse_quality_report_missing() {
1054 let payload = "Looks good, approved!";
1055 let report = EventParser::parse_quality_report(payload);
1056 assert!(report.is_none());
1057 }
1058
1059 #[test]
1060 fn test_extract_first_number_quality_line() {
1061 let value = EventParser::extract_first_number("quality.complexity: 7 (<=10)");
1062 assert_eq!(value, Some(7.0));
1063 }
1064
1065 #[test]
1066 fn test_parse_backpressure_evidence_with_specs_pass() {
1067 let payload = "tests: pass\nlint: pass\ntypecheck: pass\naudit: pass\ncoverage: pass\ncomplexity: 7\nduplication: pass\nperformance: pass\nspecs: pass";
1068 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1069 assert_eq!(evidence.specs_verified, Some(true));
1070 assert!(evidence.all_passed());
1071 }
1072
1073 #[test]
1074 fn test_parse_backpressure_evidence_with_specs_fail() {
1075 let payload = "tests: pass\nlint: pass\ntypecheck: pass\naudit: pass\ncoverage: pass\ncomplexity: 7\nduplication: pass\nperformance: pass\nspecs: fail";
1076 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1077 assert_eq!(evidence.specs_verified, Some(false));
1078 assert!(
1079 !evidence.all_passed(),
1080 "specs: fail should block build.done"
1081 );
1082 }
1083
1084 #[test]
1085 fn test_parse_backpressure_evidence_specs_omitted_does_not_block() {
1086 let payload = "tests: pass\nlint: pass\ntypecheck: pass\naudit: pass\ncoverage: pass\ncomplexity: 7\nduplication: pass\nperformance: pass";
1088 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1089 assert_eq!(evidence.specs_verified, None);
1090 assert!(
1091 evidence.all_passed(),
1092 "missing specs should not block build.done"
1093 );
1094 }
1095
1096 #[test]
1097 fn test_parse_backpressure_evidence_specs_comma_separated() {
1098 let payload = "tests: pass, lint: pass, typecheck: pass, audit: pass, coverage: pass, complexity: 7, duplication: pass, performance: pass, specs: pass";
1099 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1100 assert_eq!(evidence.specs_verified, Some(true));
1101 assert!(evidence.all_passed());
1102 }
1103
1104 #[test]
1105 fn test_parse_specs_evidence_only() {
1106 let payload = "specs: pass";
1108 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1109 assert_eq!(evidence.specs_verified, Some(true));
1110 }
1111
1112 #[test]
1113 fn test_quality_report_with_specs_pass() {
1114 let payload = "quality.tests: pass\nquality.coverage: 82%\nquality.lint: pass\nquality.audit: pass\nquality.mutation: 71%\nquality.complexity: 7\nquality.specs: pass";
1115 let report = EventParser::parse_quality_report(payload).unwrap();
1116 assert_eq!(report.specs_verified, Some(true));
1117 assert!(report.meets_thresholds());
1118 }
1119
1120 #[test]
1121 fn test_quality_report_with_specs_fail() {
1122 let payload = "quality.tests: pass\nquality.coverage: 82%\nquality.lint: pass\nquality.audit: pass\nquality.mutation: 71%\nquality.complexity: 7\nquality.specs: fail";
1123 let report = EventParser::parse_quality_report(payload).unwrap();
1124 assert_eq!(report.specs_verified, Some(false));
1125 assert!(
1126 !report.meets_thresholds(),
1127 "specs: fail should fail quality thresholds"
1128 );
1129 assert!(report.failed_dimensions().contains(&"specs"));
1130 }
1131
1132 #[test]
1133 fn test_quality_report_specs_omitted_passes() {
1134 let payload = "quality.tests: pass\nquality.coverage: 82%\nquality.lint: pass\nquality.audit: pass\nquality.mutation: 71%\nquality.complexity: 7";
1135 let report = EventParser::parse_quality_report(payload).unwrap();
1136 assert_eq!(report.specs_verified, None);
1137 assert!(
1138 report.meets_thresholds(),
1139 "missing specs should not fail quality thresholds"
1140 );
1141 assert!(!report.failed_dimensions().contains(&"specs"));
1142 }
1143
1144 #[test]
1145 fn test_strip_ansi_function() {
1146 let payload = "\x1b[0mtests: pass\x1b[0m";
1149 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1150 assert!(evidence.tests_passed);
1151
1152 let payload = "\x1b[1m\x1b[32mtests: pass\x1b[0m";
1154 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1155 assert!(evidence.tests_passed);
1156
1157 let payload = "\x1b[31mtests: fail\x1b[0m\n\x1b[32mlint: pass\x1b[0m";
1159 let evidence = EventParser::parse_backpressure_evidence(payload).unwrap();
1160 assert!(!evidence.tests_passed); assert!(evidence.lint_passed);
1162 assert!(!evidence.coverage_passed);
1163 }
1164}