forkctl 0.0.5

Control audited StGit downstream patch stacks
use std::fs;
use std::io::Write;
use std::process::{Command, Stdio};

fn forkctl() -> Command {
    Command::new(env!("CARGO_BIN_EXE_forkctl"))
}

#[test]
fn instructions_work_outside_a_repository() {
    let directory = tempfile::tempdir().unwrap();
    let output = forkctl()
        .arg("instructions")
        .current_dir(directory.path())
        .output()
        .unwrap();
    assert!(output.status.success());
    let stdout = String::from_utf8(output.stdout).unwrap();
    assert!(stdout.contains("mise run fork:init"));
    assert!(stdout.contains("Do not put generic executable fork lifecycle or presentation logic"));
}

#[test]
fn version_reports_package_version() {
    let output = forkctl().arg("--version").output().unwrap();
    assert!(output.status.success());
    assert_eq!(
        String::from_utf8(output.stdout).unwrap().trim(),
        format!("forkctl {}", env!("CARGO_PKG_VERSION"))
    );
}

#[test]
fn api_schema_describes_versioned_invocation_and_response() {
    let output = forkctl().args(["api", "schema"]).output().unwrap();
    assert!(output.status.success());
    let schema: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(schema["protocol_version"], 1);
    assert_eq!(
        schema["invocation"]["$schema"],
        "https://json-schema.org/draft/2020-12/schema"
    );
    assert_eq!(
        schema["response"]["$schema"],
        "https://json-schema.org/draft/2020-12/schema"
    );
}

#[test]
fn clap_and_json_adapters_return_the_same_typed_result() {
    let cli = forkctl()
        .args(["instructions", "--output", "json"])
        .output()
        .unwrap();
    assert!(cli.status.success());

    let invocation = serde_json::json!({
        "protocol_version": 1,
        "request": {"command": "instructions"}
    });
    let mut child = forkctl()
        .args(["api", "call"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();
    child
        .stdin
        .take()
        .unwrap()
        .write_all(serde_json::to_string(&invocation).unwrap().as_bytes())
        .unwrap();
    let api = child.wait_with_output().unwrap();
    assert!(api.status.success());

    let cli: serde_json::Value = serde_json::from_slice(&cli.stdout).unwrap();
    let api: serde_json::Value = serde_json::from_slice(&api.stdout).unwrap();
    assert_eq!(cli["result"], api["result"]);
}

#[test]
fn json_errors_are_schema_shaped_and_do_not_contaminate_stderr() {
    let invocation = serde_json::json!({
        "protocol_version": 2,
        "request": {"command": "instructions"}
    });
    let mut child = forkctl()
        .args(["api", "call"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    child
        .stdin
        .take()
        .unwrap()
        .write_all(serde_json::to_string(&invocation).unwrap().as_bytes())
        .unwrap();
    let output = child.wait_with_output().unwrap();
    assert!(!output.status.success());
    assert!(output.stderr.is_empty());
    let response: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(response["status"], "error");
    assert_eq!(response["protocol_version"], 1);
    assert_eq!(response["error"]["code"], "unsupported_protocol");
}

#[test]
fn json_alias_remains_compatible() {
    let output = forkctl().args(["instructions", "--json"]).output().unwrap();
    assert!(output.status.success());
    let response: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(response["status"], "success");
}

#[test]
fn invalid_manifest_fails_before_patch_commands() {
    let directory = tempfile::tempdir().unwrap();
    assert!(
        Command::new("git")
            .args(["init", "--quiet"])
            .current_dir(directory.path())
            .status()
            .unwrap()
            .success()
    );
    let manifest = directory.path().join("fork.json");
    let invalid =
        include_str!("../examples/fork.json").replacen("\"schema\": 1", "\"schema\": 2", 1);
    fs::write(&manifest, invalid).unwrap();

    let output = forkctl()
        .args(["--manifest", manifest.to_str().unwrap(), "verify"])
        .current_dir(directory.path())
        .output()
        .unwrap();
    assert!(!output.status.success());
    let stderr = String::from_utf8(output.stderr).unwrap();
    assert!(stderr.contains("unsupported manifest schema"), "{stderr}");
}