magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::support::*;
use super::*;

#[test]
fn logout_custom_provider_opens_confirmation_and_removes_metadata() {
    let temp = tempfile::TempDir::new().unwrap();
    let (sender, _receiver) = bounded::<TuiEvent>(4);
    let mut app = test_app(&temp, sender);
    crate::login::configure_custom_provider(
        &app.config.paths,
        "local-ai",
        "Local AI",
        "http://localhost:8080/v1",
        "LOCAL_AI_KEY",
    )
    .unwrap();
    let mut ui_state = state::MissionControlState::default();

    app.logout_provider("local-ai", &mut ui_state);

    assert!(matches!(
        ui_state.modals.logout_confirmation.as_ref(),
        Some(confirmation) if confirmation.provider_id == "local-ai"
    ));
    ui_state.close_logout_confirmation();
    app.remove_logout_provider("local-ai", &mut ui_state);

    assert!(ui_state.status.contains("removed custom provider local-ai"));
    assert!(
        !crate::config::read_settings(&app.config.paths)
            .unwrap()
            .custom_providers
            .contains_key("local-ai")
    );
    assert!(app.worker.is_none());
    assert!(!app.active_run);
}

#[test]
fn logout_direct_provider_opens_confirmation_without_worker_or_prompt() {
    let temp = tempfile::TempDir::new().unwrap();
    let (sender, receiver) = bounded::<TuiEvent>(4);
    let mut app = test_app(&temp, sender);
    app.config.provider = Some(crate::providers::OPENAI_CODEX_PROVIDER.to_string());
    app.state.config = Some(app.config.clone());
    let mut auth = crate::config::Auth::default();
    auth.providers.insert(
        crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
        crate::config::AuthProviderRecord::OAuth {
            access: "access".into(),
            refresh: Some("refresh".into()),
            expires: None,
            account_id: Some("acct".into()),
        },
    );
    crate::config::write_auth(&app.config.paths, &auth).unwrap();
    let mut ui_state = state::MissionControlState::default();

    app.submit(
        "/logout openai-codex".to_string(),
        &mut ui_state,
        &receiver,
        test_area(),
    );

    assert!(ui_state.modals.logout_confirmation.is_some());
    assert!(app.worker.is_none());
    assert!(!app.active_run);
    assert!(receiver.try_recv().is_err());
    assert!(
        crate::config::read_auth(&app.config.paths)
            .unwrap()
            .providers
            .contains_key(crate::providers::OPENAI_CODEX_PROVIDER)
    );
}

#[test]
fn logout_confirmation_removes_auth_and_clears_runtime_state() {
    let temp = tempfile::TempDir::new().unwrap();
    let (sender, _receiver) = bounded::<TuiEvent>(4);
    let mut app = ready_codex_app(&temp, sender);
    let mut ui_state = state::MissionControlState {
        provider_ready: true,
        ..Default::default()
    };

    app.remove_logout_provider(crate::providers::OPENAI_CODEX_PROVIDER, &mut ui_state);

    assert!(
        !crate::config::read_auth(&app.config.paths)
            .unwrap()
            .providers
            .contains_key(crate::providers::OPENAI_CODEX_PROVIDER)
    );
    assert!(!app.state.auth_state.is_ready());
    assert!(!ui_state.provider_ready);
    assert!(app.config.auth.is_none());
    assert!(app.state.config.as_ref().unwrap().auth.is_none());
}

#[test]
fn logout_picker_escape_preserves_auth_runtime_state_and_workers() {
    let temp = tempfile::TempDir::new().unwrap();
    let (sender, receiver) = bounded::<TuiEvent>(4);
    let app = ready_codex_app(&temp, sender);
    let mut ui_state = state::MissionControlState::default();
    ui_state.open_logout_picker(
        vec![state::LoginProviderEntry {
            id: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
            label: "OpenAI Codex".to_string(),
            description: "codex".to_string(),
            status: "configured".to_string(),
        }],
        None,
        input::DEFAULT_PICKER_VISIBLE_ROWS as usize,
    );

    let action = input::handle_key(
        crossterm::event::KeyEvent::new(
            crossterm::event::KeyCode::Esc,
            crossterm::event::KeyModifiers::NONE,
        ),
        &mut ui_state,
    );

    assert!(matches!(action, input::InputAction::None));
    assert!(ui_state.modals.logout_picker.is_none());
    assert!(
        crate::config::read_auth(&app.config.paths)
            .unwrap()
            .providers
            .contains_key(crate::providers::OPENAI_CODEX_PROVIDER)
    );
    assert!(app.state.auth_state.is_ready());
    assert!(app.config.auth.is_some());
    assert!(app.worker.is_none());
    assert!(!app.active_run);
    assert!(receiver.try_recv().is_err());
}

#[test]
fn logout_bad_provider_and_auth_read_failures_do_not_start_workers_or_submit_prompts() {
    let temp = tempfile::TempDir::new().unwrap();
    let (sender, receiver) = bounded::<TuiEvent>(4);
    let mut app = ready_codex_app(&temp, sender);
    let mut ui_state = state::MissionControlState::default();

    app.submit(
        "/logout bad-provider".to_string(),
        &mut ui_state,
        &receiver,
        test_area(),
    );

    assert!(app.worker.is_none());
    assert!(!app.active_run);
    assert!(
        ui_state
            .status
            .contains("unsupported logout provider 'bad-provider'")
    );
    assert!(receiver.try_recv().is_err());

    std::fs::write(&app.config.paths.auth_file, b"not json").unwrap();
    app.submit(
        "/logout openai-codex".to_string(),
        &mut ui_state,
        &receiver,
        test_area(),
    );

    assert!(app.worker.is_none());
    assert!(!app.active_run);
    assert!(
        ui_state.status.contains("expected ident")
            || ui_state.status.contains("key must be a string")
    );
    assert!(receiver.try_recv().is_err());
    assert!(ui_state.modals.logout_confirmation.is_none());
}

#[cfg(unix)]
#[test]
fn logout_unsafe_auth_file_failure_does_not_start_worker_or_submit_prompt() {
    use std::os::unix::fs::PermissionsExt;

    let temp = tempfile::TempDir::new().unwrap();
    let (sender, receiver) = bounded::<TuiEvent>(4);
    let mut app = ready_codex_app(&temp, sender);
    let mut permissions = std::fs::metadata(&app.config.paths.auth_file)
        .unwrap()
        .permissions();
    permissions.set_mode(0o644);
    std::fs::set_permissions(&app.config.paths.auth_file, permissions).unwrap();
    let mut ui_state = state::MissionControlState::default();

    app.submit(
        "/logout openai-codex".to_string(),
        &mut ui_state,
        &receiver,
        test_area(),
    );

    assert!(app.worker.is_none());
    assert!(!app.active_run);
    assert!(
        ui_state
            .status
            .contains("auth.json permissions must be private")
    );
    assert!(receiver.try_recv().is_err());
    assert!(ui_state.modals.logout_confirmation.is_none());
}

#[test]
fn post_logout_prompt_starts_worker_and_finishes_with_relogin_error() {
    let temp = tempfile::TempDir::new().unwrap();
    let (sender, receiver) = bounded::<TuiEvent>(8);
    let mut app = ready_codex_app(&temp, sender);
    let mut ui_state = state::MissionControlState {
        provider_ready: true,
        ..Default::default()
    };

    app.remove_logout_provider(crate::providers::OPENAI_CODEX_PROVIDER, &mut ui_state);
    assert!(app.submit("hello".to_string(), &mut ui_state, &receiver, test_area()));
    assert!(app.worker.is_some());
    assert!(app.active_run);
    while app
        .worker
        .as_ref()
        .is_some_and(|worker| !worker.handle.is_finished())
    {
        std::thread::yield_now();
    }
    assert!(drain_tui_events(&receiver, &mut ui_state).run_finished);
    assert!(app.finish_worker_if_ready(&mut ui_state));

    assert!(app.worker.is_none());
    assert!(!app.active_run);
    assert!(!ui_state.provider_ready);
    let message = ui_state.status.clone();
    assert!(
        message.contains("openai-codex credential refresh failed")
            && message.contains("run /login openai-codex"),
        "unexpected message: {message}"
    );
}

#[test]
fn post_logout_model_discovery_requires_relogin() {
    let temp = tempfile::TempDir::new().unwrap();
    let (sender, _receiver) = bounded::<TuiEvent>(4);
    let mut app = ready_codex_app(&temp, sender);
    let mut ui_state = state::MissionControlState {
        provider_ready: true,
        ..Default::default()
    };

    app.remove_logout_provider(crate::providers::OPENAI_CODEX_PROVIDER, &mut ui_state);
    app.open_model_picker(&mut ui_state, test_area());

    assert!(ui_state.modals.model_picker.is_none());
    assert!(ui_state.status.contains("loading model catalogs"));
}