| 559 | |
| 560 | #[cfg(test)] |
| 561 | fn parse_runner_output(stdout: &str) -> Result<String> { |
| 562 | let trimmed = stdout.trim(); |
| 563 | if trimmed.is_empty() { |
| 564 | anyhow::bail!("comment ai runner returned empty output"); |
| 565 | } |
| 566 | |
| 567 | if let Some(markdown) = extract_final_reply_markdown(trimmed) { |
| 568 | return Ok(markdown); |
| 569 | } |
| 570 | |
| 571 | let normalized_quotes = normalize_json_quotes(trimmed); |
| 572 | if normalized_quotes != trimmed { |
| 573 | if let Some(markdown) = extract_final_reply_markdown(&normalized_quotes) { |
| 574 | return Ok(markdown); |
| 575 | } |
| 576 | } |
| 577 | |
| 578 | if let Some(markdown) = extract_final_reply_from_text(&normalized_quotes) { |
| 579 | return Ok(markdown); |
| 580 | } |
| 581 | |
| 582 | let normalized_unescaped = normalized_quotes |
| 583 | .replace("\\\"", "\"") |
| 584 | .replace("\\\\", "\\"); |
| 585 | if normalized_unescaped != normalized_quotes { |
| 586 | if let Some(markdown) = extract_final_reply_markdown(&normalized_unescaped) { |
| 587 | return Ok(markdown); |
| 588 | } |
| 589 | if let Some(markdown) = extract_final_reply_from_text(&normalized_unescaped) { |
| 590 | return Ok(markdown); |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | let diagnostics = inspect_runner_output(&normalized_quotes); |
| 595 | if looks_like_codex_json_stream(&normalized_quotes) { |
| 596 | anyhow::bail!( |
| 597 | "codex stream completed but no `final_reply_markdown` payload was extracted ({})", |
| 598 | diagnostics.summary() |
| 599 | ); |
| 600 | } |
| 601 | |
| 602 | anyhow::bail!( |
| 603 | "runner output missing `final_reply_markdown` payload ({})", |
| 604 | diagnostics.summary() |
| 605 | ) |
| 606 | } |
| 607 | |
| 608 | #[cfg(test)] |
| 609 | fn parse_runner_output_with_fallback( |