Infer whether a verification command passed. Exit status and normalized tool status are authoritative. Text output is a fallback for hook payloads that expose only display output. Zero-failure summaries must be recognized before generic `fail` / `error` tokens.
(
tool_input: Option<&serde_json::Value>,
tool_output: Option<&str>,
status: Option<&str>,
)
| 578 | /// fallback for hook payloads that expose only display output. Zero-failure |
| 579 | /// summaries must be recognized before generic `fail` / `error` tokens. |
| 580 | pub(crate) fn infer_verification_passed( |
| 581 | tool_input: Option<&serde_json::Value>, |
| 582 | tool_output: Option<&str>, |
| 583 | status: Option<&str>, |
| 584 | ) -> Option<bool> { |
| 585 | if let Some(exit_code) = tool_input |
| 586 | .and_then(|value| value.get("exit_code")) |
| 587 | .and_then(|value| value.as_i64()) |
| 588 | { |
| 589 | return Some(exit_code == 0); |
| 590 | } |
| 591 | |
| 592 | match status.map(str::to_ascii_lowercase).as_deref() { |
| 593 | Some("error" | "failed" | "failure") => return Some(false), |
| 594 | // `completed` commonly means only that the tool call returned. Its |
| 595 | // command may still have failed, so let output inference decide. |
| 596 | Some("success" | "succeeded" | "passed" | "ok") => return Some(true), |
| 597 | _ => {} |
| 598 | } |
| 599 | |
| 600 | tool_output.and_then(|output| { |
| 601 | let lower = output.to_ascii_lowercase(); |
| 602 | let mut has_zero_failure_count = false; |
| 603 | let mut has_nonzero_failure_count = false; |
| 604 | let mut has_unquantified_failure = false; |
| 605 | let mut has_negative_tap_result = false; |
| 606 | let mut has_success_token = false; |
| 607 | |
| 608 | for line in lower.lines() { |
| 609 | // Reporters decorate summary lines differently (`# fail 0`, |
| 610 | // `ℹ fail 0`, `0 failures`, etc.). Tokenize away decoration and |
| 611 | // inspect nearby count/label pairs in either order. |
| 612 | let tokens: Vec<&str> = line |
| 613 | .split(|c: char| !c.is_ascii_alphanumeric()) |
| 614 | .filter(|token| !token.is_empty()) |
| 615 | .collect(); |
| 616 | let line_has_negative_tap_result = tokens |
| 617 | .windows(2) |
| 618 | .any(|pair| pair[0] == "not" && pair[1] == "ok"); |
| 619 | has_negative_tap_result |= line_has_negative_tap_result; |
| 620 | has_success_token |= tokens.iter().any(|token| { |
| 621 | matches!( |
| 622 | *token, |
| 623 | "ok" | "pass" | "passed" | "success" | "succeeded" | "successful" |
| 624 | ) |
| 625 | }); |
| 626 | let trimmed = line.trim_start(); |
| 627 | let line_is_successful_test = !line_has_negative_tap_result |
| 628 | && ((tokens.first() == Some(&"test") |
| 629 | && matches!(tokens.last(), Some(&("ok" | "pass" | "passed")))) |
| 630 | || matches!(tokens.first(), Some(&("ok" | "pass" | "passed"))) |
| 631 | || trimmed.starts_with('✓') |
| 632 | || trimmed.starts_with('✔')); |
| 633 | for (index, token) in tokens.iter().enumerate() { |
| 634 | if !matches!( |
| 635 | *token, |
| 636 | "fail" | "failed" | "failing" | "failure" | "failures" | "error" | "errors" |
| 637 | ) { |
no test coverage detected