Skip to main content

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}