use anyhow::{Context as _, Result};
use mecha10_cli_core::paths::config::behavior_config_candidates;
use serde_json::Value;
use std::path::{Path, PathBuf};
pub fn resolve_behavior_config_path(behavior_name: &str, project_root: &Path) -> Result<PathBuf> {
let tiers = behavior_config_candidates(behavior_name);
for tier in &tiers {
let path = project_root.join(tier);
if path.exists() {
return Ok(path);
}
}
anyhow::bail!(
"Behavior config '{}' not found - checked {}",
behavior_name,
tiers
.iter()
.map(|c| project_root.join(c).display().to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
pub async fn load_behavior_config(behavior_name: &str, project_root: &Path) -> Result<Value> {
let config_path = resolve_behavior_config_path(behavior_name, project_root)?;
load_json_file(&config_path).await.with_context(|| {
format!(
"Failed to load behavior config '{}' from {}",
behavior_name,
config_path.display()
)
})
}
async fn load_json_file(path: &Path) -> Result<Value> {
let content = tokio::fs::read_to_string(path)
.await
.with_context(|| format!("Failed to read file: {}", path.display()))?;
serde_json::from_str(&content).with_context(|| format!("Failed to parse JSON from: {}", path.display()))
}
pub fn get_current_environment() -> String {
mecha10_core::config::env_or_default("MECHA10_ENVIRONMENT", "dev")
}
pub fn detect_project_root() -> Result<PathBuf> {
let mut current = std::env::current_dir()?;
loop {
let mecha10_json = current.join("mecha10.json");
if mecha10_json.exists() {
return Ok(current);
}
if !current.pop() {
anyhow::bail!("Could not find project root (no mecha10.json found in parent directories)");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_behavior_config(project_root: &Path, relative_path: &str, name: &str) {
let full_path = project_root.join(relative_path);
fs::create_dir_all(full_path.parent().unwrap()).unwrap();
fs::write(
full_path,
format!(r#"{{"name": "{name}", "root": {{"type": "node", "node": "noop"}}}}"#),
)
.unwrap();
}
#[test]
fn resolve_behavior_config_path_finds_framework_default() {
let project = TempDir::new().unwrap();
write_behavior_config(
project.path(),
"configs/behaviors/@mecha10/idle_wander/config.json",
"framework",
);
let resolved = resolve_behavior_config_path("idle_wander", project.path()).unwrap();
assert_eq!(
resolved,
project
.path()
.join("configs/behaviors/@mecha10/idle_wander/config.json")
);
}
#[test]
fn resolve_behavior_config_path_project_override_wins_over_framework() {
let project = TempDir::new().unwrap();
write_behavior_config(
project.path(),
"configs/behaviors/@mecha10/idle_wander/config.json",
"framework",
);
write_behavior_config(project.path(), "configs/behaviors/idle_wander/config.json", "project");
let resolved = resolve_behavior_config_path("idle_wander", project.path()).unwrap();
assert_eq!(
resolved,
project.path().join("configs/behaviors/idle_wander/config.json")
);
}
#[test]
fn resolve_behavior_config_path_errors_when_no_tier_exists() {
let project = TempDir::new().unwrap();
let err = resolve_behavior_config_path("missing_behavior", project.path()).unwrap_err();
let message = err.to_string();
assert!(message.contains("missing_behavior"));
assert!(message.contains("configs/behaviors/missing_behavior/config.json"));
assert!(message.contains("configs/behaviors/@mecha10/missing_behavior/config.json"));
}
#[tokio::test]
async fn load_behavior_config_reads_resolved_tier() {
let project = TempDir::new().unwrap();
write_behavior_config(
project.path(),
"configs/behaviors/@mecha10/idle_wander/config.json",
"framework",
);
write_behavior_config(project.path(), "configs/behaviors/idle_wander/config.json", "project");
let config = load_behavior_config("idle_wander", project.path()).await.unwrap();
assert_eq!(config["name"], "project");
}
#[test]
fn test_get_current_environment_default() {
std::env::remove_var("MECHA10_ENVIRONMENT");
assert_eq!(get_current_environment(), "dev");
}
}