cobble-lang 0.6.3

A modern, Python-like language for creating Minecraft Data Packs
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};

fn cobble() -> Command {
    Command::new(env!("CARGO_BIN_EXE_cobble"))
}

fn write_source(temp_dir: &Path, source: &str) -> PathBuf {
    let input = temp_dir.join("main.cbl");
    fs::write(&input, source).unwrap();
    input
}

fn write_say_commands_json(temp_dir: &Path) -> PathBuf {
    let commands_json = temp_dir.join("commands-fixture.json");
    fs::write(
        &commands_json,
        r#"{
            "type": "root",
            "children": {
                "say": {
                    "type": "literal",
                    "children": {
                        "message": {
                            "type": "argument",
                            "parser": "minecraft:message",
                            "executable": true
                        }
                    }
                }
            }
        }"#,
    )
    .unwrap();
    commands_json
}

fn output_text(output: &Output) -> (String, String) {
    (
        String::from_utf8_lossy(&output.stdout).into_owned(),
        String::from_utf8_lossy(&output.stderr).into_owned(),
    )
}

#[test]
fn cli_doctor_reports_project_and_command_tree_status() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let commands_json = write_say_commands_json(temp_dir.path());
    fs::create_dir_all(temp_dir.path().join("src")).unwrap();
    fs::write(
        temp_dir.path().join("cobble.toml"),
        r#"
[project]
name = "doctor_project"
description = "Doctor regression project"
namespace = "doctor_project"
version = "1.0.0"
pack_format = "101.1"

[build]
source = "src"
output = "output"
entry_points = []
"#,
    )
    .unwrap();

    let output = cobble()
        .arg("doctor")
        .arg(temp_dir.path())
        .arg("--commands-json")
        .arg(&commands_json)
        .output()
        .unwrap();

    let (stdout, stderr) = output_text(&output);
    assert!(
        output.status.success(),
        "doctor failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(stdout.contains("Cobble doctor"));
    assert!(stdout.contains("Project: doctor_project"));
    assert!(stdout.contains("Namespace: doctor_project"));
    assert!(stdout.contains("Command tree:"));
    assert!(stdout.contains(commands_json.to_string_lossy().as_ref()));
    assert!(stdout.contains("SHA-1:"));
}

#[test]
fn cli_doctor_reports_missing_command_tree_without_download() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let missing_commands = temp_dir.path().join("missing-commands.json");

    let output = cobble()
        .arg("doctor")
        .arg(temp_dir.path())
        .arg("--commands-json")
        .arg(&missing_commands)
        .output()
        .unwrap();

    let (stdout, stderr) = output_text(&output);
    assert!(
        output.status.success(),
        "doctor failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(stdout.contains("Command tree: missing"));
    assert!(stdout.contains(missing_commands.to_string_lossy().as_ref()));
}

#[test]
fn cli_build_dry_run_does_not_write_final_output() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let input = write_source(temp_dir.path(), "def main():\n    /say dry run\n");
    let output_dir = temp_dir.path().join("output");

    let output = cobble()
        .arg("build")
        .arg(&input)
        .arg("--namespace")
        .arg("cli_regression")
        .arg("--output")
        .arg(&output_dir)
        .arg("--dry-run")
        .output()
        .unwrap();

    let (stdout, stderr) = output_text(&output);
    assert!(
        output.status.success(),
        "dry-run failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(stdout.contains("Dry run: final output will not be written"));
    assert!(stdout.contains("Build summary:"));
    assert!(stdout.contains("Output: not written (--dry-run)"));
    assert!(
        !output_dir.exists(),
        "dry-run should not create the final output directory"
    );
}

#[test]
fn cli_build_dry_run_validate_reports_validation_summary() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let commands_json = write_say_commands_json(temp_dir.path());
    let input = write_source(temp_dir.path(), "def main():\n    /say valid\n");
    let output_dir = temp_dir.path().join("output");

    let output = cobble()
        .arg("build")
        .arg(&input)
        .arg("--namespace")
        .arg("cli_regression")
        .arg("--output")
        .arg(&output_dir)
        .arg("--dry-run")
        .arg("--validate")
        .arg("--commands-json")
        .arg(&commands_json)
        .output()
        .unwrap();

    let (stdout, stderr) = output_text(&output);
    assert!(
        output.status.success(),
        "dry-run validation failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(stdout.contains("Validating generated commands"));
    assert!(stdout.contains("All commands valid"));
    assert!(stdout.contains("Validation:"));
    assert!(stdout.contains("Output: not written (--dry-run)"));
    assert!(!output_dir.exists());
}

#[test]
fn cli_build_rejects_dry_run_with_zip() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let input = write_source(temp_dir.path(), "def main():\n    /say invalid options\n");

    let output = cobble()
        .arg("build")
        .arg(&input)
        .arg("--dry-run")
        .arg("--zip")
        .output()
        .unwrap();

    let (_stdout, stderr) = output_text(&output);
    assert!(!output.status.success());
    assert!(stderr.contains("--dry-run cannot be combined with --zip"));
}

#[test]
fn cli_build_validate_prints_source_mapped_diagnostics() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let commands_json = write_say_commands_json(temp_dir.path());
    let input = write_source(temp_dir.path(), "def main():\n    /not_a_command\n");
    let output_dir = temp_dir.path().join("output");

    let output = cobble()
        .arg("build")
        .arg(&input)
        .arg("--namespace")
        .arg("cli_regression")
        .arg("--output")
        .arg(&output_dir)
        .arg("--validate")
        .arg("--commands-json")
        .arg(&commands_json)
        .output()
        .unwrap();

    let (stdout, stderr) = output_text(&output);
    assert!(!output.status.success());
    assert!(stdout.contains("Validating generated commands"));
    assert!(stderr.contains("not_a_command"));
    assert!(stderr.contains("^"));
    assert!(stderr.contains("source: main.cbl:2:5"));
    assert!(stderr.contains("validation error(s) found"));
}

#[test]
fn cli_inspect_json_reports_manifest_summary() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let input = write_source(temp_dir.path(), "def main():\n    /say inspect\n");
    let output_dir = temp_dir.path().join("output");

    let build = cobble()
        .arg("build")
        .arg(&input)
        .arg("--namespace")
        .arg("cli_regression")
        .arg("--output")
        .arg(&output_dir)
        .arg("--quiet")
        .output()
        .unwrap();
    let (build_stdout, build_stderr) = output_text(&build);
    assert!(
        build.status.success(),
        "build failed\nstdout:\n{build_stdout}\nstderr:\n{build_stderr}"
    );

    let inspect = cobble()
        .arg("inspect")
        .arg(&output_dir)
        .arg("--json")
        .output()
        .unwrap();
    let (stdout, stderr) = output_text(&inspect);
    assert!(
        inspect.status.success(),
        "inspect failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );

    let value: serde_json::Value = serde_json::from_str(&stdout).unwrap();
    assert_eq!(value["manifest"]["namespace"], "cli_regression");
    assert_eq!(value["manifest"]["generated"]["functions"], 1);
    assert_eq!(value["manifest"]["generated"]["commands"], 1);
    assert_eq!(value["source_map_entries"], 1);
}

#[test]
fn cli_inspect_human_output_reports_manifest_summary() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let input = write_source(temp_dir.path(), "def main():\n    /say inspect\n");
    let output_dir = temp_dir.path().join("output");

    let build = cobble()
        .arg("build")
        .arg(&input)
        .arg("--namespace")
        .arg("cli_regression")
        .arg("--output")
        .arg(&output_dir)
        .arg("--quiet")
        .output()
        .unwrap();
    let (build_stdout, build_stderr) = output_text(&build);
    assert!(
        build.status.success(),
        "build failed\nstdout:\n{build_stdout}\nstderr:\n{build_stderr}"
    );

    let inspect = cobble().arg("inspect").arg(&output_dir).output().unwrap();
    let (stdout, stderr) = output_text(&inspect);
    assert!(
        inspect.status.success(),
        "inspect failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
    );
    assert!(stdout.contains("Cobble inspect:"));
    assert!(stdout.contains("Namespace: cli_regression"));
    assert!(stdout.contains("Functions: 1"));
    assert!(stdout.contains("Commands: 1"));
    assert!(stdout.contains("Validation: not recorded"));
}

#[test]
fn cli_inspect_missing_manifest_fails_with_actionable_error() {
    let temp_dir = tempfile::TempDir::new().unwrap();

    let output = cobble()
        .arg("inspect")
        .arg(temp_dir.path())
        .output()
        .unwrap();

    let (_stdout, stderr) = output_text(&output);
    assert!(!output.status.success());
    assert!(stderr.contains("No Cobble build manifest found"));
    assert!(stderr.contains("Run `cobble build`"));
}

#[test]
fn cli_inspect_malformed_manifest_reports_parse_error() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let cobble_dir = temp_dir.path().join(".cobble");
    fs::create_dir_all(&cobble_dir).unwrap();
    fs::write(cobble_dir.join("build_manifest.json"), "{not json").unwrap();

    let output = cobble()
        .arg("inspect")
        .arg(temp_dir.path())
        .output()
        .unwrap();

    let (_stdout, stderr) = output_text(&output);
    assert!(!output.status.success());
    assert!(stderr.contains("Failed to parse"));
    assert!(stderr.contains("build_manifest.json"));
}