solverforge-cli 3.0.0

CLI for scaffolding and managing SolverForge constraint solver projects
use super::{
    mcp_servers_document, merge_vscode_config, resolve_binary, stdio_server_json, to_crate_name,
    vscode_servers_document,
};
use serde_json::{json, Value};

#[test]
fn stdio_entry_runs_the_binary_without_arguments() {
    let entry = stdio_server_json("/tmp/app/target/release/agent_optimizer");
    assert_eq!(entry["command"], "/tmp/app/target/release/agent_optimizer");
    assert_eq!(entry["args"], json!([]));
}

#[test]
fn generic_clients_use_the_mcp_servers_shape() {
    let document =
        mcp_servers_document("agent-optimizer", stdio_server_json("/bin/agent_optimizer"));
    assert_eq!(
        document["mcpServers"]["agent-optimizer"]["command"],
        "/bin/agent_optimizer"
    );
}

#[test]
fn vscode_uses_the_servers_shape() {
    let document =
        vscode_servers_document("agent-optimizer", stdio_server_json("/bin/agent_optimizer"));
    assert_eq!(
        document["servers"]["agent-optimizer"]["command"],
        "/bin/agent_optimizer"
    );
    assert!(document.get("mcpServers").is_none());
}

#[test]
fn merge_preserves_other_vscode_servers_and_adds_the_project_entry() {
    let existing = r#"{
      "servers": {
        "other-tool": { "command": "/bin/other" }
      }
    }"#;

    let merged = merge_vscode_config(Some(existing), "agent-optimizer", "/bin/agent_optimizer")
        .expect("merge succeeds");

    let parsed: Value = serde_json::from_str(&merged).expect("valid json");
    assert_eq!(parsed["servers"]["other-tool"]["command"], "/bin/other");
    assert_eq!(
        parsed["servers"]["agent-optimizer"]["command"],
        "/bin/agent_optimizer"
    );
    assert_eq!(parsed["servers"]["agent-optimizer"]["args"], json!([]));
    assert!(parsed["servers"]["agent-optimizer"].get("url").is_none());
    assert!(merged.ends_with('\n'));
}

#[test]
fn merge_creates_the_document_when_absent() {
    let merged = merge_vscode_config(None, "app", "/bin/app").expect("merge succeeds");
    let parsed: Value = serde_json::from_str(&merged).expect("valid json");
    assert_eq!(parsed["servers"]["app"]["command"], "/bin/app");
}

#[test]
fn merge_refuses_invalid_existing_json_instead_of_clobbering() {
    let error = merge_vscode_config(Some("not json"), "app", "/bin/app")
        .expect_err("invalid JSON must be rejected");
    assert!(
        error.to_string().contains("not valid JSON"),
        "unexpected error: {error}"
    );
}

#[test]
fn merge_refuses_non_object_servers_entry() {
    let error = merge_vscode_config(Some(r#"{"servers": "nope"}"#), "app", "/bin/app")
        .expect_err("non-object servers must be rejected");
    assert!(
        error.to_string().contains("non-object `servers`"),
        "unexpected error: {error}"
    );
}

#[test]
fn resolve_binary_prefers_release_and_reports_build_state() {
    let temp = tempfile::tempdir().expect("temp dir");
    let root = temp.path();
    let executable = format!("agent_optimizer{}", std::env::consts::EXE_SUFFIX);

    let (path, built) = resolve_binary(root, "agent_optimizer");
    assert!(!built, "no binary should be found yet");
    assert!(std::path::Path::new(&path)
        .ends_with(std::path::Path::new("target/release").join(&executable)));

    let release_dir = root.join("target/release");
    std::fs::create_dir_all(&release_dir).expect("create release dir");
    std::fs::write(release_dir.join(&executable), b"bin").expect("write binary");

    let (path, built) = resolve_binary(root, "agent_optimizer");
    assert!(built, "release binary must be detected");
    assert!(std::path::Path::new(&path)
        .ends_with(std::path::Path::new("target/release").join(&executable)));

    std::fs::remove_file(release_dir.join(&executable)).expect("remove release binary");
    let debug_dir = root.join("target/debug");
    std::fs::create_dir_all(&debug_dir).expect("create debug dir");
    std::fs::write(debug_dir.join(&executable), b"bin").expect("write debug binary");

    let (path, built) = resolve_binary(root, "agent_optimizer");
    assert!(built, "debug binary must be detected as a fallback");
    assert!(std::path::Path::new(&path)
        .ends_with(std::path::Path::new("target/debug").join(executable)));
}

#[test]
fn crate_name_matches_generated_binary_naming() {
    assert_eq!(to_crate_name("agent-optimizer"), "agent_optimizer");
    assert_eq!(to_crate_name("My-App"), "my_app");
}