cora-code 0.17.2

CLI-first AI code review — BYOK, diff/scan/branch, pre-commit hooks
use anyhow::Result;
use serde_json::{Value, json};

use crate::engine::{ReviewResponse, ScanResponse};
use crate::formatters::Formatter;

/// JSON formatter: outputs raw JSON with a watermark footer.
pub struct JsonFormatter;

impl Formatter for JsonFormatter {
    fn format_review(&self, response: &ReviewResponse) -> Result<String> {
        let mut obj = serde_json::to_value(response)?;
        add_watermark(&mut obj, &response.issues);
        let json = serde_json::to_string_pretty(&obj)?;
        Ok(json)
    }

    fn format_scan(&self, response: &ScanResponse) -> Result<String> {
        let mut obj = serde_json::to_value(response)?;
        add_watermark_scan(&mut obj, &response.issues);
        let json = serde_json::to_string_pretty(&obj)?;
        Ok(json)
    }
}

/// Add a `reviewed_by` watermark field to a JSON value when issues are present.
fn add_watermark(obj: &mut Value, issues: &[crate::engine::ReviewIssue]) {
    if !issues.is_empty() {
        if let Some(map) = obj.as_object_mut() {
            map.insert(
                "reviewed_by".to_string(),
                json!({
                    "tool": "cora",
                    "version": env!("CARGO_PKG_VERSION")
                }),
            );
        }
    }
}

/// Add a `reviewed_by` watermark field for scan responses.
fn add_watermark_scan(obj: &mut Value, issues: &[crate::engine::ReviewIssue]) {
    add_watermark(obj, issues);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::engine::{ReviewIssue, Severity, TokenUsage};

    fn sample_issue() -> ReviewIssue {
        ReviewIssue::new("src/main.rs", Some(42), Severity::Critical, "SQL Injection")
            .with_type("security")
            .with_body("User input concatenated into query.")
            .with_fix("Use parameterized queries.")
    }

    fn sample_response() -> ReviewResponse {
        ReviewResponse {
            issues: vec![sample_issue()],
            summary: "Found 1 critical issue.".to_string(),
            tokens_used: Some(TokenUsage {
                input_tokens: 100,
                output_tokens: 200,
                estimated_cost_usd: 0.005,
            }),
            should_block: false,
        }
    }

    fn empty_response() -> ReviewResponse {
        ReviewResponse {
            issues: vec![],
            summary: String::new(),
            tokens_used: None,
            should_block: false,
        }
    }

    fn sample_scan_response() -> ScanResponse {
        ScanResponse {
            issues: vec![sample_issue()],
            summary: "Scan complete.".to_string(),
            files_scanned: 10,
            lines_scanned: 500,
            tokens_used: None,
            should_block: false,
        }
    }

    #[test]
    fn format_review_output_is_valid_json() {
        let fmt = JsonFormatter;
        let output = fmt.format_review(&sample_response()).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
        assert!(parsed.get("issues").is_some());
        assert!(parsed.get("summary").is_some());
    }

    #[test]
    fn format_review_contains_issue_data() {
        let fmt = JsonFormatter;
        let output = fmt.format_review(&sample_response()).unwrap();
        assert!(output.contains("src/main.rs"));
        assert!(output.contains("SQL Injection"));
        assert!(output.contains("critical"));
    }

    #[test]
    fn format_review_empty_issues() {
        let fmt = JsonFormatter;
        let output = fmt.format_review(&empty_response()).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
        assert_eq!(parsed["issues"].as_array().unwrap().len(), 0);
        // No watermark when no issues
        assert!(parsed.get("reviewed_by").is_none());
    }

    #[test]
    fn format_review_with_issues_has_watermark() {
        let fmt = JsonFormatter;
        let output = fmt.format_review(&sample_response()).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
        // Watermark should be present when issues exist
        let reviewed_by = &parsed["reviewed_by"];
        assert_eq!(reviewed_by["tool"].as_str().unwrap(), "cora");
        assert!(reviewed_by["version"].as_str().is_some());
    }

    #[test]
    fn format_scan_output_is_valid_json() {
        let fmt = JsonFormatter;
        let output = fmt.format_scan(&sample_scan_response()).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
        assert!(parsed.get("files_scanned").is_some());
        assert!(parsed.get("lines_scanned").is_some());
        assert_eq!(parsed["files_scanned"].as_u64().unwrap(), 10);
    }

    #[test]
    fn format_review_multiple_issues() {
        let mut response = sample_response();
        response.issues.push(
            ReviewIssue::new("src/lib.rs", Some(10), Severity::Minor, "Naming")
                .with_type("style")
                .with_body("Use snake_case."),
        );
        let fmt = JsonFormatter;
        let output = fmt.format_review(&response).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
        assert_eq!(parsed["issues"].as_array().unwrap().len(), 2);
    }
}