dev-report 0.9.0

Structured, machine-readable reports for AI-assisted Rust development. Foundation schema of the dev-* verification suite.
Documentation
use dev_report::{
    CheckResult, DiffOptions, Evidence, EvidenceKind, MultiReport, Report, Severity, Verdict,
};

#[test]
fn smoke_pass_path() {
    let mut r = Report::new("smoke-test", "0.1.0").with_producer("dev-report-smoke");
    r.push(CheckResult::pass("a"));
    r.push(CheckResult::pass("b").with_duration_ms(10));
    r.finish();

    assert_eq!(r.overall_verdict(), Verdict::Pass);
    assert_eq!(r.checks.len(), 2);
}

#[test]
fn smoke_fail_path() {
    let mut r = Report::new("smoke-test", "0.1.0");
    r.push(CheckResult::pass("a"));
    r.push(CheckResult::fail("b", Severity::Error).with_detail("bad output"));
    r.finish();

    assert_eq!(r.overall_verdict(), Verdict::Fail);
}

#[test]
fn smoke_json_roundtrip() {
    let mut r = Report::new("smoke-test", "0.1.0");
    r.push(CheckResult::warn("flaky", Severity::Warning));
    r.push(CheckResult::skip("not_applicable"));
    r.finish();

    let json = r.to_json().unwrap();
    let parsed = Report::from_json(&json).unwrap();
    assert_eq!(parsed.subject, "smoke-test");
    assert_eq!(parsed.checks.len(), 2);
    assert_eq!(parsed.overall_verdict(), Verdict::Warn);
}

#[test]
fn smoke_tags_and_evidence_roundtrip() {
    let mut r = Report::new("smoke-test", "0.2.0");
    r.push(
        CheckResult::pass("bench::parse")
            .with_tag("bench")
            .with_duration_ms(7)
            .with_evidence(Evidence::numeric("mean_ns", 1234.5))
            .with_evidence(Evidence::numeric("baseline_ns", 1100.0))
            .with_evidence(Evidence::kv("env", [("CI", "true"), ("RUST_LOG", "debug")]))
            .with_evidence(Evidence::file_ref_lines("site", "src/parse.rs", 10, 20)),
    );
    r.push(
        CheckResult::fail("chaos::recover", Severity::Critical)
            .with_tags(["chaos", "recovery"])
            .with_detail("recovery did not restore final state")
            .with_evidence(Evidence::snippet(
                "context",
                "expected=2 observed=2 final_state_ok=false",
            )),
    );
    r.finish();

    let json = r.to_json().unwrap();
    let parsed = Report::from_json(&json).unwrap();

    assert_eq!(parsed.overall_verdict(), Verdict::Fail);
    assert_eq!(parsed.checks.len(), 2);

    let bench: Vec<_> = parsed.checks_with_tag("bench").collect();
    assert_eq!(bench.len(), 1);
    assert_eq!(bench[0].name, "bench::parse");
    assert_eq!(bench[0].evidence.len(), 4);
    assert_eq!(bench[0].evidence[3].kind(), EvidenceKind::FileRef);

    let chaos: Vec<_> = parsed.checks_with_tag("chaos").collect();
    assert_eq!(chaos.len(), 1);
    assert!(chaos[0].has_tag("recovery"));
}

#[test]
fn smoke_diff_flags_regression() {
    let mut prev = Report::new("c", "0.1.0");
    prev.push(CheckResult::pass("compile"));
    prev.push(CheckResult::pass("hot_path").with_duration_ms(100));
    prev.finish();

    let mut curr = Report::new("c", "0.1.0");
    curr.push(CheckResult::fail("compile", Severity::Error));
    curr.push(CheckResult::pass("hot_path").with_duration_ms(200));
    curr.push(CheckResult::pass("new_check"));
    curr.finish();

    let diff = curr.diff_with(
        &prev,
        &DiffOptions {
            duration_regression_pct: Some(20.0),
            duration_regression_abs_ms: None,
        },
    );
    assert!(!diff.is_clean());
    assert_eq!(diff.newly_failing, vec!["compile".to_string()]);
    assert_eq!(diff.added, vec!["new_check".to_string()]);
    assert_eq!(diff.duration_regressions.len(), 1);
    assert_eq!(diff.duration_regressions[0].name, "hot_path");
}

#[test]
fn smoke_multi_report_aggregation() {
    let mut bench = Report::new("c", "0.1.0").with_producer("dev-bench");
    bench.push(CheckResult::pass("hot_path").with_duration_ms(50));
    bench.finish();

    let mut chaos = Report::new("c", "0.1.0").with_producer("dev-chaos");
    chaos.push(CheckResult::fail("recover", Severity::Critical));
    chaos.finish();

    let mut multi = MultiReport::new("c", "0.1.0");
    multi.push(bench);
    multi.push(chaos);
    multi.finish();

    assert_eq!(multi.overall_verdict(), Verdict::Fail);
    assert_eq!(multi.total_check_count(), 2);

    let json = multi.to_json().unwrap();
    let parsed = MultiReport::from_json(&json).unwrap();
    assert_eq!(parsed.reports.len(), 2);
}

#[cfg(feature = "terminal")]
#[test]
fn smoke_terminal_render() {
    let mut r = Report::new("c", "0.1.0").with_producer("dev-bench");
    r.push(CheckResult::pass("a"));
    r.push(CheckResult::fail("b", Severity::Error));
    r.finish();
    let out = r.to_terminal();
    assert!(out.contains("[PASS]"));
    assert!(out.contains("[FAIL error]"));
    let colored = r.to_terminal_color();
    assert!(colored.contains('\x1b'));
}

#[cfg(feature = "markdown")]
#[test]
fn smoke_markdown_render() {
    let mut r = Report::new("c", "0.1.0").with_producer("dev-bench");
    r.push(CheckResult::pass("a"));
    r.finish();
    let md = r.to_markdown();
    assert!(md.starts_with("# Report:"));
    assert!(md.contains("**Overall verdict:** **PASS**"));
}