use super::*;
use crate::model_catalog::{CatalogForUi, ModelCatalogEntry};
fn catalog(entries: Vec<ModelCatalogEntry>) -> CatalogForUi {
CatalogForUi {
entries,
stale: false,
notice: None,
}
}
fn select_field(editor: &mut SettingsEditor, path: &str) {
editor.tab = editor
.fields
.iter()
.find(|field| field.path == path)
.unwrap()
.tab;
editor.selected = editor
.visible_fields()
.iter()
.position(|field| field.path == path)
.unwrap();
}
#[test]
fn model_picker_all_pairs_cancel_accept_and_clear_without_refresh() {
for path in [
"agent.compaction.model",
"agent.summarizer.model",
"agent.side.model",
"sessions.titles.model",
"capabilities.tools.view_image.vision_model.model",
] {
let mut editor = ready_editor();
editor.set_catalog(&catalog(vec![ModelCatalogEntry::new(
"provider/with/slash",
"org/model/v1",
)]));
select_field(&mut editor, path);
assert!(matches!(
editor.key(key(KeyCode::Enter)),
EditorRequest::None
));
editor.key(key(KeyCode::End));
editor.key(key(KeyCode::Esc));
assert!(editor.changes().is_empty());
editor.key(key(KeyCode::Enter));
editor.key(key(KeyCode::End));
assert!(matches!(
editor.key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL)),
EditorRequest::None
));
assert!(editor.changes().is_empty());
editor.key(key(KeyCode::Enter));
let changes = editor.changes();
assert_eq!(changes.len(), 2);
assert!(changes.contains(&(path, json!("org/model/v1"))));
assert!(
changes
.iter()
.any(|(path, value)| path.ends_with(".provider") && value == "provider/with/slash")
);
editor.mark_saved();
editor.key(key(KeyCode::Enter));
editor.key(key(KeyCode::Home));
editor.key(key(KeyCode::Enter));
assert!(editor.changes().iter().all(|(_, value)| value.is_null()));
assert!(!editor.changes().is_empty());
}
}
#[test]
fn model_picker_uses_draft_exclusions_and_displays_missing_selection() {
let mut editor = ready_editor();
editor.set_catalog(&catalog(vec![
ModelCatalogEntry::new("anthropic", "enabled"),
ModelCatalogEntry::new("anthropic", "disabled"),
]));
editor.tab = MODELS_TAB;
editor.selected = editor
.models
.iter()
.position(|row| row.0 == "anthropic/disabled")
.unwrap()
+ 1;
editor.key(key(KeyCode::Enter));
select_field(&mut editor, "agent.compaction.model");
editor.key(key(KeyCode::Enter));
let picker = editor.model_picker.as_ref().unwrap();
assert_eq!(picker.choices.len(), 2);
assert!(
picker.choices[1]
.as_ref()
.unwrap()
.label
.contains("enabled")
);
editor.key(key(KeyCode::Esc));
for field in &mut editor.fields {
match field.path {
"agent.compaction.provider" => field.value = json!("missing-provider"),
"agent.compaction.model" => field.value = json!("missing/model"),
_ => {}
}
}
let label = editor.model_selection_label(editor.visible_fields()[editor.selected]);
assert_eq!(label, "missing-provider ยท missing/model");
editor.key(key(KeyCode::Enter));
assert_eq!(editor.model_picker.as_ref().unwrap().choices.len(), 2);
let changes = editor.changes();
editor.key(key(KeyCode::Tab));
assert!(editor.model_picker.is_none());
assert_eq!(editor.changes(), changes);
}
#[test]
fn model_picker_titles_cannot_clear_when_enabled_and_scope_cancels_choice() {
let mut editor = ready_editor();
select_field(&mut editor, "sessions.titles.enabled");
editor.key(key(KeyCode::Enter));
select_field(&mut editor, "sessions.titles.model");
editor.key(key(KeyCode::Enter));
assert!(editor.model_picker.as_ref().unwrap().choices.is_empty());
editor.key(key(KeyCode::Enter));
assert!(editor.model_picker.is_some());
editor.key(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL));
assert!(editor.model_picker.is_none());
assert!(editor.notice.contains("Save or discard"));
}
#[test]
fn model_picker_scrolls_to_selection_in_small_terminal_without_resizing() {
let mut editor = ready_editor();
editor.set_catalog(&catalog(
(0..100)
.map(|i| ModelCatalogEntry::new("anthropic", format!("model-{i:03}")))
.collect(),
));
select_field(&mut editor, "agent.compaction.model");
for (width, height) in [(40, 15), (80, 24), (120, 40)] {
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
let area = ratatui::layout::Rect::new(0, 0, width, height);
let geometry = render::models_geometry(area, &editor);
editor.key(key(KeyCode::Enter));
editor.key(key(KeyCode::End));
terminal
.draw(|frame| draw(frame, frame.area(), &editor))
.unwrap();
let text: String = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect();
assert!(text.contains("model-099"), "{width}x{height}: {text}");
assert_eq!(render::models_geometry(area, &editor), geometry);
editor.key(key(KeyCode::Esc));
}
}
#[test]
fn model_picker_persistence_pins_both_leaves_and_preserves_unknown_fields() {
let temp = tempfile::tempdir().unwrap();
let paths = crate::config::McPaths::from_root(temp.path().join("mc"));
std::fs::create_dir_all(&paths.root).unwrap();
std::fs::write(&paths.settings_file, r#"{"agent":{"compaction":{"provider":"anthropic","model":"old","future":42}},"future":{"enabled":true}}"#).unwrap();
let settings = crate::config::load_settings_editor(&paths, SettingsScope::Global).unwrap();
let mut editor = ready_editor();
editor.loaded(&settings);
editor.set_catalog(&catalog(vec![ModelCatalogEntry::new(
"anthropic",
"org/new",
)]));
select_field(&mut editor, "agent.compaction.model");
editor.key(key(KeyCode::Enter));
editor.key(key(KeyCode::End));
editor.key(key(KeyCode::Enter));
assert!(
editor
.changes()
.contains(&("agent.compaction.provider", json!("anthropic")))
);
crate::config::save_settings_editor(&paths, SettingsScope::Global, &editor.changes()).unwrap();
let raw: Value =
serde_json::from_str(&std::fs::read_to_string(&paths.settings_file).unwrap()).unwrap();
assert_eq!(
raw.pointer("/agent/compaction/model"),
Some(&json!("org/new"))
);
assert_eq!(
raw.pointer("/agent/compaction/provider"),
Some(&json!("anthropic"))
);
assert_eq!(raw.pointer("/agent/compaction/future"), Some(&json!(42)));
assert_eq!(raw.pointer("/future/enabled"), Some(&json!(true)));
}
#[test]
fn side_model_picker_keeps_pair_and_scope_until_saved() {
let mut editor = ready_editor();
let settings = Settings {
side: crate::config::SideAgentSettings {
provider: Some("anthropic".into()),
model: Some("shared-model".into()),
},
..Settings::default()
};
editor.loaded(&settings);
editor.set_catalog(&catalog(vec![ModelCatalogEntry::new(
"openai-codex",
"shared-model",
)]));
select_field(&mut editor, "agent.side.model");
assert!(
!editor
.visible_fields()
.iter()
.any(|field| field.path == "agent.side.provider")
);
assert_eq!(
editor.model_selection_label(editor.visible_fields()[editor.selected]),
"anthropic ยท shared-model"
);
editor.key(key(KeyCode::Enter));
editor.key(key(KeyCode::End));
editor.key(key(KeyCode::Enter));
let changes = editor.changes();
assert_eq!(changes.len(), 2);
assert!(changes.contains(&("agent.side.provider", json!("openai-codex"))));
assert!(changes.contains(&("agent.side.model", json!("shared-model"))));
editor.key(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL));
assert_eq!(editor.scope, SettingsScope::Global);
editor.set_catalog(&catalog(vec![]));
assert_eq!(editor.changes(), changes);
assert!(matches!(
editor.key(KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL)),
EditorRequest::Save
));
editor.mark_saved();
assert!(editor.restart_required);
assert!(editor.changes().is_empty());
}