use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn resolve_with_config(config: &str, args: &[&str]) -> BTreeMap<String, String> {
let fixture = TempDir::new().expect("fixture root");
let home = fixture.path().join("sealed-home");
fs::create_dir_all(home.join(".codewhale")).expect("sealed config dir");
fs::write(home.join(".codewhale").join("config.toml"), config).expect("seed config");
let mut command = Command::new(codewhale_binary());
command.arg("model").arg("resolve").args(args);
let output = command
.env_clear()
.env("HOME", &home)
.env("USERPROFILE", &home)
.env("CODEWHALE_HOME", home.join(".codewhale"))
.env("CODEWHALE_SECRET_BACKEND", "file")
.output()
.expect("run model resolve");
assert!(
output.status.success(),
"model resolve {args:?} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| line.split_once(": "))
.map(|(key, value)| (key.trim().to_string(), value.trim().to_string()))
.collect()
}
#[test]
fn resolve_reports_the_configured_provider_not_a_deepseek_fallback() {
let report = resolve_with_config(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\n",
&[],
);
assert_eq!(
report.get("provider").map(String::as_str),
Some("zai"),
"configured provider must survive to the diagnostic: {report:?}"
);
assert_eq!(
report.get("provider_source").map(String::as_str),
Some("config"),
"provenance must name the config file: {report:?}"
);
}
#[test]
fn resolve_reports_a_provider_scoped_model_as_explicitly_configured() {
let report = resolve_with_config(
"provider = \"moonshot\"\n\n[providers.moonshot]\napi_key = \"k\"\nmodel = \"kimi-k3-turbo\"\n",
&[],
);
assert_eq!(report.get("provider").map(String::as_str), Some("moonshot"));
assert_eq!(
report.get("requested").map(String::as_str),
Some("kimi-k3-turbo"),
"a configured model is a request, not a fallback: {report:?}"
);
assert_eq!(
report.get("used_fallback").map(String::as_str),
Some("false"),
"{report:?}"
);
assert_eq!(
report.get("model_source").map(String::as_str),
Some("config [providers.*].model"),
"{report:?}"
);
}
#[test]
fn resolve_admits_when_nothing_was_configured() {
let report = resolve_with_config("", &[]);
assert_eq!(
report.get("requested").map(String::as_str),
Some(""),
"an unconfigured model must not be presented as a request: {report:?}"
);
assert_eq!(
report.get("used_fallback").map(String::as_str),
Some("true"),
"{report:?}"
);
assert_eq!(
report.get("model_source").map(String::as_str),
Some("provider default"),
"{report:?}"
);
}
#[test]
fn an_explicit_model_argument_still_answers_the_hypothetical() {
let report = resolve_with_config(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\n",
&["deepseek-v4-flash"],
);
assert_eq!(
report.get("requested").map(String::as_str),
Some("deepseek-v4-flash"),
"{report:?}"
);
assert_eq!(
report.get("model_source").map(String::as_str),
Some("argument"),
"{report:?}"
);
assert_eq!(
report.get("used_fallback").map(String::as_str),
Some("false"),
"{report:?}"
);
}
#[test]
fn an_explicit_provider_flag_is_reported_as_the_source() {
let report = resolve_with_config(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\n",
&["--provider", "moonshot"],
);
assert_eq!(report.get("provider").map(String::as_str), Some("moonshot"));
assert_eq!(
report.get("provider_source").map(String::as_str),
Some("--provider"),
"{report:?}"
);
}
fn resolve_with_global_flags(
config: &str,
global: &[&str],
args: &[&str],
) -> BTreeMap<String, String> {
let fixture = TempDir::new().expect("fixture root");
let home = fixture.path().join("sealed-home");
fs::create_dir_all(home.join(".codewhale")).expect("sealed config dir");
fs::write(home.join(".codewhale").join("config.toml"), config).expect("seed config");
let mut command = Command::new(codewhale_binary());
command.args(global).arg("model").arg("resolve").args(args);
let output = command
.env_clear()
.env("HOME", &home)
.env("USERPROFILE", &home)
.env("CODEWHALE_HOME", home.join(".codewhale"))
.env("CODEWHALE_SECRET_BACKEND", "file")
.output()
.expect("run model resolve");
assert!(
output.status.success(),
"model resolve {global:?} {args:?} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| line.split_once(": "))
.map(|(key, value)| (key.trim().to_string(), value.trim().to_string()))
.collect()
}
#[test]
fn top_level_provider_and_model_flags_report_the_runtime_route() {
let report = resolve_with_global_flags(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\n",
&["--provider", "moonshot", "--model", "kimi-k3"],
&[],
);
assert_eq!(report.get("provider").map(String::as_str), Some("moonshot"));
assert_eq!(
report.get("resolved").map(String::as_str),
Some("kimi-k3"),
"the diagnostic must not contradict the model the run will use: {report:?}"
);
assert_eq!(
report.get("requested").map(String::as_str),
Some("kimi-k3"),
"{report:?}"
);
assert_eq!(
report.get("used_fallback").map(String::as_str),
Some("false"),
"{report:?}"
);
assert_eq!(
report.get("model_source").map(String::as_str),
Some("--model"),
"{report:?}"
);
}
#[test]
fn moonshot_k3_products_resolve_without_crossing_providers() {
for model in ["kimi-k3", "k3"] {
let report = resolve_with_global_flags(
"provider = \"moonshot\"\n\n[providers.moonshot]\napi_key = \"k\"\n",
&[],
&[model, "--provider", "moonshot"],
);
assert_eq!(
report.get("provider").map(String::as_str),
Some("moonshot"),
"a Moonshot question must not be answered by another provider: {report:?}"
);
assert_eq!(
report.get("resolved").map(String::as_str),
Some(model),
"{report:?}"
);
assert_eq!(
report.get("used_fallback").map(String::as_str),
Some("false"),
"{report:?}"
);
}
}
#[test]
fn an_unservable_model_on_the_selected_provider_is_reported_as_a_fallback() {
let report = resolve_with_global_flags(
"provider = \"moonshot\"\n\n[providers.moonshot]\napi_key = \"k\"\n",
&[],
&["glm-5.2", "--provider", "moonshot"],
);
assert_eq!(report.get("provider").map(String::as_str), Some("moonshot"));
assert_eq!(
report.get("used_fallback").map(String::as_str),
Some("true"),
"an unservable id must not be presented as an honoured request: {report:?}"
);
}
#[test]
fn a_new_glm_sibling_is_servable_on_zai_but_not_on_moonshot() {
let served = resolve_with_global_flags(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\n",
&[],
&["glm-5.3", "--provider", "zai"],
);
assert_eq!(served.get("provider").map(String::as_str), Some("zai"));
assert_eq!(
served.get("resolved").map(String::as_str),
Some("GLM-5.3"),
"a catalogued model must resolve to itself, not to the provider default: {served:?}"
);
assert_eq!(
served.get("used_fallback").map(String::as_str),
Some("false"),
"a model the provider serves must not be reported as a fallback: {served:?}"
);
let refused = resolve_with_global_flags(
"provider = \"moonshot\"\n\n[providers.moonshot]\napi_key = \"k\"\n",
&[],
&["glm-5.3", "--provider", "moonshot"],
);
assert_eq!(
refused.get("provider").map(String::as_str),
Some("moonshot")
);
assert_eq!(
refused.get("used_fallback").map(String::as_str),
Some("true"),
"a Z.ai id must not be presented as honoured by Moonshot: {refused:?}"
);
let resolved = refused
.get("resolved")
.map(String::as_str)
.unwrap_or_default();
assert!(
!resolved.to_ascii_lowercase().contains("glm"),
"a provider that cannot serve GLM must not be handed a fabricated GLM id: {refused:?}"
);
}
#[test]
fn the_openrouter_glm_sibling_resolves_to_its_own_gateway_wire_id() {
let served = resolve_with_global_flags(
"provider = \"openrouter\"\n\n[providers.openrouter]\napi_key = \"k\"\n",
&[],
&["glm-5.3", "--provider", "openrouter"],
);
assert_eq!(
served.get("provider").map(String::as_str),
Some("openrouter")
);
assert_eq!(
served.get("resolved").map(String::as_str),
Some("z-ai/glm-5.3"),
"the bare alias must be rewritten to the OpenRouter wire id, not passed through: {served:?}"
);
assert_eq!(
served.get("used_fallback").map(String::as_str),
Some("false"),
"a gateway row the provider serves must not be reported as a fallback: {served:?}"
);
let default_route = resolve_with_config(
"provider = \"openrouter\"\n\n[providers.openrouter]\napi_key = \"k\"\n",
&[],
);
let resolved = default_route
.get("resolved")
.map(String::as_str)
.unwrap_or_default();
assert!(
!resolved.to_ascii_lowercase().contains("glm"),
"adding a GLM sibling must not make GLM the OpenRouter default: {default_route:?}"
);
}
#[test]
fn zai_default_route_resolves_to_glm_5_3_with_provider_default_provenance() {
let report = resolve_with_config(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\n",
&[],
);
assert_eq!(
report.get("resolved").map(String::as_str),
Some("GLM-5.3"),
"the Z.ai default is GLM-5.3: {report:?}"
);
assert_eq!(
report.get("model_source").map(String::as_str),
Some("provider default"),
"{report:?}"
);
}
#[test]
fn explicit_glm_5_2_selection_survives_the_default_move() {
let report = resolve_with_config(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\nmodel = \"GLM-5.2\"\n",
&[],
);
assert_eq!(
report.get("resolved").map(String::as_str),
Some("GLM-5.2"),
"an explicit GLM-5.2 route must not be upgraded: {report:?}"
);
assert_ne!(
report.get("model_source").map(String::as_str),
Some("provider default"),
"{report:?}"
);
}
fn codewhale_binary() -> PathBuf {
if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale") {
return PathBuf::from(path);
}
if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale") {
return PathBuf::from(path);
}
let mut path = std::env::current_exe().expect("current test executable path");
path.pop();
if path.ends_with("deps") {
path.pop();
}
path.push(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
path
}