use std::path::Path;use std::process::Command;use std::time::Duration;use anyhow::Context;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::MarketplaceUpgradeParams;use codex_app_server_protocol::MarketplaceUpgradeResponse;use codex_app_server_protocol::RequestId;use codex_config::MarketplaceConfigUpdate;use codex_config::record_user_marketplace;use codex_utils_absolute_path::AbsolutePathBuf;use pretty_assertions::assert_eq;use tempfile::TempDir;use tokio::time::timeout;#[cfg(windows)]const DEFAULT_TIMEOUT: Duration = Duration::from_secs(25);#[cfg(not(windows))]const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces";fn run_git(cwd: &Path, args: &[&str]) -> Result<String> { let output = Command::new("git").current_dir(cwd).args(args).output()?; if !output.status.success() { anyhow::bail!( "git {} failed in {}: {}", args.join(" "), cwd.display(), String::from_utf8_lossy(&output.stderr) ); } Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())}fn write_marketplace_files(root: &Path, marketplace_name: &str, marker: &str) -> Result<()> { std::fs::create_dir_all(root.join(".agents/plugins"))?; std::fs::write( root.join(".agents/plugins/marketplace.json"), format!(r#"{{"name":"{marketplace_name}","plugins":[]}}"#), )?; std::fs::write(root.join("marker.txt"), marker)?; Ok(())}fn init_marketplace_repo(root: &Path, marketplace_name: &str, marker: &str) -> Result<String> { run_git(root, &["init"])?; run_git(root, &["config", "user.email", "codex@example.com"])?; run_git(root, &["config", "user.name", "Codex Tests"])?; write_marketplace_files(root, marketplace_name, marker)?; run_git(root, &["add", "."])?; run_git(root, &["commit", "-m", "initial marketplace"])?; run_git(root, &["rev-parse", "HEAD"])}fn commit_marketplace_marker(root: &Path, marker: &str) -> Result<String> { std::fs::write(root.join("marker.txt"), marker)?; run_git(root, &["add", "marker.txt"])?; run_git(root, &["commit", "-m", "update marker"])?; run_git(root, &["rev-parse", "HEAD"])}fn configured_git_marketplace_update<'a>( source: &'a str, last_revision: Option<&'a str>, ref_name: Option<&'a str>,) -> MarketplaceConfigUpdate<'a> { MarketplaceConfigUpdate { last_updated: "2026-04-13T00:00:00Z", last_revision, source_type: "git", source, ref_name, sparse_paths: &[], }}fn configured_local_marketplace_update(source: &str) -> MarketplaceConfigUpdate<'_> { MarketplaceConfigUpdate { last_updated: "2026-04-13T00:00:00Z", last_revision: None, source_type: "local", source, ref_name: None, sparse_paths: &[], }}fn record_git_marketplace( codex_home: &Path, marketplace_name: &str, source: &Path, last_revision: &str, ref_name: Option<&str>,) -> Result<()> { let source = source.display().to_string(); record_user_marketplace( codex_home, marketplace_name, &configured_git_marketplace_update(&source, Some(last_revision), ref_name), )?; Ok(())}fn disable_plugin_startup_tasks(codex_home: &Path) -> Result<()> { let config_path = codex_home.join("config.toml"); let config = std::fs::read_to_string(&config_path)?; std::fs::write( config_path, format!("{config}\n[features]\nplugins = false\n"), )?; Ok(())}fn marketplace_install_root(codex_home: &Path) -> std::path::PathBuf { codex_home.join(INSTALLED_MARKETPLACES_DIR)}fn expected_installed_root(codex_home: &Path, marketplace_name: &str) -> Result<AbsolutePathBuf> { AbsolutePathBuf::try_from( marketplace_install_root(&codex_home.canonicalize()?).join(marketplace_name), ) .context("expected installed root should be absolute")}async fn send_marketplace_upgrade( mcp: &mut TestAppServer, marketplace_name: Option<&str>,) -> Result<MarketplaceUpgradeResponse> { let request_id = mcp .send_marketplace_upgrade_request(MarketplaceUpgradeParams { marketplace_name: marketplace_name.map(str::to_string), }) .await?; let response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(request_id)), ) .await??; to_response(response)}#[tokio::test]async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> { let codex_home = TempDir::new()?; let debug_source = TempDir::new()?; let tools_source = TempDir::new()?; let debug_old_revision = init_marketplace_repo(debug_source.path(), "debug", "debug old")?; let tools_old_revision = init_marketplace_repo(tools_source.path(), "tools", "tools old")?; let debug_new_revision = commit_marketplace_marker(debug_source.path(), "debug new")?; let tools_new_revision = commit_marketplace_marker(tools_source.path(), "tools new")?; record_git_marketplace( codex_home.path(), "debug", debug_source.path(), &debug_old_revision, Some(&debug_new_revision), )?; record_git_marketplace( codex_home.path(), "tools", tools_source.path(), &tools_old_revision, Some(&tools_new_revision), )?; disable_plugin_startup_tasks(codex_home.path())?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let debug_root = expected_installed_root(codex_home.path(), "debug")?; let tools_root = expected_installed_root(codex_home.path(), "tools")?; let response = send_marketplace_upgrade(&mut mcp, /*marketplace_name*/ None).await?; assert_eq!( response, MarketplaceUpgradeResponse { selected_marketplaces: vec!["debug".to_string(), "tools".to_string()], upgraded_roots: vec![debug_root.clone(), tools_root.clone()], errors: Vec::new(), } ); assert_eq!( std::fs::read_to_string(debug_root.as_path().join("marker.txt"))?, "debug new" ); assert_eq!( std::fs::read_to_string(tools_root.as_path().join("marker.txt"))?, "tools new" ); let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; assert!(config.contains(&debug_new_revision)); assert!(config.contains(&tools_new_revision)); Ok(())}#[tokio::test]async fn marketplace_upgrade_named_marketplace_only() -> Result<()> { let codex_home = TempDir::new()?; let debug_source = TempDir::new()?; let tools_source = TempDir::new()?; let debug_old_revision = init_marketplace_repo(debug_source.path(), "debug", "debug old")?; let tools_old_revision = init_marketplace_repo(tools_source.path(), "tools", "tools old")?; commit_marketplace_marker(debug_source.path(), "debug new")?; commit_marketplace_marker(tools_source.path(), "tools new")?; record_git_marketplace( codex_home.path(), "debug", debug_source.path(), &debug_old_revision, /*ref_name*/ None, )?; record_git_marketplace( codex_home.path(), "tools", tools_source.path(), &tools_old_revision, /*ref_name*/ None, )?; disable_plugin_startup_tasks(codex_home.path())?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let tools_root = expected_installed_root(codex_home.path(), "tools")?; let response = send_marketplace_upgrade(&mut mcp, Some("tools")).await?; assert_eq!( response, MarketplaceUpgradeResponse { selected_marketplaces: vec!["tools".to_string()], upgraded_roots: vec![tools_root.clone()], errors: Vec::new(), } ); assert_eq!( std::fs::read_to_string(tools_root.as_path().join("marker.txt"))?, "tools new" ); assert!( !marketplace_install_root(codex_home.path()) .join("debug") .exists() ); Ok(())}#[tokio::test]async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Result<()> { let codex_home = TempDir::new()?; let source = TempDir::new()?; let old_revision = init_marketplace_repo(source.path(), "debug", "debug old")?; commit_marketplace_marker(source.path(), "debug new")?; record_git_marketplace( codex_home.path(), "debug", source.path(), &old_revision, /*ref_name*/ None, )?; disable_plugin_startup_tasks(codex_home.path())?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let first_response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?; assert!(first_response.errors.is_empty()); let response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?; assert_eq!( response, MarketplaceUpgradeResponse { selected_marketplaces: vec!["debug".to_string()], upgraded_roots: Vec::new(), errors: Vec::new(), } ); Ok(())}#[tokio::test]async fn marketplace_upgrade_rejects_unknown_or_non_git_marketplace() -> Result<()> { let codex_home = TempDir::new()?; let local_source = TempDir::new()?; record_user_marketplace( codex_home.path(), "local-only", &configured_local_marketplace_update(&local_source.path().display().to_string()), )?; let mut mcp = TestAppServer::new(codex_home.path()).await?; timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; for marketplace_name in ["missing", "local-only"] { let request_id = mcp .send_marketplace_upgrade_request(MarketplaceUpgradeParams { marketplace_name: Some(marketplace_name.to_string()), }) .await?; let err = timeout( DEFAULT_TIMEOUT, mcp.read_stream_until_error_message(RequestId::Integer(request_id)), ) .await??; assert_eq!(err.error.code, -32600); assert_eq!( err.error.message, format!("marketplace `{marketplace_name}` is not configured as a Git marketplace"), ); } Ok(())}