provenance-cli 0.2.3

Command-line interface for Provenance requirements traceability
use assert_cmd::Command;
use provenance_core::SUPPORTED_SCHEMA_VERSION;

#[test]
fn import_replaces_only_target_scope_and_removes_all_stale_target_shards() {
    let dir = tempfile::tempdir().unwrap();
    let repo = dir.path().join("repo");
    init(&repo);
    add_other_scope(&repo);
    create_source(&repo, "default", "source_target");
    create_source(&repo, "other", "source_other");
    create_requirement(&repo, "default", "requirement_target");
    create_requirement(&repo, "other", "requirement_other");
    seed_stale_target_shards(&repo);

    let export = dir.path().join("replacement.json");
    Command::cargo_bin("provenance")
        .unwrap()
        .args([
            "export",
            "--repo",
            repo.to_str().unwrap(),
            "--scope",
            "default",
            "--format",
            "json",
            "--output",
            export.to_str().unwrap(),
        ])
        .assert()
        .success();
    let mut replacement: serde_json::Value =
        serde_json::from_slice(&std::fs::read(&export).unwrap()).unwrap();
    replacement["sources"] = serde_json::json!([]);
    replacement["requirements"] = serde_json::json!([]);
    replacement["threads"] = serde_json::json!([]);
    replacement["messages"] = serde_json::json!([]);
    std::fs::write(&export, serde_json::to_vec(&replacement).unwrap()).unwrap();

    Command::cargo_bin("provenance")
        .unwrap()
        .args([
            "import",
            "--repo",
            repo.to_str().unwrap(),
            "--scope",
            "default",
            "--input",
            export.to_str().unwrap(),
        ])
        .assert()
        .success();

    let state = repo.join(".provenance/state");
    assert_eq!(
        std::fs::read_to_string(state.join("scopes/default/sources/source.jsonl")).unwrap(),
        ""
    );
    assert!(
        std::fs::read_to_string(state.join("scopes/other/sources/source.jsonl"))
            .unwrap()
            .contains("source_other")
    );
    assert!(!state
        .join("scopes/default/ideation/landings.jsonl")
        .exists());
    assert!(!state.join("scopes/default/threads/2025-01.jsonl").exists());
    Command::cargo_bin("provenance")
        .unwrap()
        .args([
            "check",
            "--repo",
            repo.to_str().unwrap(),
            "--format",
            "json",
        ])
        .assert()
        .success();
}

fn init(repo: &std::path::Path) {
    Command::cargo_bin("provenance")
        .unwrap()
        .args([
            "init",
            "--path",
            repo.to_str().unwrap(),
            "--scope",
            "default",
        ])
        .assert()
        .success();
}

fn add_other_scope(repo: &std::path::Path) {
    let path = repo.join(".provenance/state/manifest.json");
    let mut manifest: serde_json::Value =
        serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
    manifest["scopes"]
        .as_array_mut()
        .unwrap()
        .push(serde_json::json!({
            "id": "other", "path_prefix": "other"
        }));
    std::fs::write(path, serde_json::to_vec_pretty(&manifest).unwrap()).unwrap();
}

fn create_source(repo: &std::path::Path, scope: &str, id: &str) {
    Command::cargo_bin("provenance")
        .unwrap()
        .args([
            "sources",
            "create",
            "--repo",
            repo.to_str().unwrap(),
            "--scope",
            scope,
            "--id",
            id,
            "--name",
            id,
            "--source-type",
            "policy",
            "--format",
            "json",
        ])
        .assert()
        .success();
}

fn create_requirement(repo: &std::path::Path, scope: &str, id: &str) {
    Command::cargo_bin("provenance")
        .unwrap()
        .args([
            "requirements",
            "create",
            "--repo",
            repo.to_str().unwrap(),
            "--scope",
            scope,
            "--id",
            id,
            "--statement",
            id,
            "--status",
            "active",
            "--format",
            "json",
        ])
        .assert()
        .success();
}

fn seed_stale_target_shards(repo: &std::path::Path) {
    let scope = repo.join(".provenance/state/scopes/default");
    std::fs::create_dir_all(scope.join("ideation")).unwrap();
    std::fs::write(
        scope.join("ideation/landings.jsonl"),
        "{\"contributions\":[],\"synthesis_packets\":[],\"proposals\":[],\"assertions\":[],\"dispositions\":[]}\n",
    )
    .unwrap();
    std::fs::create_dir_all(scope.join("threads")).unwrap();
    std::fs::write(
        scope.join("threads/2025-01.jsonl"),
        format!("{{\"schema_version\":{},\"scope_id\":\"default\",\"id\":\"message_stale\",\"thread_id\":\"thread_stale\",\"role\":\"user\",\"body\":\"stale\",\"created_at\":1}}\n", SUPPORTED_SCHEMA_VERSION.0),
    )
    .unwrap();
}