use std::time::Duration;use anyhow::Result;use app_test_support::TestAppServer;use app_test_support::to_response;use codex_app_server_protocol::JSONRPCResponse;use codex_app_server_protocol::PermissionProfileListParams;use codex_app_server_protocol::PermissionProfileListResponse;use codex_app_server_protocol::PermissionProfileSummary;use codex_app_server_protocol::RequestId;use codex_core::config::set_project_trust_level;use codex_protocol::config_types::TrustLevel;use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;use pretty_assertions::assert_eq;use tempfile::TempDir;use tokio::time::timeout;const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);#[tokio::test]async fn permission_profile_list_returns_builtin_and_configured_profiles() -> Result<()> { let codex_home = TempDir::new()?; std::fs::write( codex_home.path().join("config.toml"), r#"default_permissions = "dev"[permissions.dev]description = "Day-to-day coding work."[permissions.dev.filesystem]":workspace_roots" = "write"[permissions.audit]description = "Inspect without writes."[permissions.audit.filesystem]":workspace_roots" = "read""#, )?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_permission_profile_list_request(PermissionProfileListParams { cursor: None, limit: None, cwd: None, }) .await?; let actual = read_response::<PermissionProfileListResponse>(&mut mcp, request_id).await?; assert_eq!( actual, PermissionProfileListResponse { data: vec![ PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, PermissionProfileSummary { id: "audit".to_string(), description: Some("Inspect without writes.".to_string()), }, PermissionProfileSummary { id: "dev".to_string(), description: Some("Day-to-day coding work.".to_string()), }, ], next_cursor: None, } ); Ok(())}#[tokio::test]async fn permission_profile_list_resolves_project_profiles_and_paginates() -> Result<()> { let codex_home = TempDir::new()?; let workspace = TempDir::new()?; let project_config_dir = workspace.path().join(".codex"); std::fs::create_dir_all(&project_config_dir)?; std::fs::write( codex_home.path().join("config.toml"), r#"default_permissions = ":workspace""#, )?; std::fs::write( project_config_dir.join("config.toml"), r#"[permissions.project]description = "Project-scoped profile."[permissions.project.filesystem]":workspace_roots" = "write""#, )?; set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let first_request_id = mcp .send_permission_profile_list_request(PermissionProfileListParams { cursor: None, limit: Some(3), cwd: Some(workspace.path().to_string_lossy().into_owned()), }) .await?; let first = read_response::<PermissionProfileListResponse>(&mut mcp, first_request_id).await?; assert_eq!( first, PermissionProfileListResponse { data: vec![ PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, ], next_cursor: Some("3".to_string()), } ); let second_request_id = mcp .send_permission_profile_list_request(PermissionProfileListParams { cursor: first.next_cursor, limit: Some(3), cwd: Some(workspace.path().to_string_lossy().into_owned()), }) .await?; let second = read_response::<PermissionProfileListResponse>(&mut mcp, second_request_id).await?; assert_eq!( second, PermissionProfileListResponse { data: vec![PermissionProfileSummary { id: "project".to_string(), description: Some("Project-scoped profile.".to_string()), }], next_cursor: None, } ); Ok(())}#[tokio::test]async fn permission_profile_list_discovers_project_profiles_without_default_selection() -> Result<()>{ let codex_home = TempDir::new()?; let workspace = TempDir::new()?; let project_config_dir = workspace.path().join(".codex"); std::fs::create_dir_all(&project_config_dir)?; std::fs::write( project_config_dir.join("config.toml"), r#"[permissions.project]description = "Project-scoped profile."[permissions.project.filesystem]":workspace_roots" = "write""#, )?; set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp .send_permission_profile_list_request(PermissionProfileListParams { cursor: None, limit: None, cwd: Some(workspace.path().to_string_lossy().into_owned()), }) .await?; let actual = read_response::<PermissionProfileListResponse>(&mut mcp, request_id).await?; assert_eq!( actual, PermissionProfileListResponse { data: vec![ PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string(), description: None, }, PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string(), description: None, }, PermissionProfileSummary { id: BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS.to_string(), description: None, }, PermissionProfileSummary { id: "project".to_string(), description: Some("Project-scoped profile.".to_string()), }, ], next_cursor: None, } ); Ok(())}async fn read_response<T: serde::de::DeserializeOwned>( mcp: &mut TestAppServer, request_id: i64,) -> Result<T> { let response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(request_id)), ) .await??; to_response(response)}