mecha10-cli 0.6.5

Mecha10 CLI tool
Documentation
//! Simulation command handler
//!
//! Orchestrates `mecha10 sim generate`/`sim run`, delegating the actual MJCF generation and
//! Gymnasium env stepping to the Python MuJoCo backend (see `crate::sim`).
//!
//! `sim push`/`sim pull` (LAB-1984, LAB-2021) were removed in LAB-2936: simulation assets
//! (models, environments) now live under `<project>/configs/simulation`, so `mecha10 config
//! push`/`pull` already syncs them generically - the dedicated sim-asset sync path they used
//! (`mecha10_robot_config::sim_assets`, since removed) is no longer needed.

use crate::commands::SimCommand;
use crate::context::CliContext;
use crate::sim;
use anyhow::Result;

/// Handle the `sim` command family.
pub async fn handle_sim(_ctx: &mut CliContext, command: &SimCommand) -> Result<()> {
    match command {
        SimCommand::Generate(args) => {
            println!();
            println!("🤖 Generating MuJoCo MJCF");
            println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            println!();

            sim::generate(args).await?;

            println!();
            println!("✅ Wrote MJCF to {}", args.out.display());
            Ok(())
        }
        SimCommand::Run(args) => {
            println!();
            println!("🤖 Running MuJoCo rollout");
            println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
            println!();

            sim::run(args).await?;

            println!();
            println!("✅ Ran {} steps", args.steps);
            Ok(())
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::{SimBackend, SimGenerateArgs, SimRunArgs};
    use crate::context::CliContextBuilder;
    use std::path::PathBuf;
    use tempfile::tempdir;

    /// Build a throwaway `CliContext` rooted at a fresh temp dir - `handle_sim`'s `Generate`/`Run`
    /// arms don't touch it, so this only needs to satisfy the type signature.
    fn test_ctx() -> (tempfile::TempDir, CliContext) {
        let dir = tempdir().unwrap();
        let ctx = CliContextBuilder::new()
            .working_dir(dir.path().to_path_buf())
            .build()
            .unwrap();
        (dir, ctx)
    }

    #[tokio::test]
    async fn handle_sim_generate_propagates_backend_error() {
        let Ok(_) = std::process::Command::new("python3").arg("--version").output() else {
            eprintln!("skipping: no python3 on PATH in this environment");
            return;
        };

        let (_dir, mut ctx) = test_ctx();
        let command = SimCommand::Generate(SimGenerateArgs {
            backend: SimBackend::Mujoco,
            model: "rover".to_string(),
            env: None,
            out: PathBuf::from("/tmp/mecha10-handle-sim-test-generate.xml"),
            assets_path: None,
        });

        let Err(err) = handle_sim(&mut ctx, &command).await else {
            panic!(
                "expected handle_sim to propagate a backend error in this environment \
                 (mecha10_rl not installed) rather than report success"
            );
        };

        let message = err.chain().map(ToString::to_string).collect::<Vec<_>>().join(": ");
        assert!(
            message.contains("MuJoCo backend not available") || message.contains("mecha10_rl.mujoco.cli exited with"),
            "expected the underlying backend error to survive handle_sim unchanged, got: {message}"
        );
    }

    #[tokio::test]
    async fn handle_sim_run_propagates_backend_error() {
        let Ok(_) = std::process::Command::new("python3").arg("--version").output() else {
            eprintln!("skipping: no python3 on PATH in this environment");
            return;
        };

        let (_dir, mut ctx) = test_ctx();
        let command = SimCommand::Run(SimRunArgs {
            backend: SimBackend::Mujoco,
            model: "rover".to_string(),
            env: "basic_arena".to_string(),
            steps: 10,
            assets_path: None,
        });

        let Err(err) = handle_sim(&mut ctx, &command).await else {
            panic!(
                "expected handle_sim to propagate a backend error in this environment \
                 (mecha10_rl not installed) rather than report success"
            );
        };

        let message = err.chain().map(ToString::to_string).collect::<Vec<_>>().join(": ");
        assert!(
            message.contains("MuJoCo backend not available") || message.contains("mecha10_rl.mujoco.cli exited with"),
            "expected the underlying backend error to survive handle_sim unchanged, got: {message}"
        );
    }
}