use super::*;
use clap::Parser as _;
fn args(extra: &[&str]) -> cli::ServeArgs {
let mut argv = vec!["met", "serve", "dummy/model"];
argv.extend_from_slice(extra);
match cli::Cli::parse_from(argv).command {
cli::Command::Serve(a) => a,
cli::Command::Benchmark(_)
| cli::Command::DumpServeOptions
| cli::Command::SyncRecipes
| cli::Command::Doctor
| cli::Command::Circuit(_) => {
unreachable!("parsed a serve command")
}
}
}
#[test]
fn an_invalid_config_is_refused_before_anything_is_torn_down() {
let host = Arc::new(ModelHost::empty());
let bad = args(&["--scheduler", "nonsense"]);
let err = swap(&host, bad).expect_err("refused");
let text = format!("{err:#}");
assert!(text.contains("--scheduler"), "{text}");
assert!(
!host.is_loaded(),
"the host was empty and must still be empty"
);
}
#[test]
fn a_multi_rank_deployment_is_refused() {
let host = Arc::new(ModelHost::empty());
let multi = args(&["--world-size", "2"]);
let err = swap(&host, multi).expect_err("refused");
assert!(format!("{err:#}").contains("single-node only"));
}
#[test]
fn a_refused_swap_leaves_the_running_model_alone() {
let host = Arc::new(ModelHost::empty());
assert!(!host.is_loaded());
let _ = swap(&host, args(&["--world-size", "4"]));
assert!(!host.is_loaded(), "clear() must not have run");
}
#[test]
fn the_host_remembers_what_it_is_running() {
let host = Arc::new(ModelHost::empty());
assert!(
host.args().is_none(),
"nothing loaded, nothing to restore to"
);
let first = args(&["--port", "9001"]);
host.set_args(first.clone());
assert_eq!(
host.args().map(|a| a.port),
Some(9001),
"a swap can now restore to what was running"
);
}
#[test]
fn a_recipe_cannot_switch_off_the_operators_auto_swap_policy() {
use clap::Parser as _;
let previous = cli::ServeArgs::parse_from(["met", "org/live", "--auto-swap"]);
let mut next = cli::ServeArgs::parse_from(["met", "org/next"]);
assert!(!next.auto_swap, "the recipe says nothing about it");
super::carry_process_flags(&mut next, &previous);
assert!(next.auto_swap, "the operator's policy survives the swap");
assert_eq!(
next.model.as_deref(),
Some("org/next"),
"the MODEL still swaps"
);
}
#[test]
fn a_recipe_cannot_switch_on_auto_swap_where_it_was_forbidden() {
use clap::Parser as _;
let previous = cli::ServeArgs::parse_from(["met", "org/live"]);
let mut next = cli::ServeArgs::parse_from(["met", "org/next", "--auto-swap"]);
super::carry_process_flags(&mut next, &previous);
assert!(
!super::super::auto_swap::enabled(&next),
"the recipe's --auto-swap does not survive the swap"
);
}
#[test]
fn a_recipes_port_cannot_move_a_socket_that_is_already_bound() {
use clap::Parser as _;
let previous = cli::ServeArgs::parse_from(["met", "org/live", "--port", "8888"]);
let mut next = cli::ServeArgs::parse_from(["met", "org/next", "--port", "9100"]);
super::carry_process_flags(&mut next, &previous);
assert_eq!(next.port, 8888, "the bound port is authoritative");
}
#[test]
fn a_model_this_build_has_no_kernels_for_is_refused_before_teardown() {
let host = Arc::new(ModelHost::empty());
let dir = tempfile::tempdir().expect("tmp");
std::fs::write(
dir.path().join("config.json"),
r#"{"model_type":"no_such_architecture","hidden_size":4096,"num_hidden_layers":1}"#,
)
.expect("write");
use clap::Parser as _;
let args = cli::ServeArgs::parse_from(["met", dir.path().to_str().expect("utf8")]);
let err = super::swap(&host, args).expect_err("refused");
let text = format!("{err:#}");
assert!(
text.contains("no compiled kernels") || text.contains("no_such_architecture"),
"{text}"
);
assert!(
text.contains("running model is untouched") || host.current().is_none(),
"nothing was torn down: {text}"
);
}
#[test]
fn two_swaps_at_once_do_not_both_tear_down_the_model() {
let host = Arc::new(ModelHost::empty());
let barrier = Arc::new(std::sync::Barrier::new(2));
let overlapping = Arc::new(std::sync::atomic::AtomicBool::new(false));
let inside = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let threads: Vec<_> = (0..2)
.map(|_| {
let (host, barrier) = (host.clone(), barrier.clone());
let (overlapping, inside) = (overlapping.clone(), inside.clone());
std::thread::spawn(move || {
barrier.wait();
let _guard = host.swap_guard();
if inside.fetch_add(1, std::sync::atomic::Ordering::SeqCst) > 0 {
overlapping.store(true, std::sync::atomic::Ordering::SeqCst);
}
std::thread::sleep(std::time::Duration::from_millis(60));
inside.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
})
})
.collect();
for t in threads {
t.join().expect("no panic");
}
assert!(
!overlapping.load(std::sync::atomic::Ordering::SeqCst),
"two swaps were inside the guard at once"
);
}
#[test]
fn the_swap_the_winner_already_performed_is_not_repeated() {
use clap::Parser as _;
let a = cli::ServeArgs::parse_from(["met", "org/m"]);
let mut b = cli::ServeArgs::parse_from(["met", "org/m"]);
assert_eq!(a, b, "same argv");
b.max_batch_size =
metrale_model_engine::factory::SlotRequest::Count(a.max_batch_size.ceiling() + 1);
assert_ne!(a, b, "a different recipe for the same model is a real swap");
}
#[test]
fn a_swap_is_refused_once_shutdown_has_been_requested() {
let err = super::refuse_if_shutting_down(true).expect_err("refused");
assert!(format!("{err:#}").contains("shutdown"), "{err:#}");
super::refuse_if_shutting_down(false).expect("a live process may swap");
}
#[test]
fn a_recipe_cannot_turn_off_authentication() {
let host = Arc::new(ModelHost::empty());
let cfg = std::sync::Arc::new(
crate::auth::AuthConfig::from_inline("sk-test-token").expect("valid token"),
);
host.set_auth(Some(cfg));
use clap::Parser as _;
let mut args = cli::ServeArgs::parse_from(["met", "org/m"]);
args.world_size = 2;
let _ = super::swap(&host, args);
assert!(
host.auth().is_some(),
"the API-key policy must not be a casualty of a swap"
);
}
#[test]
fn a_swap_does_not_silently_stop_request_dumping() {
use clap::Parser as _;
let previous = cli::ServeArgs::parse_from(["met", "org/live", "--dump", "/tmp/probe.jsonl"]);
let mut next = cli::ServeArgs::parse_from(["met", "org/next"]);
assert!(next.dump.is_none(), "the recipe says nothing about it");
super::carry_process_flags(&mut next, &previous);
assert_eq!(next.dump.as_deref(), Some("/tmp/probe.jsonl"));
assert_eq!(
next.model.as_deref(),
Some("org/next"),
"the MODEL still swaps"
);
}
#[test]
fn a_repeat_of_the_live_config_is_a_no_op_even_with_process_flags_set() {
use clap::Parser as _;
let mut live = cli::ServeArgs::parse_from(["met", "org/m"]);
let operator =
cli::ServeArgs::parse_from(["met", "org/boot", "--auto-swap", "--dump", "/tmp/d.jsonl"]);
super::carry_process_flags(&mut live, &operator);
let mut queued = cli::ServeArgs::parse_from(["met", "org/m"]);
assert_ne!(
live, queued,
"they differ before carrying — the old comparison"
);
super::carry_process_flags(&mut queued, &live);
assert_eq!(
live, queued,
"and are equal after it, which is what makes the no-op fire"
);
}
#[test]
fn a_swap_takes_the_published_weight_quantization() {
let published = metrale_model_layers::layers::weight_quantization();
assert_eq!(published, metrale_config::WeightQuantTier::default());
let mut nvfp4 = args(&[
"--weight-quantization",
"nvfp4",
"--w4a4-downcast",
"--w4a4-downcast-wide",
]);
pin_published_weight_quantization(&mut nvfp4);
assert_eq!(nvfp4.weight_quant_tier().expect("valid"), published);
assert!(!nvfp4.w4a4_downcast && !nvfp4.w4a4_downcast_wide);
assert!(cli::validate_serve_args(&nvfp4).is_ok());
let mut same = args(&[]);
let before = same.clone();
pin_published_weight_quantization(&mut same);
assert_eq!(same, before);
}