mecha10_cli_core/paths.rs
1//! Centralized path definitions for the CLI
2//!
3//! All hardcoded paths should be defined here to ensure consistency
4//! and make it easy to update paths across the codebase.
5//!
6//! # Organization
7//!
8//! - **Project paths**: Paths relative to project root (mecha10.json location)
9//! - **User paths**: Paths in user's home directory (~/.mecha10)
10//! - **Docker paths**: Paths used inside Docker containers
11//! - **Framework paths**: Paths within the mecha10 framework source
12
13#![allow(dead_code)]
14
15/// Project configuration file
16pub const PROJECT_CONFIG: &str = "mecha10.json";
17
18/// Project directories
19pub mod project {
20 /// Node source code directory
21 pub const NODES_DIR: &str = "nodes";
22
23 /// Framework node (`@mecha10` scope) schema directory, under the project's `nodes/`
24 /// directory rather than `configs/nodes/` (wave 10). This is where a framework node's
25 /// `<name>.schema.json` lives -- the `nodes/`-rooted counterpart to
26 /// `config::NODES_MECHA10_DIR` (which now carries only `config.json`).
27 pub const NODES_MECHA10_DIR: &str = "nodes/@mecha10";
28
29 /// Get the framework node's schema *directory* (no `<name>.schema.json` suffix) under
30 /// `nodes/@mecha10/<node_name>` -- for callers that need to copy a whole schema template
31 /// subtree into it (e.g. `mecha10 add`'s `copy_node_configs`), the `nodes/`-rooted sibling
32 /// of `config::framework_node_dir`.
33 pub fn framework_node_schema_dir(node_name: &str) -> String {
34 format!("{}/{}", NODES_MECHA10_DIR, node_name)
35 }
36
37 /// Driver source code directory
38 pub const DRIVERS_DIR: &str = "drivers";
39
40 /// Types directory
41 pub const TYPES_DIR: &str = "types";
42
43 /// Behavior trees directory - nested under `configs/` (LAB-2072) so `mecha10 config
44 /// push`/`pull`/`list` genuinely sync it like any other config, instead of living as a
45 /// sibling top-level directory `collect_configs_from_dir` never walked.
46 pub const BEHAVIORS_DIR: &str = "configs/behaviors";
47
48 /// Models directory (ONNX models, etc.)
49 pub const MODELS_DIR: &str = "models";
50
51 /// Model catalog subdirectory, bind-mounted into the `ollama` Compose service as
52 /// `/root/.ollama/models` (see `docker-compose.yml.template`). Must exist on disk before
53 /// `mecha10 dev`'s first `docker compose up` for a project that only uses an Ollama-backed
54 /// node (e.g. `llm-command`) and never pulls an ONNX catalog model - otherwise this
55 /// subdirectory is only ever created lazily by `ModelService::catalog_model_dir` on a
56 /// catalog pull, and Docker's own auto-create of a missing bind-mount target can fail with
57 /// "mkdir /root/.ollama: no such file or directory" on hosts where it can't create nested
58 /// missing directories through the mount.
59 pub const MODELS_CATALOG_DIR: &str = "models/catalog";
60
61 /// Logs directory
62 pub const LOGS_DIR: &str = "logs";
63
64 /// Simulation directory - nested under `configs/` (moved from a top-level `simulation/`
65 /// sibling) so `mecha10 config push`/`pull` syncs it like any other config, the same reason
66 /// `BEHAVIORS_DIR` moved under `configs/` in LAB-2072.
67 pub const SIMULATION_DIR: &str = "configs/simulation";
68
69 /// Simulation models directory
70 pub const SIMULATION_MODELS_DIR: &str = "configs/simulation/models";
71
72 /// Simulation environments directory
73 pub const SIMULATION_ENVIRONMENTS_DIR: &str = "configs/simulation/environments";
74
75 /// Simulation billboard-image assets directory (`type: "image"` `environment.json`/
76 /// `task.json` objects' `source` fields point here) - flat, no per-environment nesting: a
77 /// project only ever has one pool of these images, so the earlier shared-vs-env-specific
78 /// tiering (`mecha10_cli_core::simulation_assets`, now removed) was unneeded complexity.
79 /// Nested under `SIMULATION_DIR` (not a top-level `assets/` sibling) so `mecha10 config
80 /// push`/`pull` covers it like every other simulation asset - the previous top-level
81 /// `<project>/assets/` location was never synced anywhere, which silently broke remote RL
82 /// training for any environment with an image billboard.
83 pub const SIMULATION_IMAGES_DIR: &str = "configs/simulation/images";
84
85 /// RL task-curriculum configs directory (LAB-2430) - scaffolded from the templates
86 /// package's bundled catalog at `mecha10 init` time. Deliberately NOT under
87 /// `SIMULATION_MODELS_DIR`/`SIMULATION_ENVIRONMENTS_DIR`'s `simulation/` tree (LAB-2429):
88 /// task-curriculum configs are a project's RL configuration (room_size, target_range,
89 /// obstacle_count, ...), not a physics/rendering asset, so they live alongside the rest of a
90 /// project's RL config (`configs/rl/jobs/`) instead. `mecha10-rl`'s
91 /// `AssetResolver` resolves task configs (e.g. `task1`) from here for training
92 /// (`mecha10 train`); `mecha10_rl.mujoco.cli serve --assets-path` (supervised by
93 /// `packages/nodes/simulator`, live dev-mode physics only) never reads task configs at all.
94 pub const RL_TASKS_DIR: &str = "configs/rl/tasks";
95
96 /// Simulation output directory (generated robot/scene files)
97 pub const SIMULATION_OUTPUT_DIR: &str = "configs/simulation/output";
98
99 /// Cargo build target directory
100 pub const TARGET_DIR: &str = "target";
101
102 /// Source directory
103 pub const SRC_DIR: &str = "src";
104
105 /// Local (gitignored) project-scoped state directory - mirrors the `~/.mecha10`
106 /// user-home convention (`user::MECHA10_DIR`) but scoped to this project checkout, for
107 /// caches/state that are specific to one clone and shouldn't be committed to git (e.g.
108 /// `mecha10 dev`'s Python dependency preflight cache - see
109 /// `DEV_PREFLIGHT_CACHE_FILE`/`handlers/dev/ops/preflight.rs` in the `mecha10-cli` crate).
110 pub const STATE_DIR: &str = ".mecha10";
111
112 /// Cache file for `mecha10 dev`'s project-level Python dependency preflight gate - see
113 /// `handlers/dev/ops/preflight.rs` in the `mecha10-cli` crate. Purely a speed
114 /// optimization: a missing or stale-and-discarded cache always falls back to a real
115 /// scan, so nothing about correctness depends on this file existing or being fresh.
116 pub const DEV_PREFLIGHT_CACHE_FILE: &str = ".mecha10/dev-preflight-cache.json";
117}
118
119/// Configuration paths within project
120pub mod config {
121 use super::project;
122
123 /// Base configs directory
124 pub const DIR: &str = "configs";
125
126 /// Node configs base directory
127 pub const NODES_DIR: &str = "configs/nodes";
128
129 /// Framework node configs (@mecha10 scope)
130 pub const NODES_MECHA10_DIR: &str = "configs/nodes/@mecha10";
131
132 /// Framework-tier (`@mecha10`) `config.json` path for `name` under `category_dir`.
133 fn framework_config(category_dir: &str, name: &str) -> String {
134 format!("{category_dir}/@mecha10/{name}/config.json")
135 }
136
137 /// Project-tier (plain name) `config.json` path for `name` under `category_dir`.
138 fn project_config(category_dir: &str, name: &str) -> String {
139 format!("{category_dir}/{name}/config.json")
140 }
141
142 /// The 2-tier `config.json` lookup candidates for `name` under `category_dir`, in canonical
143 /// resolution order: framework (`@mecha10`) -> project (plain name).
144 fn config_candidates(category_dir: &str, name: &str) -> [String; 2] {
145 [framework_config(category_dir, name), project_config(category_dir, name)]
146 }
147
148 /// Get config path for a framework node
149 pub fn framework_node(node_name: &str) -> String {
150 framework_config(NODES_DIR, node_name)
151 }
152
153 /// Get config path for a project node (plain name)
154 pub fn project_node(node_name: &str) -> String {
155 project_config(NODES_DIR, node_name)
156 }
157
158 /// Get the framework node's config *directory* (no `config.json` suffix) - the `@mecha10`
159 /// counterpart to [`framework_node`], for callers that need the directory itself (e.g. to
160 /// copy a whole config template tree into it) rather than the `config.json` file path.
161 pub fn framework_node_dir(node_name: &str) -> String {
162 format!("{}/{}", NODES_MECHA10_DIR, node_name)
163 }
164
165 /// The 2-tier `config.json` lookup candidates for a node, in canonical resolution order:
166 /// framework (`@mecha10`) -> project (plain name). This is the ONE place that knows the tier
167 /// order/naming convention for "does this node have a config.json, and if so which scope is
168 /// it in" - callers that need to check both tiers (e.g. "use the first one that exists on
169 /// disk") should use this rather than hand-rolling the same `format!()`/literal strings
170 /// themselves, which is exactly what caused LAB-1982.
171 pub fn node_config_candidates(node_name: &str) -> [String; 2] {
172 config_candidates(NODES_DIR, node_name)
173 }
174
175 /// Get config path for a framework-default behavior (bundled into the project at scaffold
176 /// time under `configs/behaviors/@mecha10/<name>/config.json`).
177 pub fn framework_behavior(behavior_name: &str) -> String {
178 framework_config(project::BEHAVIORS_DIR, behavior_name)
179 }
180
181 /// Get the framework behavior's directory (no `config.json` suffix) - the `@mecha10`
182 /// counterpart to [`framework_behavior`], for callers that need the directory itself (e.g.
183 /// to copy a whole behavior-config template tree into it, as `mecha10 config sync` does to
184 /// bring newly-added bundled behaviors into an already-scaffolded project).
185 pub fn framework_behavior_dir(behavior_name: &str) -> String {
186 format!("{}/@mecha10/{}", project::BEHAVIORS_DIR, behavior_name)
187 }
188
189 /// Get config path for a project behavior (plain name:
190 /// `configs/behaviors/<name>/config.json`).
191 pub fn project_behavior(behavior_name: &str) -> String {
192 project_config(project::BEHAVIORS_DIR, behavior_name)
193 }
194
195 /// The 2-tier `config.json` lookup candidates for a behavior, in override-precedence order
196 /// (highest first): project (plain name) -> framework (`@mecha10`). A project-committed
197 /// override beats the bundled framework default. There is no `@local` tier for behaviors
198 /// (LAB-1990). See [`node_config_candidates`] for the sibling helper for nodes.
199 pub fn behavior_config_candidates(behavior_name: &str) -> [String; 2] {
200 [project_behavior(behavior_name), framework_behavior(behavior_name)]
201 }
202}
203
204/// Docker-related paths within project
205pub mod docker {
206 /// Docker directory
207 pub const DIR: &str = "docker";
208
209 /// Main docker-compose file
210 pub const COMPOSE_FILE: &str = "docker/docker-compose.yml";
211
212 /// Remote nodes docker-compose file
213 pub const COMPOSE_REMOTE_FILE: &str = "docker/docker-compose.remote.yml";
214
215 /// Remote nodes Dockerfile
216 pub const DOCKERFILE_REMOTE: &str = "docker/Dockerfile.remote";
217
218 /// Remote container entrypoint script (execs mecha10-launcher)
219 pub const ENTRYPOINT_REMOTE: &str = "docker/entrypoint.sh";
220
221 /// rl target docker-compose file - runs a trained RL policy in production
222 pub const COMPOSE_RL_FILE: &str = "docker/docker-compose.rl.yml";
223
224 /// rl target Dockerfile
225 pub const DOCKERFILE_RL: &str = "docker/Dockerfile.rl";
226
227 /// rl target container entrypoint script (execs mecha10-launcher --target rl) - a distinct
228 /// filename from `ENTRYPOINT_REMOTE` since both `docker/entrypoint.sh` and this file coexist
229 /// in the same project (remote and rl are independent, both-scaffolded targets).
230 pub const ENTRYPOINT_RL: &str = "docker/entrypoint-rl.sh";
231}
232
233/// Environment files
234pub mod env {
235 /// Environment example file
236 pub const EXAMPLE: &str = ".env.example";
237
238 /// Environment file
239 pub const FILE: &str = ".env";
240}
241
242/// Meta files (README, gitignore, etc.)
243pub mod meta {
244 /// README file
245 pub const README: &str = "README.md";
246
247 /// Git ignore file
248 pub const GITIGNORE: &str = ".gitignore";
249
250 /// Package.json for Node.js tooling
251 pub const PACKAGE_JSON: &str = "package.json";
252
253 /// Python requirements
254 pub const REQUIREMENTS_TXT: &str = "requirements.txt";
255
256 /// Pinned Python version for `mecha10 setup` to target (see
257 /// `find_python_executable` in `packages/cli/src/handlers/setup.rs`) -
258 /// matches the exact-version-pin pattern `scripts/dev/setup.py` already
259 /// uses for framework-contributor setup, applied here to scaffolded
260 /// end-user projects too.
261 pub const PYTHON_VERSION: &str = ".python-version";
262}
263
264/// Rust project files
265pub mod rust {
266 /// Cargo workspace manifest
267 pub const CARGO_TOML: &str = "Cargo.toml";
268
269 /// Cargo lock file
270 pub const CARGO_LOCK: &str = "Cargo.lock";
271
272 /// Main entry point
273 pub const MAIN_RS: &str = "src/main.rs";
274
275 /// Library entry point
276 pub const LIB_RS: &str = "src/lib.rs";
277
278 /// Build script
279 pub const BUILD_RS: &str = "build.rs";
280
281 /// Rustfmt config
282 pub const RUSTFMT_TOML: &str = "rustfmt.toml";
283}
284
285/// Model files within a model directory
286pub mod model {
287 /// ONNX model file
288 pub const ONNX_FILE: &str = "model.onnx";
289
290 /// Labels file
291 pub const LABELS_FILE: &str = "labels.txt";
292
293 /// Model config file
294 pub const CONFIG_FILE: &str = "config.json";
295}
296
297/// User-level paths in home directory
298pub mod user {
299 use std::path::PathBuf;
300
301 /// Base mecha10 directory in user home.
302 ///
303 /// Re-exports `mecha10_core::fs_utils::MECHA10_DIR`, the single source of truth for this
304 /// literal shared with `launcher-service` (which can't depend on this crate - see that
305 /// constant's doc comment) - kept as a `mecha10-cli-core`-local constant (rather than
306 /// requiring callers to reach into `mecha10-core` directly) so existing callers of
307 /// `paths::user::MECHA10_DIR` don't need to change (LAB-1999).
308 pub const MECHA10_DIR: &str = mecha10_core::fs_utils::MECHA10_DIR;
309
310 /// Credentials file, relative to the user home directory.
311 ///
312 /// Documentation-only - `mecha10-auth`'s `credentials::default_credentials_path()` is the
313 /// actual source of truth for this path (see `credentials_file()` below), since
314 /// `mecha10-auth` can't depend back on this crate (`mecha10-cli-core` already depends on
315 /// `mecha10-auth`, so the reverse edge would be a dependency cycle).
316 pub const CREDENTIALS_FILE: &str = ".mecha10/credentials.json";
317
318 /// Templates cache directory
319 pub const TEMPLATES_DIR: &str = ".mecha10/templates";
320
321 /// Binary installation directory
322 pub const BIN_DIR: &str = ".mecha10/bin";
323
324 /// Get the base mecha10 directory path
325 pub fn mecha10_dir() -> PathBuf {
326 dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")).join(MECHA10_DIR)
327 }
328
329 /// Get the credentials file path.
330 ///
331 /// Delegates to `mecha10-auth`'s `credentials::default_credentials_path()`, which is the
332 /// single implementation of this resolution logic (see the `CREDENTIALS_FILE` doc comment
333 /// for why the dependency runs in this direction).
334 pub fn credentials_file() -> PathBuf {
335 mecha10_auth::default_credentials_path()
336 }
337
338 /// Get the templates cache directory
339 pub fn templates_dir() -> PathBuf {
340 mecha10_dir().join("templates")
341 }
342
343 /// Get the binary directory
344 pub fn bin_dir() -> PathBuf {
345 mecha10_dir().join("bin")
346 }
347
348 /// Get path to a binary in the mecha10 bin directory
349 pub fn bin(name: &str) -> PathBuf {
350 bin_dir().join(name)
351 }
352
353 /// Get path to a binary in cargo bin directory
354 pub fn cargo_bin(name: &str) -> PathBuf {
355 home_bin_dir(".cargo", name)
356 }
357
358 /// Get path to a binary in local bin directory
359 pub fn local_bin(name: &str) -> PathBuf {
360 home_bin_dir(".local", name)
361 }
362
363 /// Get path to a binary in a `bin` subdirectory of the given home-relative directory
364 /// (e.g. `~/.cargo/bin/{name}`, `~/.local/bin/{name}`).
365 fn home_bin_dir(home_subdir: &str, name: &str) -> PathBuf {
366 mecha10_core::fs_utils::home_subpath(&[home_subdir, "bin", name])
367 }
368}
369
370/// Paths used inside Docker containers
371pub mod container {
372 /// Project mount point in container
373 pub const PROJECT_ROOT: &str = "/project";
374
375 /// Framework mount point in container
376 pub const FRAMEWORK_ROOT: &str = "/mecha10";
377
378 /// App directory (for mecha10-remote)
379 pub const APP_ROOT: &str = "/app";
380
381 /// Config file in mecha10-remote container
382 pub const REMOTE_CONFIG: &str = "/app/mecha10.json";
383
384 /// Get project-relative path inside container
385 pub fn project_path(relative: &str) -> String {
386 format!("{}/{}", PROJECT_ROOT, relative)
387 }
388
389 /// Get framework-relative path inside container
390 pub fn framework_path(relative: &str) -> String {
391 format!("{}/{}", FRAMEWORK_ROOT, relative)
392 }
393}
394
395/// Paths within the mecha10 framework source
396pub mod framework {
397 /// Packages directory
398 pub const PACKAGES_DIR: &str = "packages";
399
400 /// Nodes package directory
401 pub const NODES_DIR: &str = "packages/nodes";
402
403 /// Drivers package directory
404 pub const DRIVERS_DIR: &str = "packages/drivers";
405
406 /// Simulation catalog directory - the checked-in template source, not a separate package.
407 /// The old `packages/simulation` package was folded into the templates crate as its single
408 /// source of truth, replacing the release-time copy step.
409 pub const SIMULATION_DIR: &str = "packages/mecha10-templates/templates/configs/simulation";
410
411 /// Simulation models directory
412 pub const SIMULATION_MODELS_DIR: &str = "packages/mecha10-templates/templates/configs/simulation/models";
413
414 /// Simulation environments directory
415 pub const SIMULATION_ENVIRONMENTS_DIR: &str =
416 "packages/mecha10-templates/templates/configs/simulation/environments";
417
418 /// Simulation billboard-image assets directory - the framework-tree counterpart to
419 /// `project::SIMULATION_IMAGES_DIR`.
420 pub const SIMULATION_IMAGES_DIR: &str = "packages/mecha10-templates/templates/configs/simulation/images";
421
422 /// Robot tasks environments directory (for environment matching)
423 pub const ROBOT_TASKS_DIR: &str =
424 "packages/mecha10-templates/templates/configs/simulation/environments/robot-tasks";
425
426 /// Robot tasks catalog file
427 pub const ROBOT_TASKS_CATALOG: &str =
428 "packages/mecha10-templates/templates/configs/simulation/environments/robot-tasks/catalog.json";
429
430 /// Marker directory used to detect the mecha10 framework root
431 pub const ROOT_MARKER_DIR: &str = "packages/core";
432
433 /// Release binary
434 pub const RELEASE_BINARY: &str = "target/release/mecha10";
435
436 /// Debug binary
437 pub const DEBUG_BINARY: &str = "target/debug/mecha10";
438
439 /// Get node config path within framework
440 pub fn node_config(node_name: &str) -> String {
441 format!("packages/nodes/{}/configs/config.json", node_name)
442 }
443}
444
445/// External service URLs
446pub mod urls {
447 /// Default base URL for the public `mecha10-api` service.
448 ///
449 /// The CLI never talks to the private homelab Minio instance directly - all
450 /// framework/runtime artifact downloads (CLI/launcher/node binaries, simulation
451 /// assets, templates, remote-image manifests, ML models) are proxied through
452 /// `mecha10-api`'s `/api/downloads/*` routes (see `packages/mecha10-api/src/catalog/downloads`),
453 /// which run *inside* the private network and stream bytes back out.
454 pub const MECHA10_API_URL: &str = "https://mecha.industries/api";
455
456 /// Build the `mecha10-api` base URL (e.g. `https://mecha.industries/api`).
457 ///
458 /// Override at runtime with the `MECHA10_API_URL` env var (e.g. to point at a
459 /// local `mecha10-api` instance during development).
460 pub fn mecha10_api_base_url() -> String {
461 std::env::var("MECHA10_API_URL")
462 .unwrap_or_else(|_| MECHA10_API_URL.to_string())
463 .trim_end_matches('/')
464 .to_string()
465 }
466
467 /// Join `path` onto [`mecha10_api_base_url`] - the one place that should ever concatenate
468 /// the two, so every `mecha10-api` caller shares this instead of hand-rolling its own
469 /// `format!("{base}/...")`. `mecha10_api_base_url()` already bakes in the `/api` prefix
470 /// (e.g. `https://mecha.industries/api`), so `path` must start right after that (e.g.
471 /// `/models/catalog`, `/downloads/model/{repo}/{filename}`) - never with a leading `/api`
472 /// of its own, or the request doubles up to `.../api/api/...` and 404s (this exact mistake
473 /// has shipped more than once against this base URL).
474 pub fn mecha10_api_path(path: &str) -> String {
475 format!("{}/{}", mecha10_api_base_url(), path.trim_start_matches('/'))
476 }
477
478 /// Default authentication URL
479 pub const AUTH_URL: &str = "https://mecha.industries/api/auth";
480}
481
482/// Build a target output path
483pub fn target_path(profile: &str, binary: &str) -> String {
484 format!("target/{}/{}", profile, binary)
485}
486
487/// Build a target output path with optional target triple
488pub fn target_path_with_triple(target: Option<&str>, profile: &str) -> String {
489 match target {
490 Some(t) => format!("target/{}/{}", t, profile),
491 None => format!("target/{}", profile),
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn credentials_file_matches_mecha10_auth_source_of_truth() {
501 assert_eq!(user::credentials_file(), mecha10_auth::default_credentials_path());
502 }
503
504 /// `cargo_bin`/`local_bin` share a `home_bin_dir` helper (LAB-2893) that only differs on
505 /// which home-relative directory it joins - pin both ends of that against the home dir so
506 /// a future edit to the shared helper can't quietly break one without the other noticing.
507 #[test]
508 fn cargo_bin_and_local_bin_join_name_under_their_own_home_subdir() {
509 let home = dirs::home_dir().expect("home dir must resolve in test environment");
510 assert_eq!(
511 user::cargo_bin("mecha10"),
512 home.join(".cargo").join("bin").join("mecha10")
513 );
514 assert_eq!(
515 user::local_bin("mecha10"),
516 home.join(".local").join("bin").join("mecha10")
517 );
518 }
519
520 #[test]
521 fn simulation_project_dirs_share_the_simulation_root() {
522 assert_eq!(project::SIMULATION_MODELS_DIR, "configs/simulation/models");
523 assert_eq!(project::SIMULATION_ENVIRONMENTS_DIR, "configs/simulation/environments");
524 for dir in [project::SIMULATION_MODELS_DIR, project::SIMULATION_ENVIRONMENTS_DIR] {
525 assert!(dir.starts_with(&format!("{}/", project::SIMULATION_DIR)));
526 }
527 }
528
529 /// LAB-2429: task-curriculum configs deliberately live under `configs/rl/`, NOT
530 /// `SIMULATION_DIR` - they're a project's RL configuration (room_size, target_range,
531 /// obstacle_count, ...), not a physics/rendering asset, so they're not part of the
532 /// simulation-root grouping the test above pins.
533 #[test]
534 fn rl_tasks_dir_lives_under_configs_rl_not_simulation() {
535 assert_eq!(project::RL_TASKS_DIR, "configs/rl/tasks");
536 assert!(!project::RL_TASKS_DIR.starts_with(&format!("{}/", project::SIMULATION_DIR)));
537 }
538
539 #[test]
540 fn test_config_paths() {
541 assert_eq!(
542 config::framework_node("object-detector"),
543 "configs/nodes/@mecha10/object-detector/config.json"
544 );
545 assert_eq!(config::project_node("camera"), "configs/nodes/camera/config.json");
546 }
547
548 #[test]
549 fn test_framework_node_dir() {
550 assert_eq!(config::framework_node_dir("speaker"), "configs/nodes/@mecha10/speaker");
551 }
552
553 #[test]
554 fn test_framework_node_schema_dir_lives_under_nodes_not_configs_nodes() {
555 assert_eq!(project::framework_node_schema_dir("speaker"), "nodes/@mecha10/speaker");
556 assert_ne!(
557 project::framework_node_schema_dir("speaker"),
558 config::framework_node_dir("speaker"),
559 "schema dir (wave 10) must not collide with the config.json dir"
560 );
561 }
562
563 #[test]
564 fn test_node_config_candidates_order() {
565 assert_eq!(
566 config::node_config_candidates("teleop"),
567 [
568 "configs/nodes/@mecha10/teleop/config.json".to_string(),
569 "configs/nodes/teleop/config.json".to_string(),
570 ]
571 );
572 }
573
574 #[test]
575 fn test_behavior_config_paths() {
576 assert_eq!(
577 config::framework_behavior("idle_wander"),
578 "configs/behaviors/@mecha10/idle_wander/config.json"
579 );
580 assert_eq!(
581 config::project_behavior("idle_wander"),
582 "configs/behaviors/idle_wander/config.json"
583 );
584 }
585
586 #[test]
587 fn test_framework_behavior_dir() {
588 assert_eq!(
589 config::framework_behavior_dir("idle-wander"),
590 "configs/behaviors/@mecha10/idle-wander"
591 );
592 assert_eq!(
593 config::framework_behavior("idle-wander"),
594 format!("{}/config.json", config::framework_behavior_dir("idle-wander"))
595 );
596 }
597
598 #[test]
599 fn test_behavior_config_candidates_order() {
600 assert_eq!(
601 config::behavior_config_candidates("idle_wander"),
602 [
603 "configs/behaviors/idle_wander/config.json".to_string(),
604 "configs/behaviors/@mecha10/idle_wander/config.json".to_string(),
605 ]
606 );
607 }
608
609 #[test]
610 fn test_target_path() {
611 assert_eq!(target_path("release", "my-node"), "target/release/my-node");
612 assert_eq!(
613 target_path_with_triple(Some("aarch64-unknown-linux-gnu"), "release"),
614 "target/aarch64-unknown-linux-gnu/release"
615 );
616 assert_eq!(target_path_with_triple(None, "debug"), "target/debug");
617 }
618
619 #[test]
620 fn test_container_paths() {
621 assert_eq!(
622 container::project_path("configs/test.json"),
623 "/project/configs/test.json"
624 );
625 assert_eq!(container::framework_path("packages/core"), "/mecha10/packages/core");
626 }
627
628 #[test]
629 fn test_mecha10_api_base_url_default() {
630 let _guard = ENV_MUTEX.lock().unwrap();
631 std::env::remove_var("MECHA10_API_URL");
632 assert_eq!(urls::mecha10_api_base_url(), "https://mecha.industries/api");
633 }
634
635 #[test]
636 fn test_mecha10_api_base_url_env_override() {
637 let _guard = ENV_MUTEX.lock().unwrap();
638 std::env::set_var("MECHA10_API_URL", "http://localhost:4000/api/");
639 assert_eq!(urls::mecha10_api_base_url(), "http://localhost:4000/api");
640 std::env::remove_var("MECHA10_API_URL");
641 }
642
643 #[test]
644 fn test_mecha10_api_path_does_not_double_the_api_segment() {
645 let _guard = ENV_MUTEX.lock().unwrap();
646 std::env::remove_var("MECHA10_API_URL");
647 assert_eq!(
648 urls::mecha10_api_path("/models/catalog"),
649 "https://mecha.industries/api/models/catalog"
650 );
651 }
652
653 #[test]
654 fn test_mecha10_api_path_works_without_a_leading_slash() {
655 let _guard = ENV_MUTEX.lock().unwrap();
656 std::env::remove_var("MECHA10_API_URL");
657 assert_eq!(
658 urls::mecha10_api_path("downloads/model/deepghs/yolos/yolov8n/model.onnx"),
659 "https://mecha.industries/api/downloads/model/deepghs/yolos/yolov8n/model.onnx"
660 );
661 }
662
663 /// Serializes tests that mutate process-wide env vars, since `cargo test` runs
664 /// tests in this module concurrently by default.
665 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
666}