use super::super::clients::list_remote_control_clients;use super::super::clients::revoke_remote_control_client;use super::*;use codex_app_server_protocol::RemoteControlClient;use codex_app_server_protocol::RemoteControlClientsListOrder;use codex_app_server_protocol::RemoteControlClientsListParams;use codex_app_server_protocol::RemoteControlClientsListResponse;use codex_app_server_protocol::RemoteControlClientsRevokeParams;use codex_app_server_protocol::RemoteControlClientsRevokeResponse;use codex_login::AuthKeyringBackendKind;use pretty_assertions::assert_eq;fn client_management_handle( remote_control_url: String, auth_manager: Arc<AuthManager>,) -> RemoteControlHandle { let desired_state_tx = watch::channel(RemoteControlDesiredState::Disabled).0; let (status_tx, _status_rx) = watch::channel(RemoteControlStatusChangedNotification { status: RemoteControlConnectionStatus::Disabled, server_name: test_server_name(), installation_id: TEST_INSTALLATION_ID.to_string(), environment_id: None, }); RemoteControlHandle { policy: RemoteControlPolicy::Allowed, desired_state_tx: Arc::new(desired_state_tx), desired_state_rpc_lock: Arc::new(Semaphore::new(1)), desired_state_persistence_lock: Arc::new(Semaphore::new(1)), status_tx: Arc::new(status_tx), state_db: None, remote_control_url, current_enrollment: Arc::new(RemoteControlEnrollmentState::new(/*enrollment*/ None)), pairing_persistence_key: watch::channel(None).0, pairing_persistence_key_required: false, auth_manager, }}fn empty_client_list() -> serde_json::Value { json!({ "items": [], "cursor": null, })}#[tokio::test]async fn remote_control_handle_lists_clients_while_disabled() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let remote_control_url = remote_control_url_for_listener(&listener); let server_task = tokio::spawn(async move { let request = accept_http_request(&listener).await; assert_eq!( request.request_line, "GET /backend-api/wham/remote/control/environments/env%20%2F%3F/clients?cursor=cursor+%2F%3F&limit=10&order=asc HTTP/1.1" ); assert_eq!( request.headers.get("authorization"), Some(&"Bearer Access Token".to_string()) ); assert_eq!( request.headers.get(REMOTE_CONTROL_ACCOUNT_ID_HEADER), Some(&"account_id".to_string()) ); respond_with_json( request.stream, json!({ "items": [{ "client_id": "client-123", "account_user_id": "user-123", "enrollment_status": "enrolled_device_key", "display_name": "Anton Phone", "device_type": "phone", "platform": "ios", "os_version": "19.0", "device_model": "iPhone", "app_version": "1.2.3", "last_seen_at": "2026-03-05T07:00:00Z", "last_seen_city": "San Francisco", }], "cursor": "next-cursor", }), ) .await; }); let handle = client_management_handle(remote_control_url, remote_control_auth_manager()); let response = handle .list_clients(RemoteControlClientsListParams { environment_id: "env /?".to_string(), cursor: Some("cursor /?".to_string()), limit: Some(10), order: Some(RemoteControlClientsListOrder::Asc), }) .await .expect("client list should succeed while remote control is disabled"); server_task.await.expect("server task should finish"); assert_eq!( response, RemoteControlClientsListResponse { data: vec![RemoteControlClient { client_id: "client-123".to_string(), display_name: Some("Anton Phone".to_string()), device_type: Some("phone".to_string()), platform: Some("ios".to_string()), os_version: Some("19.0".to_string()), device_model: Some("iPhone".to_string()), app_version: Some("1.2.3".to_string()), last_seen_at: Some(1_772_694_000), }], next_cursor: Some("next-cursor".to_string()), } );}#[tokio::test]async fn remote_control_handle_revokes_client_while_disabled() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let remote_control_url = remote_control_url_for_listener(&listener); let server_task = tokio::spawn(async move { let request = accept_http_request(&listener).await; assert_eq!( request.request_line, "DELETE /backend-api/wham/remote/control/environments/env%20%2F%3F/clients/client%20%2F%3F HTTP/1.1" ); respond_with_status(request.stream, "204 No Content", "").await; }); let handle = client_management_handle(remote_control_url, remote_control_auth_manager()); let response = handle .revoke_client(RemoteControlClientsRevokeParams { environment_id: "env /?".to_string(), client_id: "client /?".to_string(), }) .await .expect("client revoke should succeed while remote control is disabled"); server_task.await.expect("server task should finish"); assert_eq!(response, RemoteControlClientsRevokeResponse {});}#[tokio::test]async fn list_remote_control_clients_recovers_auth_after_unauthorized() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let remote_control_url = remote_control_url_for_listener(&listener); let server_task = tokio::spawn(async move { let stale_request = accept_http_request(&listener).await; assert_eq!( stale_request.headers.get("authorization"), Some(&"Bearer stale-token".to_string()) ); respond_with_status(stale_request.stream, "401 Unauthorized", "").await; let recovered_request = accept_http_request(&listener).await; assert_eq!( recovered_request.headers.get("authorization"), Some(&"Bearer fresh-token".to_string()) ); respond_with_json(recovered_request.stream, empty_client_list()).await; }); let codex_home = TempDir::new().expect("temp dir should create"); let mut stale_auth = remote_control_auth_dot_json(Some("account_id")); stale_auth .tokens .as_mut() .expect("stale auth should include tokens") .access_token = "stale-token".to_string(); save_auth( codex_home.path(), &stale_auth, AuthCredentialsStoreMode::File, AuthKeyringBackendKind::default(), ) .expect("stale auth should save"); let auth_manager = AuthManager::shared( codex_home.path().to_path_buf(), /*enable_codex_api_key_env*/ false, AuthCredentialsStoreMode::File, /*chatgpt_base_url*/ None, AuthKeyringBackendKind::default(), ) .await; let mut fresh_auth = remote_control_auth_dot_json(Some("account_id")); fresh_auth .tokens .as_mut() .expect("fresh auth should include tokens") .access_token = "fresh-token".to_string(); save_auth( codex_home.path(), &fresh_auth, AuthCredentialsStoreMode::File, AuthKeyringBackendKind::default(), ) .expect("fresh auth should save"); let response = list_remote_control_clients( &remote_control_url, &auth_manager, RemoteControlClientsListParams { environment_id: "env-123".to_string(), ..Default::default() }, ) .await .expect("client list should recover auth"); server_task.await.expect("server task should finish"); assert_eq!( response, RemoteControlClientsListResponse { data: Vec::new(), next_cursor: None, } );}#[tokio::test]async fn list_remote_control_clients_retries_unauthorized_only_once() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let remote_control_url = remote_control_url_for_listener(&listener); let server_task = tokio::spawn(async move { let stale_request = accept_http_request(&listener).await; assert_eq!( stale_request.headers.get("authorization"), Some(&"Bearer stale-token".to_string()) ); respond_with_status(stale_request.stream, "401 Unauthorized", "").await; let recovered_request = accept_http_request(&listener).await; assert_eq!( recovered_request.headers.get("authorization"), Some(&"Bearer fresh-token".to_string()) ); respond_with_status(recovered_request.stream, "401 Unauthorized", "").await; assert!( timeout(Duration::from_millis(100), accept_http_request(&listener)) .await .is_err() ); }); let codex_home = TempDir::new().expect("temp dir should create"); let mut stale_auth = remote_control_auth_dot_json(Some("account_id")); stale_auth .tokens .as_mut() .expect("stale auth should include tokens") .access_token = "stale-token".to_string(); save_auth( codex_home.path(), &stale_auth, AuthCredentialsStoreMode::File, AuthKeyringBackendKind::default(), ) .expect("stale auth should save"); let auth_manager = AuthManager::shared( codex_home.path().to_path_buf(), /*enable_codex_api_key_env*/ false, AuthCredentialsStoreMode::File, /*chatgpt_base_url*/ None, AuthKeyringBackendKind::default(), ) .await; let mut fresh_auth = remote_control_auth_dot_json(Some("account_id")); fresh_auth .tokens .as_mut() .expect("fresh auth should include tokens") .access_token = "fresh-token".to_string(); save_auth( codex_home.path(), &fresh_auth, AuthCredentialsStoreMode::File, AuthKeyringBackendKind::default(), ) .expect("fresh auth should save"); let err = list_remote_control_clients( &remote_control_url, &auth_manager, RemoteControlClientsListParams { environment_id: "env-123".to_string(), ..Default::default() }, ) .await .expect_err("second unauthorized response should fail"); server_task.await.expect("server task should finish"); assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);}#[tokio::test]async fn revoke_remote_control_client_does_not_retry_forbidden() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let remote_control_url = remote_control_url_for_listener(&listener); let server_task = tokio::spawn(async move { let request = accept_http_request(&listener).await; respond_with_status_and_headers( request.stream, "403 Forbidden", &[("x-request-id", "request-123"), ("cf-ray", "ray-123")], "forbidden", ) .await; }); let err = revoke_remote_control_client( &remote_control_url, &remote_control_auth_manager(), RemoteControlClientsRevokeParams { environment_id: "env-123".to_string(), client_id: "client-123".to_string(), }, ) .await .expect_err("forbidden revoke should fail"); server_task.await.expect("server task should finish"); assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied); assert_eq!( err.to_string(), format!( "remote control client revoke failed at `{remote_control_url}wham/remote/control/environments/env-123/clients/client-123`: HTTP 403 Forbidden, request-id: request-123, cf-ray: ray-123, body: forbidden" ) );}#[tokio::test]async fn list_remote_control_clients_preserves_decode_error_context() { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let remote_control_url = remote_control_url_for_listener(&listener); let server_task = tokio::spawn(async move { let request = accept_http_request(&listener).await; respond_with_status(request.stream, "200 OK", "{").await; }); let err = list_remote_control_clients( &remote_control_url, &remote_control_auth_manager(), RemoteControlClientsListParams { environment_id: "env-123".to_string(), ..Default::default() }, ) .await .expect_err("malformed client list should fail"); server_task.await.expect("server task should finish"); assert!( err.to_string().contains( "failed to parse remote control client list response from `http://127.0.0.1:" ) ); assert!(err.to_string().contains("HTTP 200 OK")); assert!(err.to_string().contains("body: {")); assert!(err.to_string().contains("decode error:"));}