use crate::commands::SimCommand;
use crate::context::CliContext;
use crate::handlers::dev::ops::inject_locomotion_limits_env;
use crate::sim;
use anyhow::Result;
use std::collections::HashMap;
pub async fn handle_sim(ctx: &mut CliContext, command: &SimCommand) -> Result<()> {
let project_config = ctx.load_project_config().await?;
let mut env: HashMap<String, String> = HashMap::new();
inject_locomotion_limits_env(&project_config, &mut env);
match command {
SimCommand::Generate(args) => {
println!();
println!("🤖 Generating MuJoCo MJCF");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
sim::generate(args, &env).await?;
println!();
println!("✅ Wrote MJCF to {}", args.out.display());
Ok(())
}
SimCommand::Run(args) => {
println!();
println!("🤖 Running MuJoCo rollout");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
sim::run(args, &env).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;
fn test_ctx() -> (tempfile::TempDir, CliContext) {
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join("mecha10.json"),
r#"{
"name": "test-project",
"version": "0.1.0",
"robot_id": "00000000-0000-0000-0000-000000000000",
"robot_name": "test-project",
"robot_platform": "rover",
"actuator": {
"locomotion": {
"max_linear_vel": 10.0,
"max_angular_vel": 10.0,
"max_acceleration": 10.0
}
}
}"#,
)
.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}"
);
}
}