#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AcceptanceResult {
Pass,
Fail { findings: Vec<String> },
Continue,
Gated,
}
pub(crate) const CANONICAL_VERDICTS: &[(&str, &str)] = &[
("ACCEPTANCE: PASS", "pass"),
("ACCEPTANCE: FAIL", "fail"),
("ACCEPTANCE: CONTINUE", "continue"),
("ACCEPTANCE: GATED", "gated"),
("ACCEPTANCE: BLOCKED", "gated"),
];
pub(crate) fn parse_json_verdict(text: &str) -> Option<(&'static str, Vec<String>)> {
let trimmed = text.trim();
if !trimmed.starts_with('{') || !trimmed.ends_with('}') {
return None;
}
let value: serde_json::Value = serde_json::from_str(trimmed).ok()?;
let obj = value.as_object()?;
let raw_kind = obj.get("acceptance")?.as_str()?;
let kind = match raw_kind.trim().to_ascii_lowercase().as_str() {
"pass" => "pass",
"fail" => "fail",
"continue" => "continue",
"gated" => "gated",
"blocked" => "gated",
_ => return None,
};
let findings = obj
.get("findings")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Vec<_>>()
})
.unwrap_or_default();
Some((kind, findings))
}
pub fn detect_verdict_in_line(line: &str) -> Option<&'static str> {
if let Some((kind, _)) = parse_json_verdict(line) {
return Some(kind);
}
if let Some(text) = crate::stream_json_textifier::extract_text_from_stream_json(line) {
for inner in text.lines() {
if let Some((kind, _)) = parse_json_verdict(inner) {
return Some(kind);
}
}
for inner in text.lines() {
if let Some(kind) = canonical_verdict_kind(inner) {
return Some(kind);
}
}
return None;
}
canonical_verdict_kind(line)
}
pub(crate) fn canonical_verdict_kind(line: &str) -> Option<&'static str> {
let normalized = strip_markdown_decorations(line.trim());
CANONICAL_VERDICTS
.iter()
.find(|(marker, _)| normalized == *marker)
.map(|(_, kind)| *kind)
}
pub fn parse_acceptance_output(output: &str) -> AcceptanceResult {
let mut fallback_kind: Option<&'static str> = None;
let mut in_code_block = false;
for raw_line in output.lines() {
let trimmed = raw_line.trim();
if trimmed.starts_with("```") {
in_code_block = !in_code_block;
continue;
}
if in_code_block {
continue;
}
let mut candidates: Vec<String> = vec![trimmed.to_string()];
if let Some(text) = crate::stream_json_textifier::extract_text_from_stream_json(trimmed) {
for inner in text.lines() {
candidates.push(inner.to_string());
}
}
for candidate in &candidates {
let cand = candidate.trim();
if let Some((kind, findings)) = parse_json_verdict(cand) {
return match kind {
"pass" => AcceptanceResult::Pass,
"fail" => AcceptanceResult::Fail { findings },
"continue" => AcceptanceResult::Continue,
"gated" | "blocked" => AcceptanceResult::Gated,
_ => AcceptanceResult::Continue,
};
}
if fallback_kind.is_none() {
if let Some(kind) = canonical_verdict_kind(cand) {
fallback_kind = Some(kind);
}
}
}
}
match fallback_kind {
Some("pass") => AcceptanceResult::Pass,
Some("continue") => AcceptanceResult::Continue,
Some("gated") => AcceptanceResult::Gated,
Some("fail") => {
let findings = parse_findings(output);
AcceptanceResult::Fail { findings }
}
_ => AcceptanceResult::Continue,
}
}
pub(crate) fn strip_markdown_decorations(text: &str) -> String {
let mut s = text.to_string();
s = s.replace("**", "");
s = s.replace(['*', '_'], "");
let trimmed = s.trim();
let trimmed = trimmed.trim_start_matches('#');
let trimmed = trimmed.trim_start_matches('>');
let trimmed = trimmed.trim_start_matches('-');
trimmed.trim().to_string()
}
fn parse_findings(output: &str) -> Vec<String> {
let lines: Vec<&str> = output.lines().collect();
let mut findings = Vec::new();
let mut in_findings_section = false;
for line in lines {
let trimmed = line.trim();
if trimmed == "FINDINGS:" {
in_findings_section = true;
continue;
}
if in_findings_section {
if let Some(finding) = trimmed.strip_prefix("- ") {
findings.push(finding.to_string());
} else if !trimmed.is_empty() && !trimmed.starts_with('-') {
break;
}
}
}
findings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_pass() {
let output = "ACCEPTANCE: PASS\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn acceptance_append_prompt_text_is_not_parsed_as_output_verdict() {
let prompt = crate::agent::append_optional_prompt(
"generated acceptance prompt".to_string(),
Some("optional guidance mentioning ACCEPTANCE: FAIL and {change_id}"),
);
assert!(prompt.ends_with("optional guidance mentioning ACCEPTANCE: FAIL and {change_id}"));
assert_eq!(
parse_acceptance_output("ACCEPTANCE: PASS\n"),
AcceptanceResult::Pass
);
}
#[test]
fn test_parse_pass_with_extra_output() {
let output = "Some debug output\nACCEPTANCE: PASS\nMore output\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_fail_with_findings() {
let output = "ACCEPTANCE: FAIL\nFINDINGS:\n- Issue 1\n- Issue 2\n";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 2);
assert_eq!(findings[0], "Issue 1");
assert_eq!(findings[1], "Issue 2");
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_fail_with_no_findings() {
let output = "ACCEPTANCE: FAIL\n";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 0);
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_fail_with_multiline_findings() {
let output = r#"ACCEPTANCE: FAIL
FINDINGS:
- Task 1.3 is not completed
- Missing unit tests for new feature
- Code does not handle error case X
"#;
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 3);
assert_eq!(findings[0], "Task 1.3 is not completed");
assert_eq!(findings[1], "Missing unit tests for new feature");
assert_eq!(findings[2], "Code does not handle error case X");
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_no_status() {
let output = "Some random output\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_no_marker_defaults_to_continue() {
assert_eq!(parse_acceptance_output(""), AcceptanceResult::Continue);
let output = "Some debug output\nNo marker here\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
let output = "FINDINGS:\n- Issue 1\n- Issue 2\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_findings_with_trailing_content() {
let output = r#"ACCEPTANCE: FAIL
FINDINGS:
- Issue 1
- Issue 2
Additional output here
"#;
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 2);
assert_eq!(findings[0], "Issue 1");
assert_eq!(findings[1], "Issue 2");
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_continue() {
let output = "ACCEPTANCE: CONTINUE\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_continue_with_extra_output() {
let output = "Some debug output\nACCEPTANCE: CONTINUE\nMore output\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_pass_with_bold_decoration() {
let output = "**ACCEPTANCE: PASS**\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_pass_with_bold_decoration_and_extra_output() {
let output = "Some debug output\n**ACCEPTANCE: PASS**\nMore output\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_fail_with_bold_decoration() {
let output = "**ACCEPTANCE: FAIL**\nFINDINGS:\n- Issue 1\n- Issue 2\n";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 2);
assert_eq!(findings[0], "Issue 1");
assert_eq!(findings[1], "Issue 2");
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_continue_with_bold_decoration() {
let output = "**ACCEPTANCE: CONTINUE**\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_pass_with_italic_decoration() {
let output = "*ACCEPTANCE: PASS*\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_pass_with_mixed_decorations() {
let output = "**_ACCEPTANCE: PASS_**\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_strip_markdown_decorations() {
assert_eq!(
strip_markdown_decorations("**ACCEPTANCE: PASS**"),
"ACCEPTANCE: PASS"
);
assert_eq!(
strip_markdown_decorations("*ACCEPTANCE: PASS*"),
"ACCEPTANCE: PASS"
);
assert_eq!(
strip_markdown_decorations("_ACCEPTANCE: PASS_"),
"ACCEPTANCE: PASS"
);
assert_eq!(
strip_markdown_decorations("**_ACCEPTANCE: PASS_**"),
"ACCEPTANCE: PASS"
);
assert_eq!(
strip_markdown_decorations("ACCEPTANCE: PASS"),
"ACCEPTANCE: PASS"
);
}
#[test]
fn test_parse_ignores_acceptance_in_code_blocks() {
let output = r#"
Example output:
```
ACCEPTANCE: FAIL
FINDINGS:
- Issue 1
```
Actual result:
ACCEPTANCE: PASS
"#;
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_ignores_multiple_code_blocks() {
let output = r#"
First example:
```
ACCEPTANCE: FAIL
```
Second example:
```
ACCEPTANCE: CONTINUE
```
Actual result:
ACCEPTANCE: PASS
"#;
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_code_block_with_language_specifier() {
let output = r#"
Example:
```bash
ACCEPTANCE: FAIL
```
Result:
ACCEPTANCE: PASS
"#;
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_unclosed_code_block() {
let output = r#"
Example:
```
ACCEPTANCE: FAIL
ACCEPTANCE: PASS
"#;
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_pass_with_trailing_text_is_not_canonical() {
let output = "ACCEPTANCE: PASSAll acceptance criteria verified:\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_pass_with_trailing_heading_is_not_canonical() {
let output = "ACCEPTANCE: PASS## Acceptance Review Summary\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_pass_with_trailing_text_in_context_is_not_canonical() {
let output = r#"Some prior output
ACCEPTANCE: PASSAll acceptance criteria verified:
1. Git working tree is clean
"#;
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_pass_with_trailing_text_falls_through_to_canonical_pass() {
let output = "ACCEPTANCE: PASSAll bad form\nACCEPTANCE: PASS\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_fail_with_trailing_text_is_not_canonical() {
let output = "ACCEPTANCE: FAILSome additional context\nFINDINGS:\n- Issue 1\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_continue_with_trailing_text_is_not_canonical() {
let output = "ACCEPTANCE: CONTINUENeeds further investigation\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_blocked_with_trailing_text_is_not_canonical() {
let output = "ACCEPTANCE: BLOCKEDWaiting for dependency\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_passed_word_boundary_is_not_canonical() {
let output = "ACCEPTANCE: PASSED\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_canonical_verdict_kind_strict_match() {
assert_eq!(canonical_verdict_kind("ACCEPTANCE: PASS"), Some("pass"));
assert_eq!(canonical_verdict_kind("**ACCEPTANCE: PASS**"), Some("pass"));
assert_eq!(canonical_verdict_kind("## ACCEPTANCE: PASS"), Some("pass"));
assert_eq!(canonical_verdict_kind("> ACCEPTANCE: FAIL"), Some("fail"));
assert_eq!(canonical_verdict_kind("ACCEPTANCE: PASSAll bad"), None);
assert_eq!(canonical_verdict_kind("ACCEPTANCE: PASS## heading"), None);
assert_eq!(canonical_verdict_kind("ACCEPTANCE: PASSED"), None);
assert_eq!(canonical_verdict_kind("not a verdict"), None);
}
#[test]
fn test_parse_blocked() {
let output = "ACCEPTANCE: BLOCKED\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Gated);
}
#[test]
fn test_parse_blocked_with_extra_output() {
let output = "Some debug output\nACCEPTANCE: BLOCKED\nMore output\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Gated);
}
#[test]
fn test_parse_blocked_with_bold_decoration() {
let output = "**ACCEPTANCE: BLOCKED**\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Gated);
}
#[test]
fn test_parse_fail_findings_excludes_preamble() {
let output =
"preamble line\nACCEPTANCE: FAIL\nFINDINGS:\n- Finding 1\n- Finding 2\npostamble";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings, vec!["Finding 1", "Finding 2"]);
assert!(!findings.iter().any(|f| f.contains("preamble")));
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_fail_findings_from_findings_section_only() {
let output =
"ACCEPTANCE: FAIL\nFINDINGS:\n- src/foo.rs:10 missing test\n- src/bar.rs:5 dead code\n";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 2);
assert_eq!(findings[0], "src/foo.rs:10 missing test");
assert_eq!(findings[1], "src/bar.rs:5 dead code");
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_pass_with_heading_prefix() {
let output = "## ACCEPTANCE: PASS\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_pass_with_heading_h3_prefix() {
let output = "### ACCEPTANCE: PASS\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_fail_with_heading_prefix() {
let output = "## ACCEPTANCE: FAIL\nFINDINGS:\n- Issue 1\n";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 1);
assert_eq!(findings[0], "Issue 1");
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_pass_with_blockquote_prefix() {
let output = "> ACCEPTANCE: PASS\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_fail_with_blockquote_prefix() {
let output = "> ACCEPTANCE: FAIL\nFINDINGS:\n- Issue 1\n";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings.len(), 1);
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_parse_pass_with_bullet_prefix() {
let output = "- ACCEPTANCE: PASS\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_continue_with_heading_prefix() {
let output = "## ACCEPTANCE: CONTINUE\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Continue);
}
#[test]
fn test_parse_blocked_with_heading_prefix() {
let output = "## ACCEPTANCE: BLOCKED\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Gated);
}
#[test]
fn test_parse_pass_with_heading_and_bold() {
let output = "## **ACCEPTANCE: PASS**\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_strip_markdown_decorations_heading() {
assert_eq!(
strip_markdown_decorations("## ACCEPTANCE: PASS"),
"ACCEPTANCE: PASS"
);
}
#[test]
fn test_strip_markdown_decorations_blockquote() {
assert_eq!(
strip_markdown_decorations("> ACCEPTANCE: PASS"),
"ACCEPTANCE: PASS"
);
}
#[test]
fn test_strip_markdown_decorations_bullet() {
assert_eq!(
strip_markdown_decorations("- ACCEPTANCE: PASS"),
"ACCEPTANCE: PASS"
);
}
#[test]
fn test_strip_markdown_decorations_heading_and_bold() {
assert_eq!(
strip_markdown_decorations("## **ACCEPTANCE: PASS**"),
"ACCEPTANCE: PASS"
);
}
#[test]
fn test_parse_fail_empty_findings_when_no_section() {
let output = "ACCEPTANCE: FAIL\nSome explanation without a FINDINGS: header\n";
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert!(findings.is_empty());
}
_ => panic!("Expected Fail"),
}
}
#[test]
fn test_marker_detection_consistency_with_parser() {
let drift_cases: &[(&str, &str)] = &[
("ACCEPTANCE: PASS", "pass"),
("**ACCEPTANCE: PASS**", "pass"),
("## ACCEPTANCE: PASS", "pass"),
("> ACCEPTANCE: PASS", "pass"),
("- ACCEPTANCE: PASS", "pass"),
("### **ACCEPTANCE: FAIL**", "fail"),
("ACCEPTANCE: CONTINUE", "continue"),
("ACCEPTANCE: GATED", "gated"),
("ACCEPTANCE: BLOCKED", "gated"),
("## ACCEPTANCE: BLOCKED", "gated"),
("> ACCEPTANCE: FAIL", "fail"),
];
for (case, expected_kind) in drift_cases {
assert_eq!(
canonical_verdict_kind(case),
Some(*expected_kind),
"canonical_verdict_kind must detect '{}' as kind '{}'",
case,
expected_kind
);
let full_output = format!("{}\n", case);
let result = parse_acceptance_output(&full_output);
let result_kind = match &result {
AcceptanceResult::Pass => "pass",
AcceptanceResult::Fail { .. } => "fail",
AcceptanceResult::Continue => "continue",
AcceptanceResult::Gated => "gated",
};
assert_eq!(
result_kind, *expected_kind,
"parse_acceptance_output returned '{}' but expected '{}' for input '{}'",
result_kind, expected_kind, case
);
}
}
#[test]
fn test_marker_detection_rejects_trailing_text_uniformly() {
let malformed: &[&str] = &[
"ACCEPTANCE: PASSAll checks completed",
"ACCEPTANCE: PASS## Acceptance Review Summary",
"ACCEPTANCE: PASSED",
"ACCEPTANCE: FAILSome explanation",
"ACCEPTANCE: CONTINUEMore work",
"ACCEPTANCE: BLOCKEDWaiting",
];
for case in malformed {
assert!(
canonical_verdict_kind(case).is_none(),
"canonical_verdict_kind must NOT detect malformed verdict '{}'",
case
);
}
}
#[test]
fn test_code_fence_markers_rejected_by_both_parser_and_detection() {
let output = "```\nACCEPTANCE: PASS\n```\n";
assert_eq!(
parse_acceptance_output(output),
AcceptanceResult::Continue,
"Parser must not match markers inside code fences"
);
}
#[test]
fn test_parse_json_verdict_pass() {
let line = r#"{"acceptance":"pass"}"#;
assert_eq!(
parse_json_verdict(line),
Some(("pass", Vec::<String>::new()))
);
}
#[test]
fn test_parse_json_verdict_fail_with_findings() {
let line = r#"{"acceptance":"fail","findings":["src/a.rs:1 bad","src/b.rs:2 worse"]}"#;
let (kind, findings) = parse_json_verdict(line).expect("strict JSON verdict");
assert_eq!(kind, "fail");
assert_eq!(findings, vec!["src/a.rs:1 bad", "src/b.rs:2 worse"]);
}
#[test]
fn test_parse_json_verdict_continue_gated_and_legacy_blocked() {
assert_eq!(
parse_json_verdict(r#"{"acceptance":"continue"}"#),
Some(("continue", Vec::<String>::new()))
);
assert_eq!(
parse_json_verdict(r#"{"acceptance":"gated"}"#),
Some(("gated", Vec::<String>::new()))
);
assert_eq!(
parse_json_verdict(r#"{"acceptance":"blocked"}"#),
Some(("gated", Vec::<String>::new()))
);
}
#[test]
fn test_parse_json_verdict_case_insensitive_value() {
assert_eq!(
parse_json_verdict(r#"{"acceptance":"PASS"}"#),
Some(("pass", Vec::<String>::new()))
);
assert_eq!(
parse_json_verdict(r#"{"acceptance":"Fail"}"#),
Some(("fail", Vec::<String>::new()))
);
}
#[test]
fn test_parse_json_verdict_rejects_non_object_and_unknown_kind() {
assert_eq!(parse_json_verdict("pass"), None);
assert_eq!(parse_json_verdict(r#"["pass"]"#), None);
assert_eq!(parse_json_verdict(r#"{"acceptance":"maybe"}"#), None);
assert_eq!(parse_json_verdict(r#"{"other":"pass"}"#), None);
assert_eq!(parse_json_verdict("not json"), None);
assert_eq!(parse_json_verdict(""), None);
}
#[test]
fn test_parse_acceptance_output_json_pass_single_line() {
let output = r#"{"acceptance":"pass"}
"#;
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_acceptance_output_json_fail_findings_preferred_over_text_section() {
let output = r#"preamble
{"acceptance":"fail","findings":["x","y"]}
"#;
match parse_acceptance_output(output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings, vec!["x", "y"]);
}
other => panic!("expected Fail, got {:?}", other),
}
}
#[test]
fn test_parse_acceptance_output_json_beats_text_fallback_regardless_of_order() {
let output = "ACCEPTANCE: CONTINUE\n{\"acceptance\":\"pass\"}\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_acceptance_output_text_fallback_when_no_json() {
let output = "ACCEPTANCE: PASS\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_acceptance_output_json_inside_agent_assistant_event() {
let event = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"{\"acceptance\":\"pass\"}"}]}}"#;
let output = format!("{}\n", event);
assert_eq!(parse_acceptance_output(&output), AcceptanceResult::Pass);
}
#[test]
fn test_parse_acceptance_output_json_inside_agent_result_event() {
let event = r#"{"type":"result","subtype":"success","result":"{\"acceptance\":\"fail\",\"findings\":[\"a\"]}","is_error":false}"#;
let output = format!("{}\n", event);
match parse_acceptance_output(&output) {
AcceptanceResult::Fail { findings } => {
assert_eq!(findings, vec!["a".to_string()]);
}
other => panic!("expected Fail, got {:?}", other),
}
}
#[test]
fn test_parse_acceptance_output_json_inside_codex_item_completed_event() {
let event = r#"{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"{\"acceptance\":\"pass\"}"}}"#;
let output = format!("{}\n", event);
assert_eq!(parse_acceptance_output(&output), AcceptanceResult::Pass);
}
#[test]
fn test_detect_verdict_in_line_json_direct() {
assert_eq!(
detect_verdict_in_line(r#"{"acceptance":"pass"}"#),
Some("pass")
);
assert_eq!(
detect_verdict_in_line(r#"{"acceptance":"gated"}"#),
Some("gated")
);
assert_eq!(
detect_verdict_in_line(r#"{"acceptance":"blocked"}"#),
Some("gated")
);
}
#[test]
fn test_detect_verdict_in_line_json_inside_assistant_event() {
let event = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"{\"acceptance\":\"pass\"}"}]}}"#;
assert_eq!(detect_verdict_in_line(event), Some("pass"));
}
#[test]
fn test_detect_verdict_in_line_text_fallback() {
assert_eq!(detect_verdict_in_line("ACCEPTANCE: PASS"), Some("pass"));
assert_eq!(detect_verdict_in_line("**ACCEPTANCE: FAIL**"), Some("fail"));
assert_eq!(
detect_verdict_in_line("ACCEPTANCE: PASSAll checks done"),
None,
"trailing-text PASS must not satisfy even the text fallback"
);
}
#[test]
fn test_detect_verdict_in_line_text_inside_assistant_event() {
let event = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ACCEPTANCE: PASS"}]}}"#;
assert_eq!(detect_verdict_in_line(event), Some("pass"));
}
#[test]
fn test_detect_verdict_in_line_unrelated_events_return_none() {
assert_eq!(
detect_verdict_in_line(r#"{"type":"system","subtype":"init"}"#),
None
);
assert_eq!(detect_verdict_in_line("plain log line"), None);
assert_eq!(detect_verdict_in_line(""), None);
}
#[test]
fn test_regression_malformed_text_then_json_pass() {
let output = "ACCEPTANCE: PASS# Acceptance Review Summary\n{\"acceptance\":\"pass\"}\n";
assert_eq!(parse_acceptance_output(output), AcceptanceResult::Pass);
}
}