prx 0.5.9

Praxis — agent-native Unix tools. Single binary replacing grep, cat, find, sed, diff for AI coding agents.
use super::{Diagnostic, ParsedResult, define_regex};

define_regex!(
    JEST_SUMMARY_RE,
    r"Tests:\s+(?:(\d+) failed, )?(\d+) passed, (\d+) total"
);
define_regex!(
    VITEST_SUMMARY_RE,
    r"Tests\s+(?:(\d+) failed \| )?(\d+) passed \((\d+)\)"
);
define_regex!(FAILURE_RE, r"^\s*●\s+(.+)$");
define_regex!(AT_RE, r"at .+ \((.+):(\d+):(\d+)\)");

pub fn parse(output: &str) -> ParsedResult {
    let mut passed = 0;
    let mut failed = 0;
    let mut summary = String::new();
    let mut failures = Vec::new();

    for line in output.lines() {
        if let Some(caps) = JEST_SUMMARY_RE.captures(line) {
            failed = caps
                .get(1)
                .and_then(|m| m.as_str().parse().ok())
                .unwrap_or(0);
            passed = caps[2].parse().unwrap_or(0);
            summary = format!("{passed} passed, {failed} failed");
        } else if let Some(caps) = VITEST_SUMMARY_RE.captures(line) {
            failed = caps
                .get(1)
                .and_then(|m| m.as_str().parse().ok())
                .unwrap_or(0);
            passed = caps[2].parse().unwrap_or(0);
            summary = format!("{passed} passed, {failed} failed");
        }
    }

    let lines: Vec<&str> = output.lines().collect();
    let mut i = 0;
    while i < lines.len() {
        if let Some(caps) = FAILURE_RE.captures(lines[i]) {
            let name = caps[1].trim().to_string();
            let mut message_lines = Vec::new();
            let mut location = None;

            i += 1;
            while i < lines.len() && !lines[i].trim().starts_with('') {
                let trimmed = lines[i].trim();
                if let Some(at_caps) = AT_RE.captures(trimmed) {
                    if location.is_none() {
                        location = Some(format!("{}:{}:{}", &at_caps[1], &at_caps[2], &at_caps[3]));
                    }
                } else if !trimmed.is_empty() {
                    message_lines.push(trimmed.to_string());
                }
                i += 1;
            }

            failures.push(Diagnostic {
                name,
                location,
                message: message_lines.join("\n"),
            });
            continue;
        }
        i += 1;
    }

    if summary.is_empty() {
        summary = format!("{passed} passed, {failed} failed");
    }

    ParsedResult::new(summary, passed, failed, 0, failures)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_jest_all_pass() {
        let output = "Tests:  50 passed, 50 total\nTime:   3.45 s\n";
        let result = parse(output);
        assert_eq!(result.passed, 50);
        assert_eq!(result.failed, 0);
    }

    #[test]
    fn parse_jest_with_failures() {
        let output = "\
  ● Auth > should authenticate user

    expect(received).toBe(expected)

    Expected: true
    Received: false

      at Object.<anonymous> (tests/auth.test.ts:43:18)

Tests:  1 failed, 49 passed, 50 total
";
        let result = parse(output);
        assert_eq!(result.passed, 49);
        assert_eq!(result.failed, 1);
        assert_eq!(result.failures.len(), 1);
        assert!(result.failures[0].name.contains("authenticate"));
        assert!(
            result.failures[0]
                .location
                .as_ref()
                .unwrap()
                .contains("auth.test.ts:43")
        );
    }

    #[test]
    fn parse_vitest_summary() {
        let output = " Tests  2 failed | 48 passed (50)\n";
        let result = parse(output);
        assert_eq!(result.passed, 48);
        assert_eq!(result.failed, 2);
    }
}