provenance-cli 0.2.3

Command-line interface for Provenance requirements traceability
use super::support::{diagnostic, error_json, provenance, record, REQUIREMENTS_SHARD, RULES_SHARD};
use provenance_macros::verifies;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};

struct Sides {
    base: PathBuf,
    ours: PathBuf,
    theirs: PathBuf,
    output: PathBuf,
}

impl Sides {
    fn new(directory: &Path, base: &str, ours: &str, theirs: &str) -> Self {
        let sides = Self {
            base: directory.join("base.jsonl"),
            ours: directory.join("ours.jsonl"),
            theirs: directory.join("theirs.jsonl"),
            output: directory.join("output.jsonl"),
        };
        std::fs::write(&sides.base, base).unwrap();
        std::fs::write(&sides.ours, ours).unwrap();
        std::fs::write(&sides.theirs, theirs).unwrap();
        sides
    }

    fn run(&self, path: &str) -> std::process::Output {
        provenance()
            .args([
                "merge-jsonl",
                self.base.to_str().unwrap(),
                self.ours.to_str().unwrap(),
                self.theirs.to_str().unwrap(),
                "--output",
                self.output.to_str().unwrap(),
                "--path",
                path,
                "--format",
                "json",
            ])
            .output()
            .unwrap()
    }
}

#[test]
#[verifies("rule_ste_merge_changed_statement_gate", examples)]
fn requirement_and_rule_merges_reject_selected_changed_statements_without_touching_output() {
    for (kind, path) in [("requirement", REQUIREMENTS_SHARD), ("rule", RULES_SHARD)] {
        let directory = tempfile::tempdir().unwrap();
        let base = record("record_a", "Original statement", kind);
        let ours = record("record_a", "Café; changed", kind);
        let sides = Sides::new(directory.path(), &base, &ours, &base);
        std::fs::write(&sides.output, b"sentinel output bytes").unwrap();

        let output = sides.run(path);

        assert!(!output.status.success());
        assert_eq!(
            std::fs::read(&sides.output).unwrap(),
            b"sentinel output bytes"
        );
        let report = error_json(&output);
        assert_eq!(report["error"], "asd_ste100_violations");
        assert_eq!(
            report["diagnostics"],
            Value::Array(vec![diagnostic(kind, "record_a", 5)])
        );

        let repeated = sides.run(path);
        assert!(!repeated.status.success());
        assert_eq!(error_json(&repeated), report);
        assert_eq!(
            std::fs::read(&sides.output).unwrap(),
            b"sentinel output bytes"
        );
    }
}

#[test]
#[verifies("rule_ste_merge_changed_statement_gate", examples)]
fn existing_three_way_selection_is_analyzed_after_both_sides_change() {
    let directory = tempfile::tempdir().unwrap();
    let first = record("req_a", "Original statement", "requirement");
    let second = record("req_b", "Second statement", "requirement");
    let base = first.clone() + &second;
    let ours = record("req_a", "Ours; invalid", "requirement") + &second;
    let mut theirs_value: Value = serde_json::from_str(second.trim()).unwrap();
    theirs_value["description"] = json!("Their metadata change");
    let theirs = first + &format!("{}\n", serde_json::to_string(&theirs_value).unwrap());
    let sides = Sides::new(directory.path(), &base, &ours, &theirs);

    let output = sides.run(REQUIREMENTS_SHARD);

    assert!(!output.status.success());
    assert_eq!(
        error_json(&output)["diagnostics"],
        Value::Array(vec![diagnostic("requirement", "req_a", 4)])
    );
    assert!(!sides.output.exists());
}

#[test]
#[verifies("rule_ste_merge_changed_statement_gate", examples)]
fn unchanged_legacy_invalid_record_does_not_block_an_unrelated_clean_merge() {
    let directory = tempfile::tempdir().unwrap();
    let legacy = record("req_legacy", "Legacy; invalid", "requirement");
    let clean = record("req_clean", "Clean addition", "requirement");
    let sides = Sides::new(
        directory.path(),
        &legacy,
        &legacy,
        &(legacy.clone() + &clean),
    );

    let output = sides.run(REQUIREMENTS_SHARD);

    assert!(
        output.status.success(),
        "{}",
        String::from_utf8_lossy(&output.stderr)
    );
    let merged = std::fs::read_to_string(&sides.output).unwrap();
    assert!(merged.contains("req_legacy"));
    assert!(merged.contains("req_clean"));
}

#[test]
#[verifies("rule_ste_merge_changed_statement_gate", examples)]
fn conflicted_changed_statement_reports_both_contracts_without_touching_output() {
    let directory = tempfile::tempdir().unwrap();
    let base = record("req_conflict", "Original statement", "requirement");
    let ours = record("req_conflict", "Ours; invalid", "requirement");
    let theirs = record("req_conflict", "Their clean change", "requirement");
    let sides = Sides::new(directory.path(), &base, &ours, &theirs);
    std::fs::write(&sides.output, b"sentinel output bytes").unwrap();

    let output = sides.run(REQUIREMENTS_SHARD);

    assert!(!output.status.success());
    assert_eq!(
        std::fs::read(&sides.output).unwrap(),
        b"sentinel output bytes"
    );
    let conflict: Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(conflict["status"], "conflicted");
    assert_eq!(conflict["conflicts"][0]["record_id"], "req_conflict");
    assert_eq!(
        error_json(&output)["diagnostics"],
        Value::Array(vec![diagnostic("requirement", "req_conflict", 4)])
    );
}