okf 0.2.6

The Open Knowledge Format (OKF) v0.2 in pure Rust: the `okf` CLI (validate, lint, inspect, index, graph, format, diff) plus the full okf-core and okf-validator library APIs re-exported.
Documentation
//! Integration tests for `--fix` on validate and lint, and clippy-style fixable diagnostics.

mod common;

use common::TempDir;
use std::process::Command;

fn okf() -> Command {
    Command::new(env!("CARGO_BIN_EXE_okf"))
}

#[test]
fn cli_validate_and_lint_show_fixable_count() {
    let tmp = TempDir::new();
    tmp.write("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Test\n");
    // Missing title, missing generated, no top heading -> fixable
    tmp.write(
        "metrics/revenue.md",
        "---\ntype: Metric\n---\nRevenue is money.\n",
    );

    let output = okf()
        .args(["lint", tmp.path().to_str().unwrap()])
        .output()
        .unwrap();
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("fixable with `--fix`"), "Output: {stdout}");

    let val_output = okf()
        .args(["validate", tmp.path().to_str().unwrap()])
        .output()
        .unwrap();
    let val_stdout = String::from_utf8(val_output.stdout).unwrap();
    assert!(
        val_stdout.contains("fixable with `--fix`"),
        "Output: {val_stdout}"
    );
}

#[test]
fn validate_fix_only_fixes_validation_issues_not_lint() {
    let tmp = TempDir::new();
    tmp.write("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Test\n");
    // Missing title & generated (validation) + no top `#` heading (lint L8)
    let original_body = "Revenue is money.\n";
    tmp.write(
        "metrics/revenue.md",
        &format!("---\ntype: Metric\n---\n{original_body}"),
    );

    // Run validate --fix
    let output = okf()
        .args(["validate", tmp.path().to_str().unwrap(), "--fix"])
        .output()
        .unwrap();
    assert!(output.status.success());

    let content = tmp.read("metrics/revenue.md");
    // Validation issues were fixed
    assert!(content.contains("title: Revenue"));
    assert!(content.contains("generated:"));
    // Lint-only issue (no top `#` heading) was NOT fixed by validate --fix
    assert!(content.contains(original_body));
    assert!(!content.contains("# Revenue\n\nRevenue is money."));

    // Now run lint --fix
    let lint_output = okf()
        .args(["lint", tmp.path().to_str().unwrap(), "--fix"])
        .output()
        .unwrap();
    let lint_stdout = String::from_utf8(lint_output.stdout).unwrap();
    assert!(lint_stdout.contains("Applied"));

    let content_after_lint = tmp.read("metrics/revenue.md");
    // Lint issue (no top `#` heading) IS now fixed by lint --fix
    assert!(content_after_lint.contains("# Revenue\n\nRevenue is money."));
}

#[test]
fn cli_lint_with_fix_flag_remediates_and_rechecks() {
    let tmp = TempDir::new();
    tmp.write(
        "overview.md",
        "---\ntype: Concept\ntitle: Overview\ndescription: Overview of the bundle.\ngenerated:\n  by: human:alice\n  at: 2026-01-01T00:00:00Z\n---\n\n# Overview\n\nSee [revenue](metrics/revenue.md).\n",
    );
    tmp.write(
        "metrics/revenue.md",
        "---\ntype: Metric\ntitle: Revenue\ndescription: Revenue tracking.\ntimestamp: 2026-01-01T00:00:00Z\n---\n\nRevenue is money.\n",
    );
    // Index will be regenerated by fix
    tmp.write(
        "index.md",
        "---\nokf_version: \"0.2\"\n---\n\n# Concepts\n\n* [Overview](overview.md)\n* [Revenue](metrics/revenue.md)\n",
    );

    let output = okf()
        .args(["lint", tmp.path().to_str().unwrap(), "--fix"])
        .output()
        .unwrap();
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("Applied"), "Output: {stdout}");

    // The file should now have generated block and top heading
    let content = tmp.read("metrics/revenue.md");
    assert!(!content.contains("timestamp:"));
    assert!(content.contains("generated:"));
    assert!(content.contains("# Revenue"));
}

#[test]
fn cli_validate_with_fix_flag_remediates_and_validates() {
    let tmp = TempDir::new();
    tmp.write("index.md", "---\nokf_version: \"0.2\"\n---\n\n# Test\n");
    tmp.write(
        "metrics/revenue.md",
        "---\ntype: Metric\ntimestamp: 2026-01-01T00:00:00Z\n---\n\n# Revenue\n\nRevenue is money.\n",
    );

    let output = okf()
        .args(["validate", tmp.path().to_str().unwrap(), "--fix"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("Applied"), "Output: {stdout}");
    assert!(
        stdout.contains("conformant with OKF v0.2"),
        "Output: {stdout}"
    );
}

#[test]
fn cli_validate_with_fix_normalizes_date_formats() {
    let tmp = TempDir::new();
    tmp.write(
        "index.md",
        "---\nokf_version: \"0.2\"\n---\n\n# Test\n\n* [Revenue](metrics/revenue.md)\n",
    );
    tmp.write(
        "metrics/revenue.md",
        "---\ntype: Metric\ntitle: Revenue\ndescription: Revenue tracking.\ngenerated:\n  by: human:alice\n  at: '2026-06-30'\nverified:\n  - by: human:bob\n    at: '2026-07-01 12:00:00'\nstale_after: '2026-12-31'\nusage_window:\n  from: '2026-01-01'\n  to: '2026-06-01'\nsources:\n  - id: '1'\n    resource: https://example.com\n    last_modified: '2026-05-15'\n---\n\n# Revenue\n\nRevenue is money.\n",
    );

    let val_before = okf()
        .args(["validate", tmp.path().to_str().unwrap(), "--json"])
        .output()
        .unwrap();
    let val_json_before: serde_json::Value = serde_json::from_slice(&val_before.stdout).unwrap();
    assert!(
        val_json_before["diagnostics"]
            .as_array()
            .unwrap()
            .iter()
            .any(|d| d["fixable"].as_bool() == Some(true)),
        "Expected fixable date diagnostics"
    );

    let fix_output = okf()
        .args(["validate", tmp.path().to_str().unwrap(), "--fix", "--json"])
        .output()
        .unwrap();
    assert!(fix_output.status.success());

    let val_after = okf()
        .args(["validate", tmp.path().to_str().unwrap(), "--json"])
        .output()
        .unwrap();
    let val_json_after: serde_json::Value = serde_json::from_slice(&val_after.stdout).unwrap();
    assert_eq!(
        val_json_after["error_count"].as_i64(),
        Some(0),
        "Expected 0 errors after fix"
    );
    assert_eq!(
        val_json_after["warning_count"].as_i64(),
        Some(0),
        "Expected 0 warnings after fix"
    );
    assert_eq!(
        val_json_after["conformant"].as_bool(),
        Some(true),
        "Expected conformant after fix"
    );

    let content = tmp.read("metrics/revenue.md");
    assert!(content.contains("2026-06-30T00:00:00Z"));
    assert!(content.contains("2026-07-01T12:00:00Z"));
    assert!(content.contains("2026-12-31T00:00:00Z"));
    assert!(content.contains("2026-01-01T00:00:00Z"));
    assert!(content.contains("2026-06-01T00:00:00Z"));
    assert!(content.contains("2026-05-15T00:00:00Z"));
}