robotrt-cli 0.1.0-beta.2

RobotRT modular robotics runtime and middleware components.
use super::common::*;

#[test]
fn orchestrate_init_validate_plan_and_dry_run_json() {
    let tmp_dir = temp_dir("orchestrate").expect("create temp dir");
    let config = tmp_dir.join("pipeline.json");
    let config_arg = config.to_str().expect("utf-8 path");

    let init = run_cli(&["orchestrate", "init", "--output", config_arg, "--json"]);
    assert!(init.status.success(), "stderr: {}", init.stderr);

    let validate = run_cli(&["orchestrate", "validate", "--file", config_arg, "--json"]);
    assert!(validate.status.success(), "stderr: {}", validate.stderr);
    let validate_json: Value =
        serde_json::from_str(&validate.stdout).expect("orchestrate validate json payload");
    assert_eq!(
        validate_json["api_version"],
        Value::String(String::from("robotrt.orchestrate.validate.v1"))
    );
    assert_eq!(validate_json["status"], Value::String(String::from("pass")));

    let plan = run_cli(&[
        "orchestrate",
        "plan",
        "--file",
        config_arg,
        "--task",
        "release-candidate",
        "--json",
    ]);
    assert!(plan.status.success(), "stderr: {}", plan.stderr);
    let plan_json: Value =
        serde_json::from_str(&plan.stdout).expect("orchestrate plan json payload");
    assert_eq!(
        plan_json["api_version"],
        Value::String(String::from("robotrt.orchestrate.plan.v1"))
    );
    let planned = plan_json["planned_tasks"]
        .as_array()
        .expect("planned tasks array");
    assert!(!planned.is_empty(), "planned tasks should not be empty");
    assert!(
        planned.iter().any(|entry| entry == "gate-fast-validated"),
        "expected dependency task in plan"
    );
    assert!(
        planned.iter().any(|entry| entry == "release-candidate"),
        "expected target task in plan"
    );

    let dry_run = run_cli(&[
        "orchestrate",
        "run",
        "--file",
        config_arg,
        "--task",
        "release-candidate",
        "--dry-run",
        "--json",
    ]);
    assert!(dry_run.status.success(), "stderr: {}", dry_run.stderr);
    let dry_json: Value = serde_json::from_str(&dry_run.stdout).expect("orchestrate dry-run json");
    assert_eq!(dry_json["status"], Value::String(String::from("pass")));
    assert_eq!(dry_json["mode"], Value::String(String::from("dry_run")));
}

#[test]
fn orchestrate_validate_rejects_cycle() {
    let config = temp_path("orchestrate-cycle", "json");
    let config_arg = config.to_str().expect("utf-8 path");

    let payload = serde_json::json!({
        "api_version": "robotrt.orchestrate.v1",
        "project": "robotrt",
        "tasks": [
            {
                "name": "a",
                "command": ["bash", "scripts/gate-fast-validated.sh"],
                "depends_on": ["b"]
            },
            {
                "name": "b",
                "command": ["bash", "scripts/compat-matrix-gate.sh"],
                "depends_on": ["a"]
            }
        ]
    });
    fs::write(
        &config,
        format!(
            "{}\n",
            serde_json::to_string_pretty(&payload).expect("serialize cycle payload")
        ),
    )
    .expect("write cycle config");

    let validate = run_cli(&["orchestrate", "validate", "--file", config_arg, "--json"]);
    assert!(!validate.status.success(), "validate unexpectedly passed");
    assert!(
        validate.stderr.contains("dependency cycle detected")
            || validate.stderr.contains("orchestrate config invalid"),
        "unexpected stderr: {}",
        validate.stderr
    );
}

#[test]
fn orchestrate_up_status_down_service_lifecycle_json() {
    let tmp_dir = temp_dir("orchestrate-service").expect("create temp dir");
    let config = tmp_dir.join("service.json");
    let state = tmp_dir.join("runtime-state.json");
    let config_arg = config.to_str().expect("utf-8 config path");
    let state_arg = state.to_str().expect("utf-8 state path");

    let payload = serde_json::json!({
        "api_version": "robotrt.orchestrate.v1",
        "project": "robotrt",
        "tasks": [
            {
                "name": "svc",
                "command": ["bash", "-lc", "sleep 5"],
                "depends_on": [],
                "run_mode": "service",
                "group": "runtime"
            }
        ]
    });
    fs::write(
        &config,
        format!(
            "{}\n",
            serde_json::to_string_pretty(&payload).expect("serialize service payload")
        ),
    )
    .expect("write service config");

    let up = run_cli(&[
        "orchestrate",
        "up",
        "--file",
        config_arg,
        "--state-file",
        state_arg,
        "--json",
    ]);
    assert!(up.status.success(), "stderr: {}", up.stderr);
    let up_json: Value = serde_json::from_str(&up.stdout).expect("orchestrate up json payload");
    assert_eq!(up_json["status"], Value::String(String::from("pass")));

    let status = run_cli(&["orchestrate", "status", "--state-file", state_arg, "--json"]);
    assert!(status.status.success(), "stderr: {}", status.stderr);
    let status_json: Value =
        serde_json::from_str(&status.stdout).expect("orchestrate status json payload");
    let entries = status_json["entries"]
        .as_array()
        .expect("status entries array");
    assert_eq!(entries.len(), 1, "unexpected status entries: {entries:?}");
    assert_eq!(entries[0]["task"], Value::String(String::from("svc")));
    assert_eq!(entries[0]["alive"], Value::Bool(true));

    let down = run_cli(&[
        "orchestrate",
        "down",
        "--task",
        "svc",
        "--state-file",
        state_arg,
        "--json",
    ]);
    assert!(down.status.success(), "stderr: {}", down.stderr);
    let down_json: Value =
        serde_json::from_str(&down.stdout).expect("orchestrate down json payload");
    assert_eq!(down_json["status"], Value::String(String::from("pass")));

    let status_after_down = run_cli(&[
        "orchestrate",
        "status",
        "--state-file",
        state_arg,
        "--prune",
        "--json",
    ]);
    assert!(
        status_after_down.status.success(),
        "stderr: {}",
        status_after_down.stderr
    );
    let status_after_json: Value =
        serde_json::from_str(&status_after_down.stdout).expect("status after down json payload");
    let after_entries = status_after_json["entries"]
        .as_array()
        .expect("status after down entries array");
    assert!(after_entries.is_empty(), "expected empty runtime state");
}

#[test]
fn orchestrate_plan_supports_profile_overlay_and_group_filter() {
    let tmp_dir = temp_dir("orchestrate-profile").expect("create temp dir");
    let config = tmp_dir.join("pipeline.json");
    let overlay = tmp_dir.join("overlay.json");
    let config_arg = config.to_str().expect("utf-8 config path");
    let overlay_arg = overlay.to_str().expect("utf-8 overlay path");

    let config_payload = serde_json::json!({
        "api_version": "robotrt.orchestrate.v1",
        "project": "robotrt",
        "tasks": [
            {
                "name": "build",
                "command": ["bash", "-lc", "true"],
                "depends_on": [],
                "group": "build"
            },
            {
                "name": "deploy",
                "command": ["bash", "-lc", "true"],
                "depends_on": ["build"],
                "group": "deploy"
            }
        ],
        "profiles": {
            "ci": {
                "tasks": {
                    "deploy": {
                        "enabled": false
                    }
                }
            }
        }
    });
    fs::write(
        &config,
        format!(
            "{}\n",
            serde_json::to_string_pretty(&config_payload).expect("serialize profile payload")
        ),
    )
    .expect("write profile config");

    let overlay_payload = serde_json::json!({
        "api_version": "robotrt.orchestrate.overlay.v1",
        "add_tasks": [
            {
                "name": "lint",
                "command": ["bash", "-lc", "true"],
                "depends_on": [],
                "group": "build"
            }
        ]
    });
    fs::write(
        &overlay,
        format!(
            "{}\n",
            serde_json::to_string_pretty(&overlay_payload).expect("serialize overlay payload")
        ),
    )
    .expect("write overlay config");

    let validate = run_cli(&[
        "orchestrate",
        "validate",
        "--file",
        config_arg,
        "--profile",
        "ci",
        "--overlay",
        overlay_arg,
        "--json",
    ]);
    assert!(validate.status.success(), "stderr: {}", validate.stderr);

    let plan = run_cli(&[
        "orchestrate",
        "plan",
        "--file",
        config_arg,
        "--profile",
        "ci",
        "--overlay",
        overlay_arg,
        "--group",
        "build",
        "--json",
    ]);
    assert!(plan.status.success(), "stderr: {}", plan.stderr);
    let plan_json: Value =
        serde_json::from_str(&plan.stdout).expect("orchestrate plan json payload");
    let planned = plan_json["planned_tasks"]
        .as_array()
        .expect("planned tasks array");
    assert!(planned.iter().any(|entry| entry == "build"));
    assert!(planned.iter().any(|entry| entry == "lint"));
    assert!(
        planned.iter().all(|entry| entry != "deploy"),
        "deploy should be disabled by profile"
    );
}

#[test]
fn orchestrate_run_honors_max_parallel_for_independent_tasks() {
    let config = temp_path("orchestrate-parallel", "json");
    let config_arg = config.to_str().expect("utf-8 path");

    let payload = serde_json::json!({
        "api_version": "robotrt.orchestrate.v1",
        "project": "robotrt",
        "max_parallel": 1,
        "tasks": [
            {
                "name": "a",
                "command": ["bash", "-lc", "sleep 1"],
                "depends_on": []
            },
            {
                "name": "b",
                "command": ["bash", "-lc", "sleep 1"],
                "depends_on": []
            },
            {
                "name": "c",
                "command": ["bash", "-lc", "true"],
                "depends_on": ["a", "b"]
            }
        ]
    });
    fs::write(
        &config,
        format!(
            "{}\n",
            serde_json::to_string_pretty(&payload).expect("serialize parallel payload")
        ),
    )
    .expect("write parallel config");

    let started_at = std::time::Instant::now();
    let run = run_cli(&[
        "orchestrate",
        "run",
        "--file",
        config_arg,
        "--max-parallel",
        "2",
        "--json",
    ]);
    let elapsed = started_at.elapsed();

    assert!(run.status.success(), "stderr: {}", run.stderr);
    let run_json: Value = serde_json::from_str(&run.stdout).expect("orchestrate run json payload");
    assert_eq!(run_json["status"], Value::String(String::from("pass")));
    assert!(
        elapsed < std::time::Duration::from_millis(2500),
        "expected parallel scheduling to finish quickly, elapsed={elapsed:?}"
    );
}