mod common;
use common::TestProject;
use mecha10_cli::services::ConfigService;
use std::path::PathBuf;
#[test]
#[serial_test::serial(cwd)]
fn test_is_initialized_here() {
let project = TestProject::new().unwrap();
std::env::set_current_dir(&project.root).unwrap();
assert!(!ConfigService::is_initialized_here());
project
.create_file(
"mecha10.json",
r#"{
"robot_id": "test-001",
"nodes": {}
}"#,
)
.unwrap();
assert!(ConfigService::is_initialized_here());
}
#[test]
fn test_is_initialized() {
let project = TestProject::new().unwrap();
assert!(!ConfigService::is_initialized(&project.root));
project
.create_file(
"mecha10.json",
r#"{
"robot_id": "test-001",
"nodes": {}
}"#,
)
.unwrap();
assert!(ConfigService::is_initialized(&project.root));
}
#[tokio::test]
async fn test_load_from_specific_path() {
let project = TestProject::new().unwrap();
let config_path = project
.create_file(
"custom.json",
r#"{
"name": "custom-robot",
"version": "0.1.0",
"robot_id": "custom-robot",
"nodes": {}
}"#,
)
.unwrap();
let config = ConfigService::load_from(&config_path).await.unwrap();
assert_eq!(config.robot.id, "custom-robot");
}
#[tokio::test]
async fn test_load_from_nonexistent_file() {
let result = ConfigService::load_from(&PathBuf::from("/nonexistent/path/mecha10.json")).await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not found"));
}
#[tokio::test]
async fn test_load_invalid_json() {
let project = TestProject::new().unwrap();
let config_path = project.create_file("mecha10.json", "{ invalid json ").unwrap();
let result = ConfigService::load_from(&config_path).await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Failed to parse"));
}
#[tokio::test]
async fn test_load_robot_id() {
let project = TestProject::new().unwrap();
let config_path = project
.create_file(
"mecha10.json",
r#"{
"name": "robot-123",
"version": "0.1.0",
"robot_id": "robot-123",
"nodes": {}
}"#,
)
.unwrap();
let robot_id = ConfigService::load_robot_id(&config_path).await.unwrap();
assert_eq!(robot_id, "robot-123");
}
#[test]
fn test_find_config_in_current_dir() {
let project = TestProject::new().unwrap();
project
.create_file(
"mecha10.json",
r#"{
"robot_id": "test",
"nodes": {}
}"#,
)
.unwrap();
let found = ConfigService::find_config_from(&project.root).unwrap();
assert_eq!(found, project.root.join("mecha10.json"));
}
#[test]
fn test_find_config_in_parent_dir() {
let project = TestProject::new().unwrap();
project
.create_file(
"mecha10.json",
r#"{
"robot_id": "test",
"nodes": {}
}"#,
)
.unwrap();
let subdir = project.root.join("subdir/nested");
std::fs::create_dir_all(&subdir).unwrap();
let found = ConfigService::find_config_from(&subdir).unwrap();
assert_eq!(found, project.root.join("mecha10.json"));
}
#[test]
fn test_find_config_not_found() {
let project = TestProject::new().unwrap();
let result = ConfigService::find_config_from(&project.root);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("No mecha10.json found"));
}
#[test]
fn test_validate_nonexistent_file() {
let result = ConfigService::validate(&PathBuf::from("/nonexistent/mecha10.json"));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn test_resolve_and_validate_missing_explicit_path() {
let result = ConfigService::resolve_and_validate(Some(&PathBuf::from("/nonexistent/config.json")));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
fn test_resolve_and_validate_explicit_path_rejects_bare_node_config() {
let project = TestProject::new().unwrap();
let node_config_path = project
.create_file(
"config/camera.json",
r#"{"id": "camera_node", "executable": "./bin/camera_node"}"#,
)
.unwrap();
let result = ConfigService::resolve_and_validate(Some(&node_config_path));
assert!(
result.is_err(),
"a bare node config has no project to resolve its schema from"
);
let err_chain = format!("{:#}", result.unwrap_err());
assert!(
err_chain.contains("cannot validate a node configuration file on its own"),
"error chain should explain that a bare node config needs project context: {}",
err_chain
);
}
#[test]
fn test_resolve_and_validate_explicit_path_rejects_bare_node_config_even_when_invalid() {
let project = TestProject::new().unwrap();
let node_config_path = project
.create_file("config/camera.json", r#"{"id": "camera_node"}"#)
.unwrap();
let result = ConfigService::resolve_and_validate(Some(&node_config_path));
assert!(result.is_err());
let err_chain = format!("{:#}", result.unwrap_err());
assert!(
err_chain.contains("cannot validate a node configuration file on its own"),
"a bare node config fails on missing project context before its own fields are even \
checked, regardless of whether those fields are otherwise valid: {}",
err_chain
);
}
#[test]
#[serial_test::serial(cwd)]
fn test_resolve_and_validate_explicit_path_to_project_config_recurses_into_referenced_node_configs() {
let project = TestProject::new().unwrap();
std::env::set_current_dir(&project.root).unwrap();
project
.create_file(
"config/camera.json",
r#"{"id": "camera_node", "executable": "./bin/camera_node"}"#,
)
.unwrap();
project
.create_file(
"drivers/camera/camera.schema.json",
r#"{
"type": "object",
"required": ["id", "executable"],
"properties": {
"id": { "type": "string" },
"executable": { "type": "string" }
}
}"#,
)
.unwrap();
let project_config_path = project
.create_file(
"mecha10.json",
r#"{
"name": "my-robot",
"version": "1.0.0",
"nodes": {
"drivers": [
{"name": "camera", "path": "./drivers/camera", "config": "./config/camera.json"}
]
}
}"#,
)
.unwrap();
let result = ConfigService::resolve_and_validate(Some(&project_config_path));
assert!(
result.is_ok(),
"expected project + referenced node config to pass: {:?}",
result.err()
);
assert_eq!(result.unwrap(), project_config_path);
}
#[test]
#[serial_test::serial(cwd)]
fn test_resolve_and_validate_default_recurses_into_referenced_node_configs() {
let project = TestProject::new().unwrap();
std::env::set_current_dir(&project.root).unwrap();
project
.create_file(
"config/camera.json",
r#"{"id": "camera_node", "executable": "./bin/camera_node"}"#,
)
.unwrap();
project
.create_file(
"drivers/camera/camera.schema.json",
r#"{
"type": "object",
"required": ["id", "executable"],
"properties": {
"id": { "type": "string" },
"executable": { "type": "string" }
}
}"#,
)
.unwrap();
project
.create_file(
"mecha10.json",
r#"{
"name": "my-robot",
"version": "1.0.0",
"nodes": {
"drivers": [
{"name": "camera", "path": "./drivers/camera", "config": "./config/camera.json"}
]
}
}"#,
)
.unwrap();
let result = ConfigService::resolve_and_validate(None);
assert!(
result.is_ok(),
"expected project + referenced node config to pass: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_config_with_targets() {
let project = TestProject::new().unwrap();
let config_path = project
.create_file(
"mecha10.json",
r#"{
"name": "target-robot",
"version": "0.1.0",
"robot_id": "target-robot",
"nodes": {},
"targets": {
"dev": { "nodes": ["@mecha10/listener"] },
"robot": { "nodes": ["@mecha10/motor"], "platforms": ["aarch64-unknown-linux-gnu"] }
}
}"#,
)
.unwrap();
let config = ConfigService::load_from(&config_path).await.unwrap();
assert_eq!(config.robot.id, "target-robot");
assert_eq!(config.targets.nodes_for("dev"), ["@mecha10/listener".to_string()]);
let robot = config.targets.robot.expect("targets.robot should be present");
assert_eq!(robot.nodes, vec!["@mecha10/motor".to_string()]);
assert_eq!(robot.platforms, Some(vec!["aarch64-unknown-linux-gnu".to_string()]));
}
#[tokio::test]
async fn test_config_with_nodes() {
let project = TestProject::new().unwrap();
let config_path = project
.create_file(
"mecha10.json",
r#"{
"name": "node-robot",
"version": "0.1.0",
"robot_id": "node-robot",
"nodes": {
"@mecha10/camera": "0.1.0",
"custom-node": "0.1.0"
}
}"#,
)
.unwrap();
let config = ConfigService::load_from(&config_path).await.unwrap();
assert_eq!(config.nodes.len(), 2);
assert!(config.nodes.contains("camera"));
assert!(config.nodes.contains("custom-node"));
}