magi-code 0.80.2

Repository-aware CLI coding agent for terminal work
Documentation
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());
}