use anyhow::Result; use anyhow::bail; use app_test_support::McpProcess; use app_test_support::to_response; use codex_app_server_protocol::GetAuthStatusParams; use codex_app_server_protocol::GetAuthStatusResponse; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LogoutAccountResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerNotification; use codex_core::auth::AuthCredentialsStoreMode; use codex_login::login_with_api_key; use pretty_assertions::assert_eq; use std::path::Path; use tempfile::TempDir; use tokio::time::timeout; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); // Helper to create a minimal config.toml for the app server fn create_config_toml(codex_home: &Path) -> std::io::Result<()> { let config_toml = codex_home.join("config.toml"); std::fs::write( config_toml, r#" model = "mock-model" approval_policy = "never" sandbox_mode = "danger-full-access" model_provider = "mock_provider" [model_providers.mock_provider] name = "Mock provider for test" base_url = "http://127.0.0.1:0/v1" wire_api = "chat" request_max_retries = 0 stream_max_retries = 0 "#, ) } #[tokio::test] async fn logout_account_removes_auth_and_notifies() -> Result<()> { let codex_home = TempDir::new()?; create_config_toml(codex_home.path())?; login_with_api_key( codex_home.path(), "sk-test-key", AuthCredentialsStoreMode::File, )?; assert!(codex_home.path().join("auth.json").exists()); let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let id = mcp.send_logout_account_request().await?; let resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(id)), ) .await??; let _ok: LogoutAccountResponse = to_response(resp)?; let note = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_notification_message("account/updated"), ) .await??; let parsed: ServerNotification = note.try_into()?; let ServerNotification::AccountUpdated(payload) = parsed else { bail!("unexpected notification: {parsed:?}"); }; assert!( payload.auth_method.is_none(), "auth_method should be None after logout" ); assert!( !codex_home.path().join("auth.json").exists(), "auth.json should be deleted" ); let status_id = mcp .send_get_auth_status_request(GetAuthStatusParams { include_token: Some(true), refresh_token: Some(false), }) .await?; let status_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(status_id)), ) .await??; let status: GetAuthStatusResponse = to_response(status_resp)?; assert_eq!(status.auth_method, None); assert_eq!(status.auth_token, None); Ok(()) }