homeboy 0.134.0

CLI for multi-component deployment and development workflow automation
Documentation
use serde_json::json;

use super::{build_findings_from_native_output, IssueRenderContext};
use crate::code_audit::FindingConfidence;

#[test]
fn test_build_findings_from_native_output() {
    let output = json!({"data": {"passed": true, "lint_findings": []}});
    let rendered =
        build_findings_from_native_output("lint", output, &IssueRenderContext::default()).unwrap();

    assert_eq!(rendered.command, "lint");
    assert!(rendered.groups.is_empty());
}

#[test]
fn test_merge() {
    let mut first = build_findings_from_native_output(
        "lint",
        json!({"data": {"passed": false, "lint_findings": [
            {"id": "lint-1", "category": "A", "message": "one"}
        ]}}),
        &IssueRenderContext::default(),
    )
    .unwrap();
    let second = build_findings_from_native_output(
        "lint",
        json!({"data": {"passed": false, "lint_findings": [
            {"id": "lint-2", "category": "B", "message": "two"}
        ]}}),
        &IssueRenderContext::default(),
    )
    .unwrap();

    first.merge(second);

    assert_eq!(first.command, "lint");
    assert!(first.groups.contains_key("A"));
    assert!(first.groups.contains_key("B"));
}

#[test]
fn renders_audit_output_grouped_by_kind_with_fixability() {
    let output = json!({
        "success": false,
        "data": {
            "command": "audit",
            "passed": false,
            "component_id": "homeboy",
            "source_path": "/tmp/homeboy",
            "findings": [
                {
                    "file": "src/a.rs",
                    "kind": "unreferenced_export",
                    "confidence": "structural",
                    "description": "export is unused",
                    "suggestion": "remove it"
                },
                {
                    "file": "src/b.rs",
                    "kind": "unreferenced_export",
                    "confidence": "structural",
                    "description": "export is unused",
                    "suggestion": "remove it"
                },
                {
                    "file": "src/large.rs",
                    "kind": "god_file",
                    "confidence": "heuristic",
                    "description": "file is large",
                    "suggestion": "split it"
                }
            ],
            "fixability": {
                "by_kind": {
                    "unreferenced_export": {
                        "total": 2,
                        "automated": 1,
                        "manual_only": 1
                    }
                }
            }
        }
    });
    let context = IssueRenderContext {
        run_url: Some("https://github.com/Extra-Chill/homeboy/actions/runs/1".to_string()),
    };

    let rendered = build_findings_from_native_output("audit", output, &context).unwrap();

    assert_eq!(rendered.command, "audit");
    assert_eq!(rendered.groups.len(), 2);
    let group = rendered.groups.get("unreferenced_export").unwrap();
    assert_eq!(group.count, 2);
    assert_eq!(group.label, "unreferenced export");
    assert_eq!(group.confidence, Some(FindingConfidence::Structural));
    assert!(group.body.contains("## unreferenced export"));
    assert!(group
        .body
        .contains("Run: https://github.com/Extra-Chill/homeboy/actions/runs/1"));
    assert!(group.body.contains("### Autofix status"));
    assert!(group.body.contains("- Automated: 1"));
    assert!(group.body.contains("- `src/a.rs` — export is unused"));
}

#[test]
fn renders_lint_output_grouped_by_category() {
    let output = json!({
        "data": {
            "passed": false,
            "status": "failed",
            "exit_code": 1,
            "lint_findings": [
                {"id": "lint-1", "category": "Squiz.Commenting.FunctionComment.Missing", "message": "missing docblock"},
                {"id": "lint-2", "category": "Squiz.Commenting.FunctionComment.Missing", "message": "missing docblock"},
                {"id": "lint-3", "category": "Generic.Files.LineLength.TooLong", "message": "line too long"}
            ]
        }
    });

    let rendered =
        build_findings_from_native_output("lint", output, &IssueRenderContext::default()).unwrap();

    assert_eq!(rendered.command, "lint");
    assert_eq!(rendered.groups.len(), 2);
    let group = rendered
        .groups
        .get("Squiz.Commenting.FunctionComment.Missing")
        .unwrap();
    assert_eq!(group.count, 2);
    assert!(group.body.contains("2 lint finding(s) in this category."));
    assert!(group.body.contains("- `lint-1` — missing docblock"));
}

#[test]
fn renders_lint_aggregate_fallback_when_findings_are_missing() {
    let output = json!({
        "data": {
            "passed": false,
            "status": "failed",
            "exit_code": 2
        }
    });

    let rendered =
        build_findings_from_native_output("lint", output, &IssueRenderContext::default()).unwrap();

    let group = rendered.groups.get("lint_failure").unwrap();
    assert_eq!(group.count, 1);
    assert!(group
        .body
        .contains("Lint failed without structured findings (exit 2)."));
}

#[test]
fn renders_test_analysis_clusters_by_category() {
    let output = json!({
        "data": {
            "passed": false,
            "status": "failed",
            "exit_code": 1,
            "analysis": {
                "clusters": [
                    {
                        "category": "missing_method",
                        "count": 3,
                        "pattern": "undefined method Widget::render",
                        "affected_files": ["tests/widget.rs"],
                        "example_tests": ["widget_renders", "widget_renders_nested"],
                        "suggested_fix": "Add the missing method"
                    }
                ]
            }
        }
    });

    let rendered =
        build_findings_from_native_output("test", output, &IssueRenderContext::default()).unwrap();

    let group = rendered.groups.get("missing_method").unwrap();
    assert_eq!(group.count, 3);
    assert_eq!(group.label, "missing method");
    assert!(group.body.contains("3 test failure(s) in this cluster."));
    assert!(group
        .body
        .contains("**Pattern:** undefined method Widget::render"));
    assert!(group.body.contains("- `tests/widget.rs`"));
    assert!(group.body.contains("- `widget_renders`"));
}

#[test]
fn renders_test_aggregate_fallback_from_counts() {
    let output = json!({
        "data": {
            "passed": false,
            "status": "failed",
            "exit_code": 1,
            "test_counts": {
                "total": 12,
                "passed": 10,
                "failed": 2,
                "skipped": 0
            }
        }
    });

    let rendered =
        build_findings_from_native_output("test", output, &IssueRenderContext::default()).unwrap();

    let group = rendered.groups.get("test_failure").unwrap();
    assert_eq!(group.count, 2);
    assert!(group.body.contains("2 failed test(s) out of 12 total."));
}

#[test]
fn passing_outputs_produce_no_groups() {
    let lint = json!({"data": {"passed": true, "lint_findings": []}});
    let test = json!({"data": {"passed": true, "test_counts": {"total": 1, "passed": 1, "failed": 0, "skipped": 0}}});

    assert!(
        build_findings_from_native_output("lint", lint, &IssueRenderContext::default())
            .unwrap()
            .groups
            .is_empty()
    );
    assert!(
        build_findings_from_native_output("test", test, &IssueRenderContext::default())
            .unwrap()
            .groups
            .is_empty()
    );
}