use std::path::PathBuf;
use super::args::{parse_model_args, short_circuit, BindMode, ModelArgs};
use super::bind::tailnet_host;
use super::resolve_api_key;
fn parse(argv: &[&str]) -> Result<Option<ModelArgs>, String> {
let owned: Vec<String> = argv.iter().map(|s| s.to_string()).collect();
parse_model_args(&owned)
}
#[test]
fn legacy_positional_mode_is_left_alone() {
assert!(parse(&["/tmp/tok", "9000"]).unwrap().is_none());
}
#[test]
fn api_key_defaults_to_absent_and_reads_the_flag_value() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(d.api_key, None);
let o = parse(&["--model", "/tmp/m", "--api-key", "sk-test-123"])
.unwrap()
.unwrap();
assert_eq!(o.api_key.as_deref(), Some("sk-test-123"));
}
#[test]
fn an_empty_api_key_is_refused() {
let err = parse(&["--model", "/tmp/m", "--api-key", ""]).unwrap_err();
assert!(err.contains("--api-key"), "{err}");
}
#[test]
fn tailnet_requires_an_api_key_before_opening_the_model() {
let err = resolve_api_key(BindMode::Tailnet, None, None).unwrap_err();
assert!(err.contains("--bind tailnet requires --api-key"), "{err}");
assert_eq!(
resolve_api_key(BindMode::Tailnet, Some("flag-key".into()), None).unwrap(),
Some("flag-key".into())
);
assert_eq!(
resolve_api_key(BindMode::Tailnet, None, Some("env-key".into())).unwrap(),
Some("env-key".into())
);
assert!(resolve_api_key(BindMode::Tailnet, None, Some(String::new())).is_err());
}
#[test]
fn loopback_keeps_optional_auth_and_flag_precedence() {
assert_eq!(
resolve_api_key(BindMode::Loopback, None, None).unwrap(),
None
);
assert_eq!(
resolve_api_key(
BindMode::Loopback,
Some("flag-key".into()),
Some("env-key".into())
)
.unwrap(),
Some("flag-key".into())
);
}
#[test]
fn vision_sidecar_defaults_to_none() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(d.vision_sidecar, None);
}
#[test]
fn a_vision_sidecar_path_is_not_validated_by_the_parser() {
let d = parse(&[
"--model",
"/tmp/m",
"--vision-sidecar",
"/does/not/exist.gturbo-vision",
])
.unwrap()
.unwrap();
assert_eq!(
d.vision_sidecar.as_deref(),
Some("/does/not/exist.gturbo-vision")
);
}
#[cfg(target_os = "macos")]
#[test]
fn an_alias_resolves_to_its_install_directory() {
let root = std::env::temp_dir().join(format!(
"turbospark-server-alias-{}-{}",
std::process::id(),
line!()
));
let installed = root.join("models").join("some-alias.gturbo");
std::fs::create_dir_all(&installed).unwrap();
std::env::set_var("TURBOSPARK_HOME", &root);
assert_eq!(
catalog::resolve_model_arg("some-alias"),
installed,
"an alias should resolve to its install directory"
);
assert_ne!(
catalog::resolve_model_arg("some-alias"),
PathBuf::from("some-alias"),
"resolution must not be a pass-through for a known alias"
);
std::env::remove_var("TURBOSPARK_HOME");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn power_flags_default_to_unset_and_parse_their_documented_values() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(d.power_profile, None);
assert_eq!(d.max_tokens_per_sec, None);
let o = parse(&[
"--model",
"/tmp/m",
"--power-profile",
"efficiency",
"--max-tokens-per-sec",
"7.5",
])
.unwrap()
.unwrap();
assert_eq!(o.power_profile, Some(runtime::PowerProfile::Efficiency));
assert_eq!(o.max_tokens_per_sec, Some(7.5));
}
#[test]
fn bad_power_flag_values_are_rejected() {
assert!(parse(&["--model", "/tmp/m", "--power-profile", "turbo"]).is_err());
for bad in ["0", "-1", "abc", "inf"] {
assert!(
parse(&["--model", "/tmp/m", "--max-tokens-per-sec", bad]).is_err(),
"expected {bad} to be rejected"
);
}
}
#[test]
fn model_mode_defaults_and_overrides() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(
(d.port, d.max_context, d.expert_cache_slots),
(8080, None, None)
);
let o = parse(&[
"--model",
"/tmp/m",
"--port",
"9",
"--max-context",
"1024",
"--expert-cache-slots",
"32",
])
.unwrap()
.unwrap();
assert_eq!(
(o.port, o.max_context, o.expert_cache_slots),
(9, Some(1024), Some(32))
);
let a = parse(&["--model", "/tmp/m", "--expert-cache-slots", "auto"])
.unwrap()
.unwrap();
assert_eq!(a.expert_cache_slots, None);
assert!(parse(&["--model", "/tmp/m", "--expert-cache-slots", "20"]).is_err());
let c = parse(&["--model", "/tmp/m", "--max-context", "auto"])
.unwrap()
.unwrap();
assert_eq!(c.max_context, None);
assert!(parse(&["--model", "/tmp/m", "--max-context", "0"]).is_err());
assert!(parse(&["--model", "/tmp/m", "--max-context", "lots"]).is_err());
}
#[test]
fn repeated_model_flags_attach_distinct_installs_in_order() {
let parsed = parse(&[
"--model",
"/tmp/alpha.gturbo",
"--model",
"/tmp/beta.gturbo",
])
.unwrap()
.unwrap();
assert_eq!(parsed.model, "/tmp/alpha.gturbo");
assert_eq!(parsed.models, vec!["/tmp/alpha.gturbo", "/tmp/beta.gturbo"]);
}
#[test]
fn a_multi_install_registry_cannot_also_request_a_same_install_pool() {
let err = parse(&[
"--model",
"/tmp/alpha.gturbo",
"--model",
"/tmp/beta.gturbo",
"--pool-size",
"2",
])
.unwrap_err();
assert!(err.contains("multiple --model flags"), "{err}");
}
#[test]
fn a_flag_in_any_position_stays_out_of_the_scripted_mode() {
let ordered = parse(&["--port", "9", "--model", "/tmp/m"])
.unwrap()
.unwrap();
assert_eq!((ordered.port, ordered.model.as_str()), (9, "/tmp/m"));
let err = parse(&["--modle", "/tmp/m"]).unwrap_err();
assert!(err.contains("unknown option"), "{err}");
assert!(parse(&["/tmp/tokenizer-dir"]).unwrap().is_none());
}
#[test]
fn help_and_version_short_circuit_before_anything_is_parsed() {
assert!(short_circuit(&owned(&["--help"]))
.unwrap()
.contains("usage:"));
assert!(short_circuit(&owned(&["-h"])).unwrap().contains("usage:"));
assert!(short_circuit(&owned(&["--model", "/tmp/m", "--help"])).is_some());
assert!(short_circuit(&owned(&["--model", "/tmp/m"])).is_none());
assert!(short_circuit(&owned(&["/tmp/dir"])).is_none());
}
#[test]
fn the_version_line_matches_the_clis() {
let ours = short_circuit(&owned(&["--version"])).unwrap();
assert_eq!(ours, format!("turbospark {}\n", env!("CARGO_PKG_VERSION")));
assert!(short_circuit(&owned(&["-V"])).unwrap() == ours);
}
fn owned(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn bad_model_mode_arguments_are_rejected() {
assert!(parse(&["--model", "/tmp/m", "--expert-cache-slots", "12"]).is_err());
assert!(parse(&["--model", "/tmp/m", "--port", "70000"]).is_err());
assert!(parse(&["--model"]).is_err());
assert!(parse(&["--model", "/tmp/m", "--nope", "1"]).is_err());
assert!(parse(&["--model", "/tmp/m", "--bind", "lan"]).is_err());
}
#[test]
fn bind_mode_defaults_to_loopback() {
assert_eq!(
parse(&["--model", "/tmp/m"]).unwrap().unwrap().bind,
BindMode::Loopback
);
let t = parse(&["--model", "/tmp/m", "--bind", "tailnet"])
.unwrap()
.unwrap();
assert_eq!(t.bind, BindMode::Tailnet);
assert_eq!(BindMode::Loopback.host().unwrap(), "127.0.0.1");
}
#[test]
fn tailnet_host_accepts_exactly_one_in_range_address() {
assert_eq!(tailnet_host("100.64.0.1\n").unwrap(), "100.64.0.1");
assert_eq!(
tailnet_host("100.127.255.254\n").unwrap(),
"100.127.255.254"
);
}
#[test]
fn tailnet_host_never_falls_back() {
assert!(tailnet_host("").is_err());
assert!(tailnet_host(" \n").is_err());
assert!(tailnet_host("100.64.0.1 100.64.0.2\n").is_err());
assert!(tailnet_host("100.63.0.1").is_err());
assert!(tailnet_host("100.128.0.1").is_err());
assert!(tailnet_host("192.168.1.5").is_err());
assert!(tailnet_host("127.0.0.1").is_err());
assert!(tailnet_host("0.0.0.0").is_err());
assert!(tailnet_host("fd7a:115c:a1e0::1").is_err());
assert!(tailnet_host("100.64.0").is_err());
assert!(tailnet_host("100.64.0.256").is_err());
assert!(tailnet_host("100.064.0.1").is_err());
assert!(tailnet_host("100.64.0.1;rm -rf /").is_err());
}
#[test]
fn a_non_ascii_boundary_in_the_bad_field_does_not_panic() {
let field = format!("h{}", "\u{00e9}".repeat(32));
assert!(!field.is_char_boundary(64), "fixture must straddle byte 64");
let err = tailnet_host(&field).unwrap_err();
assert!(err.contains("not a Tailnet IPv4 address"), "{err}");
}
#[test]
fn steering_defaults_to_off_and_reads_no_file() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert!(d.steering.vectors.is_empty());
assert!(!d.steering.is_active());
assert!(d.steering.primary().is_none());
}
#[test]
fn a_steering_parameter_without_a_vector_is_refused() {
for (flag, value) in [
("--steering-mode", "renorm"),
("--steering-scale", "0.8"),
("--steering-layers", "30:40"),
("--steering-target", "2.5"),
("--steering-gate", "0.5"),
] {
let err = parse(&["--model", "/tmp/m", flag, value])
.expect_err(&format!("{flag} alone should be refused"));
assert!(
err.contains(flag) && err.contains("--steering"),
"message should name the flag and what it needs: {err}"
);
}
}
#[test]
fn an_unknown_steering_mode_names_every_mode_it_accepts() {
let err = parse(&["--model", "/tmp/m", "--steering-mode", "renrom"])
.expect_err("a misspelled mode should be refused");
assert!(
err.contains("renorm"),
"the fourth mode is the one this message lost once, and reads: {err}"
);
for name in foundation::STEERING_MODE_NAMES {
assert!(
err.contains(name),
"the rejection should name {name}, and reads: {err}"
);
}
}
#[cfg(target_os = "macos")]
fn write_vector(tag: &str, layers: usize, mode: Option<foundation::SteeringMode>) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"turbospark-server-steer-{}-{tag}",
std::process::id()
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("v.gguf");
let dirs: std::collections::BTreeMap<usize, Vec<f32>> = (1..=layers)
.map(|l| (l, (0..8).map(|i| (l * 8 + i) as f32 * 0.125).collect()))
.collect();
std::fs::write(
&path,
repack::control_vector::write_control_vector(&dirs, "qwen35", mode).unwrap(),
)
.unwrap();
path
}
#[cfg(target_os = "macos")]
#[test]
fn the_mode_comes_from_the_flag_then_the_file_then_the_default() {
let declared = write_vector("declared", 4, Some(foundation::SteeringMode::Renorm));
let bare = write_vector("bare", 4, None);
let p = |args: &[&str]| parse(args).unwrap().unwrap().steering;
let from_file = p(&[
"--model",
"/tmp/m",
"--steering",
declared.to_str().unwrap(),
]);
let file_vector = from_file.primary().expect("a vector was given");
assert_eq!(file_vector.mode, foundation::SteeringMode::Renorm);
assert_eq!(
file_vector.alpha, 1.0,
"a loaded vector means full strength"
);
let from_flag = p(&[
"--model",
"/tmp/m",
"--steering",
declared.to_str().unwrap(),
"--steering-mode",
"add",
]);
assert_eq!(
from_flag.primary().expect("a vector was given").mode,
foundation::SteeringMode::Add
);
let defaulted = p(&["--model", "/tmp/m", "--steering", bare.to_str().unwrap()]);
assert_eq!(
defaulted.primary().expect("a vector was given").mode,
foundation::SteeringMode::Ablate
);
}
#[cfg(target_os = "macos")]
#[test]
fn a_layer_range_applies_whichever_side_of_the_vector_it_is_given() {
let v = write_vector("range", 6, None);
let path = v.to_str().unwrap();
let covered = |args: &[&str]| {
let s = parse(args).unwrap().unwrap().steering;
let set = &s.primary().expect("a vector was given").set;
(
set.covered_layers(),
set.layer(0).is_some(),
set.layer(3).is_some(),
)
};
let after = covered(&[
"--model",
"/tmp/m",
"--steering",
path,
"--steering-layers",
"2:4",
]);
let before = covered(&[
"--model",
"/tmp/m",
"--steering-layers",
"2:4",
"--steering",
path,
]);
assert_eq!(after, (3, false, true));
assert_eq!(before, after, "the range must not depend on flag order");
}
#[cfg(target_os = "macos")]
#[test]
fn an_inverted_layer_range_is_refused() {
let v = write_vector("inverted", 6, None);
assert!(parse(&[
"--model",
"/tmp/m",
"--steering",
v.to_str().unwrap(),
"--steering-layers",
"4:2",
])
.is_err());
}
#[test]
fn reasoning_flag_defaults_to_off_and_parses_documented_levels() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(d.reasoning, tokenizer::ReasoningEffort::Off);
for (arg, expected) in [
("off", tokenizer::ReasoningEffort::Off),
("low", tokenizer::ReasoningEffort::Low),
("medium", tokenizer::ReasoningEffort::Medium),
("high", tokenizer::ReasoningEffort::High),
("xhigh", tokenizer::ReasoningEffort::XHigh),
] {
let p = parse(&["--model", "/tmp/m", "--reasoning", arg])
.unwrap()
.unwrap();
assert_eq!(p.reasoning, expected, "failed for --reasoning {arg}");
}
assert!(parse(&["--model", "/tmp/m", "--reasoning", "max"]).is_err());
assert!(parse(&["--model", "/tmp/m", "--reasoning", "true"]).is_err());
}
#[test]
fn kv_bits_flag_defaults_to_off_and_parses_documented_widths() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(d.kv_bits, runtime::KvQuant::Off);
for (arg, expected) in [
("off", runtime::KvQuant::Off),
(
"2",
runtime::KvQuant::TurboQuant {
k_bits: 2,
v_bits: 2,
},
),
(
"3",
runtime::KvQuant::TurboQuant {
k_bits: 3,
v_bits: 3,
},
),
(
"3.5",
runtime::KvQuant::TurboQuant {
k_bits: 3,
v_bits: 4,
},
),
(
"4",
runtime::KvQuant::TurboQuant {
k_bits: 4,
v_bits: 4,
},
),
] {
let p = parse(&["--model", "/tmp/m", "--kv-bits", arg])
.unwrap()
.unwrap();
assert_eq!(p.kv_bits, expected, "failed for --kv-bits {arg}");
}
assert!(parse(&["--model", "/tmp/m", "--kv-bits", "5"]).is_err());
assert!(parse(&["--model", "/tmp/m", "--kv-bits", "2.5"]).is_err());
}
#[test]
fn prefix_reuse_flag_defaults_to_off_and_parses_on_off() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert!(!d.prefix_reuse);
let on = parse(&["--model", "/tmp/m", "--prefix-reuse", "on"])
.unwrap()
.unwrap();
assert!(on.prefix_reuse);
let off = parse(&["--model", "/tmp/m", "--prefix-reuse", "off"])
.unwrap()
.unwrap();
assert!(!off.prefix_reuse);
assert!(parse(&["--model", "/tmp/m", "--prefix-reuse", "maybe"]).is_err());
}
#[test]
fn session_slots_defaults_to_one_and_parses_a_count() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(d.session_slots, 1);
let three = parse(&[
"--model",
"/tmp/m",
"--prefix-reuse",
"on",
"--session-slots",
"3",
])
.unwrap()
.unwrap();
assert_eq!(three.session_slots, 3);
assert!(
parse(&["--model", "/tmp/m", "--session-slots", "0"]).is_err(),
"a zero-slot pool is not a smaller version of the feature, it is nonsense"
);
assert!(parse(&["--model", "/tmp/m", "--session-slots", "auto"]).is_err());
assert!(parse(&["--model", "/tmp/m", "--session-slots", "-1"]).is_err());
}
#[test]
fn session_slots_above_one_needs_prefix_reuse_on() {
assert!(parse(&["--model", "/tmp/m", "--session-slots", "2"]).is_err());
assert!(parse(&[
"--model",
"/tmp/m",
"--prefix-reuse",
"off",
"--session-slots",
"2",
])
.is_err());
assert!(parse(&[
"--model",
"/tmp/m",
"--session-slots",
"2",
"--prefix-reuse",
"off",
])
.is_err());
assert!(parse(&[
"--model",
"/tmp/m",
"--prefix-reuse",
"off",
"--session-slots",
"1",
])
.is_ok());
}
#[test]
fn system_flag_defaults_to_absent_and_joins_repeats_with_a_newline() {
let d = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert_eq!(d.default_system, None);
let one = parse(&["--model", "/tmp/m", "--system", "be brief"])
.unwrap()
.unwrap();
assert_eq!(one.default_system.as_deref(), Some("be brief"));
let many = parse(&[
"--model", "/tmp/m", "--system", "be brief", "--system", "and kind",
])
.unwrap()
.unwrap();
assert_eq!(many.default_system.as_deref(), Some("be brief\nand kind"));
}
#[test]
fn an_empty_system_prompt_is_refused() {
let err = parse(&["--model", "/tmp/m", "--system", " "]).unwrap_err();
assert!(err.contains("--system"), "{err}");
}
#[test]
fn system_file_reads_the_prompt_from_disk() {
let dir = std::env::temp_dir().join(format!("ts-system-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("prompt.txt");
std::fs::write(&path, "answer only in haiku\n").unwrap();
let p = parse(&["--model", "/tmp/m", "--system-file", path.to_str().unwrap()])
.unwrap()
.unwrap();
assert_eq!(p.default_system.as_deref(), Some("answer only in haiku"));
std::fs::write(&path, "\n \n").unwrap();
let err = parse(&["--model", "/tmp/m", "--system-file", path.to_str().unwrap()]).unwrap_err();
assert!(err.contains("empty"), "{err}");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_missing_system_file_is_refused_by_name() {
let err = parse(&["--model", "/tmp/m", "--system-file", "/tmp/no/such/prompt"]).unwrap_err();
assert!(err.contains("/tmp/no/such/prompt"), "{err}");
}
#[test]
fn system_and_system_file_together_are_refused() {
let err = parse(&[
"--model",
"/tmp/m",
"--system",
"be brief",
"--system-file",
"/tmp/whatever",
])
.unwrap_err();
assert!(err.contains("--system-file"), "{err}");
}
#[test]
fn the_usage_text_documents_both_system_flags() {
assert!(
super::args::USAGE.contains("--system "),
"{}",
super::args::USAGE
);
assert!(super::args::USAGE.contains("--system-file"));
}
#[test]
fn omlx_compatibility_memory_guard_flags() {
let safe_args = parse(&["--model", "/tmp/m", "--memory-guard", "safe"])
.unwrap()
.unwrap();
assert_eq!(safe_args.load_policy.guard, runtime::LoadGuard::Balanced);
let gb_args = parse(&["--model", "/tmp/m", "--memory-guard-gb", "48"])
.unwrap()
.unwrap();
assert_eq!(
gb_args.load_policy.guard,
runtime::LoadGuard::Custom {
max_counted_bytes: 48 * 1024 * 1024 * 1024
}
);
}
#[test]
fn omlx_compatibility_extended_flags() {
let args = parse(&[
"--model",
"/tmp/m",
"--prefix-reuse",
"on",
"--max-concurrent-requests",
"16",
"--hf-endpoint",
"https://hf-mirror.com",
"--paged-ssd-cache-dir",
"/tmp/omlx-cache",
"--hot-cache-max-size",
"20%",
"--mcp-config",
"/tmp/mcp.json",
])
.unwrap()
.unwrap();
assert_eq!(args.session_slots, 16);
assert_eq!(args.hf_endpoint.as_deref(), Some("https://hf-mirror.com"));
assert_eq!(
args.paged_ssd_cache_dir,
Some(PathBuf::from("/tmp/omlx-cache"))
);
assert_eq!(args.hot_cache_max_size.as_deref(), Some("20%"));
assert_eq!(args.mcp_config, Some(PathBuf::from("/tmp/mcp.json")));
}
#[test]
fn embedding_model_alone_is_accepted_with_no_chat_model() {
let args = parse(&["--embedding-model", "/tmp/emb"]).unwrap().unwrap();
assert!(args.model.is_empty(), "{args:?}");
assert_eq!(args.embedding_model.as_deref(), Some("/tmp/emb"));
}
#[test]
fn neither_model_nor_embedding_model_is_refused() {
let err = parse(&["--bind", "loopback"]).unwrap_err();
assert!(err.contains("--model"), "{err}");
assert!(err.contains("--embedding-model"), "{err}");
}
#[test]
fn audio_models_alone_are_accepted_and_repeat_per_task() {
let args = parse(&[
"--stt-model",
"whisper-base-en",
"--stt-model",
"/tmp/qwen",
"--tts-model",
"kokoro-82m",
"--music-model",
"minimax-music3-4bit",
])
.unwrap()
.unwrap();
assert!(args.model.is_empty() && args.models.is_empty(), "{args:?}");
assert_eq!(args.stt_models, ["whisper-base-en", "/tmp/qwen"]);
assert_eq!(args.tts_models, ["kokoro-82m"]);
assert_eq!(args.music_models, ["minimax-music3-4bit"]);
assert!(args.has_audio());
}
#[test]
fn audio_models_ride_along_with_a_chat_model() {
let args = parse(&["--model", "/tmp/m", "--stt-model", "whisper-base-en"])
.unwrap()
.unwrap();
assert_eq!(args.models, ["/tmp/m"]);
assert_eq!(args.stt_models, ["whisper-base-en"]);
}
#[test]
fn no_audio_flags_means_no_audio() {
let args = parse(&["--model", "/tmp/m"]).unwrap().unwrap();
assert!(!args.has_audio());
}
#[test]
fn an_audio_flag_without_a_value_is_refused() {
let err = parse(&["--model", "/tmp/m", "--tts-model"]).unwrap_err();
assert!(err.contains("--tts-model"), "{err}");
}
#[test]
fn help_text_names_the_audio_flags() {
let usage = super::args::usage();
for flag in ["--stt-model", "--tts-model", "--music-model"] {
assert!(usage.contains(flag), "{flag} missing from --help");
}
}
#[test]
fn an_overflowing_memory_guard_gb_is_refused() {
let err = parse(&[
"--model",
"/tmp/m",
"--memory-guard-gb",
"18446744073709551615",
])
.unwrap_err();
assert!(err.contains("--memory-guard-gb"), "{err}");
}
#[test]
fn model_dir_auto_discovery() {
let tmp = std::env::temp_dir().join(format!("turbospark-test-model-dir-{}", line!()));
let model_folder = tmp.join("test-model.gturbo");
std::fs::create_dir_all(&model_folder).unwrap();
std::fs::write(model_folder.join("manifest.json"), "{}").unwrap();
let args = parse(&["--model-dir", tmp.to_str().unwrap()])
.unwrap()
.unwrap();
assert_eq!(args.model, model_folder.display().to_string());
std::fs::remove_dir_all(&tmp).ok();
}