mecha10-cli 0.8.0

Mecha10 CLI tool
Documentation
//! Real-binary integration tests.
//!
//! Spawns the actual built `mecha10` binary via [`assert_cmd`] to verify real CLI argument
//! parsing and multi-command workflows, rather than calling handlers in-process.

use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;

/// Spawn the real built `mecha10` binary (not a handler function call in-process).
fn mecha10() -> Command {
    Command::cargo_bin("mecha10").expect("mecha10 binary should be built by `cargo test`")
}

/// Minimal valid `mecha10.json`, matching `schemas/mecha10.schema.json`.
fn write_minimal_project(dir: &std::path::Path, name: &str) {
    fs::write(
        dir.join("mecha10.json"),
        format!(
            r#"{{
  "name": "{name}",
  "version": "0.1.0",
  "robot_id": "{name}-001",
  "nodes": {{}}
}}"#
        ),
    )
    .expect("failed to write mecha10.json fixture");
}

#[test]
fn top_level_help_lists_real_subcommands() {
    mecha10()
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("Mecha10 CLI"))
        .stdout(predicate::str::contains("init"))
        .stdout(predicate::str::contains("build"))
        .stdout(predicate::str::contains("upload"));
}

#[test]
fn version_flag_prints_a_version_and_exits_zero() {
    mecha10()
        .arg("--version")
        .assert()
        .success()
        .stdout(predicate::str::contains("mecha10"));
}

#[test]
fn unknown_subcommand_is_a_clap_usage_error() {
    mecha10()
        .arg("totally-not-a-real-command")
        .assert()
        .failure()
        .code(2)
        .stderr(predicate::str::contains("unrecognized subcommand"));
}

#[test]
fn build_without_a_target_subcommand_is_a_clap_usage_error() {
    mecha10().arg("build").assert().failure().code(2);
}

#[test]
fn init_help_documents_non_interactive_flags_without_touching_the_network() {
    mecha10()
        .arg("init")
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("--non-interactive"))
        .stdout(predicate::str::contains("--template"));
}

#[test]
fn validate_then_target_then_config_show_chain_against_a_real_project() {
    let project = TempDir::new().expect("failed to create temp project dir");
    write_minimal_project(project.path(), "test-robot");

    mecha10()
        .current_dir(project.path())
        .arg("validate")
        .assert()
        .success()
        .stdout(predicate::str::contains("is valid"));

    mecha10()
        .current_dir(project.path())
        .arg("target")
        .assert()
        .success()
        .stdout(predicate::str::contains("No targets configured"));

    mecha10()
        .current_dir(project.path())
        .arg("config")
        .arg("show")
        .assert()
        .success()
        .stdout(predicate::str::contains("Project name:"))
        .stdout(predicate::str::contains("test-robot"))
        .stdout(predicate::str::contains("test-robot-001"));
}

#[test]
fn config_validate_path_flag_targets_an_arbitrary_file_instead_of_the_project() {
    let project = TempDir::new().expect("failed to create temp project dir");
    let config_path = project.path().join("standalone.json");
    fs::write(
        &config_path,
        r#"{"name": "standalone-robot", "version": "1.0.0", "nodes": {}}"#,
    )
    .unwrap();

    mecha10()
        .current_dir(project.path())
        .arg("config")
        .arg("validate")
        .arg("--path")
        .arg("standalone.json")
        .assert()
        .success()
        .stdout(predicate::str::contains("standalone.json"))
        .stdout(predicate::str::contains("is valid"));
}

#[test]
fn validate_outside_a_project_fails_fast_with_exit_code_one() {
    let empty_dir = TempDir::new().expect("failed to create temp dir");

    mecha10()
        .current_dir(empty_dir.path())
        .arg("validate")
        .assert()
        .failure()
        .code(1)
        .stdout(predicate::str::contains("Not in a Mecha10 project directory"));
}

#[test]
fn target_outside_a_project_prints_guidance_and_exits_zero() {
    let empty_dir = TempDir::new().expect("failed to create temp dir");

    mecha10()
        .current_dir(empty_dir.path())
        .arg("target")
        .assert()
        .success()
        .stdout(predicate::str::contains("Not inside a mecha10 project"));
}

#[test]
fn validate_rejects_a_config_with_an_invalid_version_string() {
    let project = TempDir::new().expect("failed to create temp project dir");
    fs::write(
        project.path().join("mecha10.json"),
        r#"{"name": "bad-robot", "version": "latest", "nodes": {}}"#,
    )
    .unwrap();

    mecha10()
        .current_dir(project.path())
        .arg("validate")
        .assert()
        .failure()
        .code(1)
        .stdout(predicate::str::contains("Config validation failed"));
}

#[test]
fn upload_dry_run_without_a_built_binary_fails_offline_with_exit_code_one() {
    let project = TempDir::new().expect("failed to create temp project dir");
    write_minimal_project(project.path(), "test-robot");

    mecha10()
        .current_dir(project.path())
        .arg("upload")
        .arg("--dry-run")
        .assert()
        .failure()
        .code(1)
        .stdout(predicate::str::contains("Binary not found"));
}