use agent_client_protocol::{
schema::v1::{
SessionConfigKind, SessionConfigOptionCategory, SessionConfigSelect,
SessionConfigSelectOptions, SessionId, SetSessionConfigOptionRequest,
},
ErrorCode,
};
use pretty_assertions::assert_eq;
use rho_providers::model::{
catalog::{ModelCatalogEntry, ModelSelection},
display_name::ModelDisplayNameCacheGuard,
models_dev::{
with_models_dev_cache_dir_for_tests, write_cached_model_metadata_for_tests, ModelMetadata,
},
provider_models::with_provider_models_cache_dir_for_tests,
};
use super::{model_config_options, resolve_model_value, CurrentModel, MODEL_CONFIG_ID};
fn entry(provider: &str, model: &str) -> ModelCatalogEntry {
ModelCatalogEntry {
provider: provider.into(),
model: model.into(),
display_name: model.into(),
auth_modes: vec!["api-key".into()],
}
}
fn current(provider: &str, model: &str) -> CurrentModel {
CurrentModel {
provider: provider.into(),
model: model.into(),
auth: "api-key".into(),
}
}
fn select_options(
option: &agent_client_protocol::schema::v1::SessionConfigOption,
) -> &SessionConfigSelect {
match &option.kind {
SessionConfigKind::Select(select) => select,
other => panic!("expected select kind, got {other:?}"),
}
}
fn option_values(select: &SessionConfigSelect) -> Vec<String> {
option_entries(select)
.into_iter()
.map(|(value, _, _)| value)
.collect()
}
fn option_entries(select: &SessionConfigSelect) -> Vec<(String, String, Option<String>)> {
match &select.options {
SessionConfigSelectOptions::Ungrouped(options) => options
.iter()
.map(|option| {
(
option.value.0.as_ref().to_string(),
option.name.clone(),
option.description.clone(),
)
})
.collect(),
_ => panic!("expected an ungrouped model list"),
}
}
fn with_catalog_names<T>(names: &[(&str, &str, &str)], f: impl FnOnce() -> T) -> T {
let catalog = tempfile::tempdir().unwrap();
let provider = tempfile::tempdir().unwrap();
with_models_dev_cache_dir_for_tests(catalog.path().to_path_buf(), || {
with_provider_models_cache_dir_for_tests(provider.path().to_path_buf(), || {
let _names = ModelDisplayNameCacheGuard::new();
for (provider_name, model, display_name) in names {
write_cached_model_metadata_for_tests(
provider_name,
model,
&ModelMetadata {
display_name: Some((*display_name).into()),
..ModelMetadata::default()
},
);
}
f()
})
})
}
#[test]
fn model_options_list_favorites_first_with_provider_model_ids() {
let names = [
("xai", "grok-3", "Grok 3"),
("openai", "gpt-5.5", "GPT-5.5"),
("openai-codex", "gpt-5.5", "GPT-5.5"),
("openai", "gpt-5.6-sol", "GPT-5.6 Sol"),
("openai", "gpt-5.6-sol-pro", "GPT-5.6 Sol"),
("xai", "grok-a", "Grok"),
("xai", "grok-b", "Grok"),
("opencode-go", "grok", "Grok"),
];
let options = with_catalog_names(&names, || {
model_config_options(
¤t("openai", "gpt-4"),
&["xai/grok-3".into()],
vec![
entry("openai", "gpt-4"),
entry("openai", "gpt-5.5"),
entry("openai-codex", "gpt-5.5"),
entry("xai", "grok-3"),
entry("openai", "gpt-5.6-sol"),
entry("openai", "gpt-5.6-sol-pro"),
entry("xai", "grok-a"),
entry("xai", "grok-b"),
entry("opencode-go", "grok"),
],
)
});
assert_eq!(options.len(), 1);
let option = &options[0];
assert_eq!(option.id.0.as_ref(), MODEL_CONFIG_ID);
assert_eq!(option.name, "Model");
assert_eq!(option.category, Some(SessionConfigOptionCategory::Model));
let select = select_options(option);
assert_eq!(select.current_value.0.as_ref(), "openai/gpt-4");
let expected = [
("xai/grok-3", "Grok 3"),
("openai/gpt-4", "gpt-4"),
("openai/gpt-5.5", "GPT-5.5 (openai)"),
("openai-codex/gpt-5.5", "GPT-5.5 (openai-codex)"),
("openai/gpt-5.6-sol", "gpt-5.6-sol"),
("openai/gpt-5.6-sol-pro", "gpt-5.6-sol-pro"),
("xai/grok-a", "grok-a (xai)"),
("xai/grok-b", "grok-b (xai)"),
("opencode-go/grok", "Grok (opencode-go)"),
]
.map(|(value, label)| {
(
value.to_string(),
label.to_string(),
Some(value.to_string()),
)
});
assert_eq!(option_entries(select), expected.to_vec());
}
#[test]
fn current_model_absent_from_available_is_appended() {
let options = with_catalog_names(&[], || {
model_config_options(
¤t("custom", "local-model"),
&[],
vec![entry("xai", "grok-3")],
)
});
let select = select_options(&options[0]);
assert_eq!(select.current_value.0.as_ref(), "custom/local-model");
assert_eq!(
option_values(select),
vec!["xai/grok-3".to_string(), "custom/local-model".to_string()]
);
}
#[test]
fn resolve_model_value_rejects_invalid_requests() {
let current = current("xai", "grok-3");
let available_auths = ["xai-api-key".to_string()];
let cases = [
(
"unknown config id",
SetSessionConfigOptionRequest::new(SessionId::new("session"), "mode", "xai/grok-3"),
),
(
"boolean value",
SetSessionConfigOptionRequest::new(SessionId::new("session"), MODEL_CONFIG_ID, true),
),
(
"unknown provider",
SetSessionConfigOptionRequest::new(
SessionId::new("session"),
MODEL_CONFIG_ID,
"no-such-provider/no-such-model",
),
),
(
"unknown model",
SetSessionConfigOptionRequest::new(
SessionId::new("session"),
MODEL_CONFIG_ID,
"xai/no-such-model",
),
),
];
for (label, request) in cases {
let error = resolve_model_value(&request, ¤t, &available_auths).expect_err(label);
assert_eq!(error.code, ErrorCode::InvalidParams, "{label}");
}
}
#[test]
fn resolve_model_value_accepts_current_model() {
let current = current("custom", "local-model");
let request = SetSessionConfigOptionRequest::new(
SessionId::new("session"),
MODEL_CONFIG_ID,
"custom/local-model",
);
let selection = resolve_model_value(&request, ¤t, &["xai-api-key".to_string()]).unwrap();
assert_eq!(
selection,
ModelSelection {
provider: "custom".into(),
model: "local-model".into(),
auth: "api-key".into(),
from_catalog: false,
}
);
}