//! Solo / single-node flag-emission tests — split from `tests.rs` to keep //! that file under the 500-line limit. Uses the parent module's `trec`, //! `tcls`, `make_dm_ctx`, and `build_docker_command` helpers. use super::*; use crate::config::Recipe; #[test] fn single_node_run_omits_distributed_flags() { // A solo or required_nodes=1 run must NOT emit --nnodes/--tp/--master-addr // etc. — the engine defaults to single-node. (Regression for the bug // where a 1-node recipe on a multi-node cluster launched N containers each // told `--nnodes N --tp N`.) let mut r = trec(); r.required_nodes = Some(1); r.tensor_parallel_size = 1; r.pipeline_parallel_size = Some(1); // nnodes=1 is what start_cluster computes after trimming to required_nodes. let ctx = make_dm_ctx(&r, 1, 1, 1, false); let cmd = build_docker_command(&ctx, &tcls().nodes[0], true).join(" "); assert!( !cmd.contains("--nnodes"), "single-node must not pass --nnodes: {cmd}" ); assert!( !cmd.contains("--tensor-parallel-size"), "single-node must not pass --tp: {cmd}" ); assert!( !cmd.contains("--pipeline-parallel-size"), "single-node must not pass --pp: {cmd}" ); assert!( !cmd.contains("--master-addr"), "single-node must not pass --master-addr: {cmd}" ); assert!( !cmd.contains("--master-port"), "single-node must not pass --master-port: {cmd}" ); assert!( !cmd.contains("--node-rank"), "single-node must not pass --node-rank: {cmd}" ); assert!( !cmd.contains("--headless"), "single-node must not pass --headless: {cmd}" ); // But the engine still gets --host/--port. assert!(cmd.contains("--host")); assert!(cmd.contains("--port")); } #[test] fn multi_node_run_still_emits_distributed_flags() { // Regression guard: the single-node fix must not break the multi-node path. let r = trec(); // required_nodes=2, tp collapses to nnodes=2 let ctx = make_dm_ctx(&r, 2, 2, 2, false); let cmd = squash(&build_docker_command(&ctx, &tcls().nodes[0], true).join(" ")); assert!(cmd.contains("--nnodes 2")); assert!(cmd.contains("--tensor-parallel-size 2")); assert!(cmd.contains("--pipeline-parallel-size 2")); assert!(cmd.contains("--master-addr")); assert!(cmd.contains("--master-port")); assert!(cmd.contains("--node-rank 0")); } #[test] fn solo_worker_node_emits_headless_no_rendezvous() { // Multi-node: a worker (rank>0) gets --headless; single-node never does. let r = trec(); let ctx = make_dm_ctx(&r, 2, 2, 2, false); let worker_cmd = build_docker_command(&ctx, &tcls().nodes[1], false).join(" "); assert!( worker_cmd.contains("--headless"), "multi-node worker needs --headless: {worker_cmd}" ); // Single-node worker (n/a really, but guard): headless never appears. let mut r1 = r.clone(); r1.required_nodes = Some(1); let ctx1 = make_dm_ctx(&r1, 1, 1, 1, false); let solo_cmd = build_docker_command(&ctx1, &tcls().nodes[1], false).join(" "); assert!(!solo_cmd.contains("--headless")); } #[test] fn recipe_solo_field_deserializes() { let toml_str = r#" name = "Solo" model = "org/m" solo = true required_nodes = 4 "#; let r: Recipe = toml::from_str(toml_str).unwrap(); assert_eq!(r.solo, Some(true)); // unset → None (default multi-node) let toml2 = r#" name = "Multi" model = "org/m" required_nodes = 4 "#; let r2: Recipe = toml::from_str(toml2).unwrap(); assert_eq!(r2.solo, None); }