Download codex-rs/cli/src/doctor/updates.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 27.2 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/doctor/updates.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/cli/src/doctor/updates.rs
-
curl -L -o updates.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/doctor/updates.rs
27.2 kB
| //! Diagnoses whether Codex update paths target the running installation. | |
| //! | |
| //! Update diagnostics combine cached version metadata, install-channel hints, | |
| //! and bounded latest-version HTTP probes. It never executes package managers or | |
| //! other helpers selected by PATH; npm update targets are not verified. | |
| use std::path::Path; | |
| use std::path::PathBuf; | |
| use std::time::Duration; | |
| use codex_core::config::Config; | |
| use codex_http_client::ClientRouteClass; | |
| use codex_http_client::RouteAwareClientPool; | |
| use codex_install_context::InstallContext; | |
| use codex_install_context::InstallMethod; | |
| use http::Method; | |
| use serde::Deserialize; | |
| use url::Url; | |
| use super::CheckStatus; | |
| use super::DoctorCheck; | |
| use super::DoctorIssue; | |
| use super::desktop::platform::InstalledApp; | |
| use super::doctor_install_context; | |
| use super::doctor_managed_by_npm; | |
| use super::network; | |
| const MAX_VERSION_RESPONSE_BYTES: usize = 1024 * 1024; | |
| const VERSION_FILE_NAME: &str = "version.json"; | |
| const GITHUB_LATEST_RELEASE_URL: &str = "https://api.github.com/repos/openai/codex/releases/latest"; | |
| const HOMEBREW_CASK_API_URL: &str = "https://formulae.brew.sh/api/cask/codex.json"; | |
| const DESKTOP_UPDATE_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/appcast-x64.xml"; | |
| const DESKTOP_UPDATE_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml"; | |
| const BACKEND_DESKTOP_UPDATE_URL: &str = "https://chatgpt.com/backend-api/wham/app/appcast"; | |
| const DESKTOP_UPDATE_URL: &str = | |
| "https://persistent.oaistatic.com/codex-app-prod/windows-store-update.json"; | |
| /// Builds the update-health row for the current installation. | |
| /// | |
| /// Network failures while fetching latest-version metadata degrade the row to a | |
| /// warning instead of failing doctor outright; update freshness is useful | |
| /// support context but should not mask more direct install/config failures. | |
| pub(super) async fn updates_check(config: &Config) -> DoctorCheck { | |
| let current_exe = std::env::current_exe().ok(); | |
| let install_context = doctor_install_context(current_exe.as_deref()); | |
| let mut details = vec![ | |
| format!( | |
| "check for update on startup: {}", | |
| config.check_for_update_on_startup | |
| ), | |
| format!("update action: {}", update_action_label(&install_context)), | |
| ]; | |
| let version_file = config.codex_home.join(VERSION_FILE_NAME); | |
| push_cached_version_details(&mut details, &version_file); | |
| let mut status = CheckStatus::Ok; | |
| let summary = "update configuration is locally consistent".to_string(); | |
| if doctor_managed_by_npm(current_exe.as_deref()) { | |
| details | |
| .push("npm update target: not inspected (PATH helpers are not executed)".to_string()); | |
| } | |
| let client = RouteAwareClientPool::new_without_request_logging( | |
| config.http_client_factory(), | |
| ClientRouteClass::Other, | |
| ); | |
| match fetch_latest_version(&client, &install_context).await { | |
| Ok(latest_version) => { | |
| details.push(format!("latest version: {latest_version}")); | |
| if is_newer(&latest_version, env!("CARGO_PKG_VERSION")) == Some(true) { | |
| details.push("latest version status: newer version is available".to_string()); | |
| } else { | |
| details.push("latest version status: current version is not older".to_string()); | |
| } | |
| } | |
| Err(err) => { | |
| status = status.max(CheckStatus::Warning); | |
| details.push(format!("latest version probe: {err}")); | |
| } | |
| } | |
| DoctorCheck::new("updates.status", "updates", status, summary).details(details) | |
| } | |
| pub(super) async fn append_desktop_update( | |
| checks: &mut [DoctorCheck], | |
| config: Option<&Config>, | |
| application: &InstalledApp, | |
| ) { | |
| let deadline = tokio::time::Instant::now() + Duration::from_secs(2); | |
| if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) | |
| && let Some(build) = latest_macos_staged_build( | |
| &home | |
| .join("Library/Caches") | |
| .join(application.identity) | |
| .join("org.sparkle-project.Sparkle/Installation"), | |
| application.build, | |
| ) | |
| .await | |
| && let Some(update) = checks.iter_mut().find(|check| check.id == "updates.status") | |
| { | |
| update.details.extend([ | |
| "desktop update status: ready to install".to_string(), | |
| format!("desktop latest build: {build}"), | |
| format!("desktop application: {}", application.identity), | |
| ]); | |
| } | |
| let Some(config) = config else { | |
| return; | |
| }; | |
| let Some(reachability_index) = checks | |
| .iter() | |
| .position(|check| check.id == "network.provider_reachability") | |
| else { | |
| return; | |
| }; | |
| let desktop_update_url = std::env::var_os("HOME") | |
| .map(PathBuf::from) | |
| .map(|home| { | |
| macos_desktop_update_url(&home, application, &os_info::get().version().to_string()) | |
| }) | |
| .unwrap_or_else(|| DESKTOP_UPDATE_URL.to_string()); | |
| let desktop_update_url = DESKTOP_UPDATE_URL; | |
| let desktop_update_url = desktop_update_url.as_str(); | |
| let desktop_update_display_url = desktop_update_url | |
| .split_once('?') | |
| .map_or(desktop_update_url, |(endpoint, _)| endpoint); | |
| let client = RouteAwareClientPool::new_without_request_logging( | |
| config.http_client_factory(), | |
| ClientRouteClass::Other, | |
| ); | |
| let outcome = match client | |
| .request(Method::GET, desktop_update_url) | |
| .timeout(deadline.saturating_duration_since(tokio::time::Instant::now())) | |
| .send() | |
| .await | |
| { | |
| Ok(response) => { | |
| let status = response.status().as_u16(); | |
| if status == 404 && response.url().scheme() == "https" { | |
| checks[reachability_index].details.push(format!( | |
| "desktop assets CDN: {desktop_update_display_url} reachable (HTTP 404; no update available)" | |
| )); | |
| return; | |
| } | |
| if cfg!(target_os = "windows") && response.url().scheme() != "https" { | |
| Err("update manifest redirected to a non-HTTPS URL".to_string()) | |
| } else if status == 407 { | |
| Err("proxy authentication required (HTTP 407)".to_string()) | |
| } else if !(200..=299).contains(&status) { | |
| Err(format!("HTTP {status}")) | |
| } else { | |
| checks[reachability_index].details.push(format!( | |
| "desktop assets CDN: {desktop_update_display_url} reachable (HTTP {status})" | |
| )); | |
| if let Some(update) = checks.iter_mut().find(|check| check.id == "updates.status") { | |
| match response.bytes().await { | |
| Ok(body) => match windows_store_update(&body, &application.version) { | |
| Ok(Some(build)) => update.details.extend([ | |
| "desktop update status: available".to_string(), | |
| format!("desktop latest build: {build}"), | |
| format!("desktop application: {}", application.identity), | |
| ]), | |
| Ok(None) => {} | |
| Err(error) => { | |
| update.status = update.status.max(CheckStatus::Warning); | |
| update | |
| .details | |
| .push(format!("desktop update manifest: {error}")); | |
| } | |
| }, | |
| Err(_) => { | |
| update.status = update.status.max(CheckStatus::Warning); | |
| update | |
| .details | |
| .push("desktop update manifest: response could not be read".into()); | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } | |
| } | |
| Err(error) => Err(network::request_error(error)), | |
| }; | |
| if let Err(error) = outcome { | |
| let reachability = &mut checks[reachability_index]; | |
| reachability.details.push(format!( | |
| "desktop assets CDN: {desktop_update_display_url} {error} (optional)" | |
| )); | |
| if reachability.status == CheckStatus::Ok { | |
| reachability.status = CheckStatus::Warning; | |
| reachability.summary = "desktop update and runtime CDN is unreachable".to_string(); | |
| } | |
| reachability.issues.push( | |
| DoctorIssue::new( | |
| CheckStatus::Warning, | |
| "desktop update and runtime CDN is unreachable", | |
| ) | |
| .measured(format!("{desktop_update_display_url} {error}")) | |
| .expected("desktop update and runtime CDN reachable over HTTPS") | |
| .remedy( | |
| if desktop_update_display_url.starts_with("https://chatgpt.com/") { | |
| "check proxy, firewall, DNS, and certificate access to chatgpt.com" | |
| } else { | |
| "check proxy, firewall, DNS, and certificate access to persistent.oaistatic.com" | |
| }, | |
| ) | |
| .field("desktop assets CDN"), | |
| ); | |
| } | |
| } | |
| fn macos_desktop_update_url(home: &Path, application: &InstalledApp, os_version: &str) -> String { | |
| struct ProductionAppcastState { | |
| backend_appcast_enabled: bool, | |
| installation_id: Option<String>, | |
| } | |
| let state_path = home | |
| .join("Library/Application Support") | |
| .join(application.identity) | |
| .join("production-appcast-bootstrap.json"); | |
| let Some(state) = std::fs::read(state_path) | |
| .ok() | |
| .and_then(|contents| serde_json::from_slice::<ProductionAppcastState>(&contents).ok()) | |
| else { | |
| return DESKTOP_UPDATE_URL.to_string(); | |
| }; | |
| let Some(installation_id) = state | |
| .backend_appcast_enabled | |
| .then_some(state.installation_id) | |
| .flatten() | |
| else { | |
| return DESKTOP_UPDATE_URL.to_string(); | |
| }; | |
| let Ok(mut url) = Url::parse(BACKEND_DESKTOP_UPDATE_URL) else { | |
| return DESKTOP_UPDATE_URL.to_string(); | |
| }; | |
| url.query_pairs_mut().extend_pairs([ | |
| ("installation_id", installation_id.as_str()), | |
| ( | |
| "arch", | |
| if cfg!(target_arch = "x86_64") { | |
| "x64" | |
| } else { | |
| "arm64" | |
| }, | |
| ), | |
| ("app_version", application.version.as_str()), | |
| ("beta", "false"), | |
| ("os-version", os_version), | |
| ("plan_type", "unknown"), | |
| ]); | |
| url.to_string() | |
| } | |
| fn windows_store_update( | |
| manifest: &[u8], | |
| installed_version: &str, | |
| ) -> Result<Option<String>, &'static str> { | |
| struct StoreManifest { | |
| schema_version: u64, | |
| build_version: String, | |
| store_product_id: String, | |
| package_identity: String, | |
| } | |
| let manifest: StoreManifest = | |
| serde_json::from_slice(manifest).map_err(|_| "invalid Windows Store update manifest")?; | |
| if manifest.schema_version == 0 | |
| || manifest.store_product_id != "9PLM9XGG6VKS" | |
| || manifest.package_identity != "OpenAI.Codex" | |
| { | |
| return Err("Windows Store update manifest does not target the production application"); | |
| } | |
| let version = |value: &str| -> Option<[u64; 4]> { | |
| value | |
| .split('.') | |
| .map(str::parse::<u64>) | |
| .collect::<Result<Vec<_>, _>>() | |
| .ok()? | |
| .try_into() | |
| .ok() | |
| }; | |
| let latest = version(&manifest.build_version) | |
| .ok_or("Windows Store update manifest contains an invalid build version")?; | |
| let installed = | |
| version(installed_version).ok_or("installed Windows application has an invalid version")?; | |
| Ok((latest > installed).then_some(manifest.build_version)) | |
| } | |
| async fn latest_macos_staged_build(root: &Path, installed_build: u64) -> Option<u64> { | |
| const MAX_STAGED_BUNDLES: usize = 64; | |
| if !std::fs::symlink_metadata(root).ok()?.is_dir() { | |
| return None; | |
| } | |
| let deadline = tokio::time::Instant::now() + Duration::from_secs(1); | |
| let mut inspected = 0; | |
| let mut latest = None; | |
| for entry in std::fs::read_dir(root).ok()? { | |
| if inspected == MAX_STAGED_BUNDLES || tokio::time::Instant::now() >= deadline { | |
| break; | |
| } | |
| let Ok(entry) = entry else { | |
| continue; | |
| }; | |
| if !entry.file_type().is_ok_and(|kind| kind.is_dir()) { | |
| continue; | |
| } | |
| let extracted = entry.path().join("extracted"); | |
| if !std::fs::symlink_metadata(&extracted).is_ok_and(|metadata| metadata.is_dir()) { | |
| continue; | |
| } | |
| let bundle = extracted.join("ChatGPT.app"); | |
| if !std::fs::symlink_metadata(&bundle).is_ok_and(|metadata| metadata.is_dir()) { | |
| continue; | |
| } | |
| inspected += 1; | |
| let Ok(result) = tokio::time::timeout_at( | |
| deadline, | |
| super::desktop::platform::inspect_macos_bundle(&bundle), | |
| ) | |
| .await | |
| else { | |
| break; | |
| }; | |
| if let Ok(Some(application)) = result | |
| && application.build > installed_build | |
| { | |
| latest = Some(latest.map_or(application.build, |latest: u64| { | |
| latest.max(application.build) | |
| })); | |
| } | |
| } | |
| latest | |
| } | |
| fn push_cached_version_details(details: &mut Vec<String>, version_file: &Path) { | |
| details.push(format!("version cache: {}", version_file.display())); | |
| match std::fs::read_to_string(version_file) { | |
| Ok(contents) => match serde_json::from_str::<VersionInfo>(&contents) { | |
| Ok(info) => { | |
| details.push(format!("cached latest version: {}", info.latest_version)); | |
| if let Some(last_checked_at) = info.last_checked_at { | |
| details.push(format!("last checked at: {last_checked_at}")); | |
| } | |
| if let Some(dismissed_version) = info.dismissed_version { | |
| details.push(format!("dismissed version: {dismissed_version}")); | |
| } | |
| } | |
| Err(err) => details.push(format!("version cache parse: {err}")), | |
| }, | |
| Err(err) if err.kind() == std::io::ErrorKind::NotFound => { | |
| details.push("version cache: missing".to_string()); | |
| } | |
| Err(err) => details.push(format!("version cache read: {err}")), | |
| } | |
| } | |
| fn update_action_label(context: &InstallContext) -> &'static str { | |
| match &context.method { | |
| InstallMethod::Npm => "npm install -g @openai/codex", | |
| InstallMethod::Bun => "bun install -g @openai/codex", | |
| InstallMethod::VitePlus => "vp install -g @openai/codex", | |
| InstallMethod::Pnpm => "pnpm add -g @openai/codex", | |
| InstallMethod::Brew => "brew upgrade --cask codex", | |
| InstallMethod::Standalone { .. } => "standalone installer", | |
| InstallMethod::Other => "manual or unknown", | |
| } | |
| } | |
| async fn fetch_latest_version( | |
| client: &RouteAwareClientPool, | |
| context: &InstallContext, | |
| ) -> Result<String, String> { | |
| match &context.method { | |
| InstallMethod::Brew => fetch_homebrew_cask_version(client).await, | |
| InstallMethod::Npm | |
| | InstallMethod::Bun | |
| | InstallMethod::VitePlus | |
| | InstallMethod::Pnpm | |
| | InstallMethod::Standalone { .. } | |
| | InstallMethod::Other => fetch_latest_github_release_version(client).await, | |
| } | |
| } | |
| async fn fetch_latest_github_release_version( | |
| client: &RouteAwareClientPool, | |
| ) -> Result<String, String> { | |
| struct ReleaseInfo { | |
| tag_name: String, | |
| } | |
| let info = http_get_json::<ReleaseInfo>(client, GITHUB_LATEST_RELEASE_URL).await?; | |
| info.tag_name | |
| .strip_prefix("rust-v") | |
| .map(str::to_string) | |
| .ok_or_else(|| format!("failed to parse latest tag {}", info.tag_name)) | |
| } | |
| async fn fetch_homebrew_cask_version(client: &RouteAwareClientPool) -> Result<String, String> { | |
| struct HomebrewCaskInfo { | |
| version: String, | |
| } | |
| http_get_json::<HomebrewCaskInfo>(client, HOMEBREW_CASK_API_URL) | |
| .await | |
| .map(|info| info.version) | |
| } | |
| async fn http_get_json<T>(client: &RouteAwareClientPool, url: &str) -> Result<T, String> | |
| where | |
| T: for<'de> Deserialize<'de>, | |
| { | |
| tokio::time::timeout(Duration::from_secs(/*secs*/ 5), async { | |
| let mut response = client | |
| .request(Method::GET, url) | |
| .header( | |
| http::header::USER_AGENT, | |
| concat!("codex-doctor/", env!("CARGO_PKG_VERSION")), | |
| ) | |
| .send() | |
| .await | |
| .map_err(|err| err.to_string())?; | |
| if !response.status().is_success() { | |
| return Err(format!("HTTP {}", response.status())); | |
| } | |
| let mut body = Vec::new(); | |
| while let Some(chunk) = response.chunk().await.map_err(|err| err.to_string())? { | |
| if chunk.len() > MAX_VERSION_RESPONSE_BYTES.saturating_sub(body.len()) { | |
| return Err("version response exceeds size limit".to_string()); | |
| } | |
| body.extend_from_slice(&chunk); | |
| } | |
| serde_json::from_slice(&body).map_err(|err| err.to_string()) | |
| }) | |
| .await | |
| .map_err(|_| "version request timed out".to_string())? | |
| } | |
| fn is_newer(latest: &str, current: &str) -> Option<bool> { | |
| match (parse_version(latest), parse_version(current)) { | |
| (Some(latest), Some(current)) => Some(latest > current), | |
| (Some(_), None) | (None, Some(_)) | (None, None) => None, | |
| } | |
| } | |
| fn parse_version(value: &str) -> Option<(u64, u64, u64)> { | |
| let mut parts = value.trim().split('.'); | |
| let major = parts.next()?.parse::<u64>().ok()?; | |
| let minor = parts.next()?.parse::<u64>().ok()?; | |
| let patch = parts.next()?.parse::<u64>().ok()?; | |
| Some((major, minor, patch)) | |
| } | |
| struct VersionInfo { | |
| latest_version: String, | |
| last_checked_at: Option<String>, | |
| dismissed_version: Option<String>, | |
| } | |
| mod tests { | |
| use super::*; | |
| use pretty_assertions::assert_eq; | |
| async fn version_http_probe_decodes_json_and_rejects_invalid_responses() { | |
| use codex_http_client::HttpClientFactory; | |
| use codex_http_client::OutboundProxyPolicy; | |
| use wiremock::Mock; | |
| use wiremock::MockServer; | |
| use wiremock::ResponseTemplate; | |
| use wiremock::matchers::method; | |
| use wiremock::matchers::path; | |
| let server = MockServer::start().await; | |
| let client = RouteAwareClientPool::new_without_request_logging( | |
| HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), | |
| ClientRouteClass::Other, | |
| ); | |
| for (endpoint, response) in [ | |
| ( | |
| "valid", | |
| ResponseTemplate::new(/*s*/ 200) | |
| .set_body_json(serde_json::json!({"version": "1.2.3"})), | |
| ), | |
| ( | |
| "invalid", | |
| ResponseTemplate::new(/*s*/ 200).set_body_string("not JSON"), | |
| ), | |
| ("unavailable", ResponseTemplate::new(/*s*/ 503)), | |
| ("proxy_auth_required", ResponseTemplate::new(/*s*/ 407)), | |
| ( | |
| "redirect", | |
| ResponseTemplate::new(/*s*/ 302) | |
| .insert_header("Location", format!("{}/valid", server.uri())), | |
| ), | |
| ( | |
| "timeout", | |
| ResponseTemplate::new(/*s*/ 200).set_delay(Duration::from_secs(/*secs*/ 6)), | |
| ), | |
| ( | |
| "oversized", | |
| ResponseTemplate::new(/*s*/ 200) | |
| .set_body_bytes(vec![b' '; MAX_VERSION_RESPONSE_BYTES + 1]), | |
| ), | |
| ] { | |
| Mock::given(method("GET")) | |
| .and(path(format!("/{endpoint}"))) | |
| .respond_with(response) | |
| .mount(&server) | |
| .await; | |
| let result = http_get_json::<serde_json::Value>( | |
| &client, | |
| &format!("{}/{endpoint}", server.uri()), | |
| ) | |
| .await; | |
| if matches!(endpoint, "valid" | "redirect") { | |
| assert_eq!(result, Ok(serde_json::json!({"version": "1.2.3"}))); | |
| } else if endpoint == "timeout" { | |
| assert_eq!(result, Err("version request timed out".to_string())); | |
| } else if endpoint == "proxy_auth_required" { | |
| assert_eq!( | |
| result, | |
| Err("HTTP 407 Proxy Authentication Required".to_string()) | |
| ); | |
| } else { | |
| assert!(result.is_err(), "{endpoint} must not be accepted"); | |
| } | |
| } | |
| } | |
| fn macos_update_probe_uses_the_persisted_production_appcast_feed() { | |
| let home = tempfile::tempdir().expect("temporary home should be created"); | |
| let application = InstalledApp { | |
| identity: "com.openai.codex", | |
| version: "26.623.10000".to_string(), | |
| bundle: PathBuf::new(), | |
| build: 6139, | |
| }; | |
| assert_eq!( | |
| macos_desktop_update_url(home.path(), &application, "26.6.0"), | |
| DESKTOP_UPDATE_URL | |
| ); | |
| let state_directory = home | |
| .path() | |
| .join("Library/Application Support/com.openai.codex"); | |
| std::fs::create_dir_all(&state_directory) | |
| .expect("production appcast state directory should be created"); | |
| std::fs::write( | |
| state_directory.join("production-appcast-bootstrap.json"), | |
| r#"{"backendAppcastEnabled":true,"installationId":"028e90f8-5f2a-47db-a05c-6a48f548d728"}"#, | |
| ) | |
| .expect("production appcast state should be created"); | |
| let arch = if cfg!(target_arch = "x86_64") { | |
| "x64" | |
| } else { | |
| "arm64" | |
| }; | |
| assert_eq!( | |
| macos_desktop_update_url(home.path(), &application, "26.6.0"), | |
| format!( | |
| "{BACKEND_DESKTOP_UPDATE_URL}?installation_id=028e90f8-5f2a-47db-a05c-6a48f548d728&arch={arch}&app_version=26.623.10000&beta=false&os-version=26.6.0&plan_type=unknown" | |
| ) | |
| ); | |
| } | |
| async fn macos_staged_updates_require_a_newer_matching_extracted_bundle() { | |
| let root = tempfile::tempdir().expect("temporary Sparkle cache should be created"); | |
| for index in 0..320 { | |
| std::fs::create_dir(root.path().join(format!("unrelated-{index}"))) | |
| .expect("unrelated Sparkle cache directory should be created"); | |
| } | |
| for (name, identity, build) in [ | |
| ("newest", "com.openai.codex", "6268"), | |
| ("newer", "com.openai.codex", "6168"), | |
| ("older", "com.openai.codex", "6138"), | |
| ("different", "com.example.other", "9999"), | |
| ("invalid", "com.openai.codex", "invalid"), | |
| ] { | |
| let bundle = root.path().join(name).join("extracted/ChatGPT.app"); | |
| write_macos_bundle(&bundle, identity, build); | |
| } | |
| let outside = tempfile::tempdir().expect("external fixture should be created"); | |
| let linked = outside.path().join("ChatGPT.app"); | |
| write_macos_bundle(&linked, "com.openai.codex", "9999"); | |
| std::os::unix::fs::symlink(&linked, root.path().join("ChatGPT.app")) | |
| .expect("symlinked staged app fixture should be created"); | |
| assert_eq!( | |
| latest_macos_staged_build(root.path(), /*installed_build*/ 6139).await, | |
| Some(6268) | |
| ); | |
| assert_eq!( | |
| latest_macos_staged_build(root.path(), /*installed_build*/ 6268).await, | |
| None | |
| ); | |
| } | |
| fn windows_store_updates_compare_all_four_production_build_components() { | |
| let mut manifest = serde_json::json!({ | |
| "schemaVersion": 1, | |
| "buildVersion": "26.803.5235.1", | |
| "storeProductId": "9PLM9XGG6VKS", | |
| "packageIdentity": "OpenAI.Codex", | |
| }); | |
| assert_eq!( | |
| windows_store_update(&serde_json::to_vec(&manifest).unwrap(), "26.803.5235.0"), | |
| Ok(Some("26.803.5235.1".to_string())) | |
| ); | |
| assert_eq!( | |
| windows_store_update(&serde_json::to_vec(&manifest).unwrap(), "26.803.5235.1"), | |
| Ok(None) | |
| ); | |
| manifest["storeProductId"] = "other".into(); | |
| assert!( | |
| windows_store_update(&serde_json::to_vec(&manifest).unwrap(), "26.803.5235.0").is_err() | |
| ); | |
| } | |
| fn write_macos_bundle(path: &Path, identity: &str, build: &str) { | |
| let contents = path.join("Contents"); | |
| std::fs::create_dir_all(&contents).expect("staged app fixture should be created"); | |
| std::fs::write( | |
| contents.join("Info.plist"), | |
| format!( | |
| "<?xml version=\"1.0\"?><plist version=\"1.0\"><dict>\ | |
| <key>CFBundleIdentifier</key><string>{identity}</string>\ | |
| <key>CFBundleVersion</key><string>{build}</string>\ | |
| </dict></plist>" | |
| ), | |
| ) | |
| .expect("staged app metadata should be created"); | |
| } | |
| fn is_newer_compares_plain_semver() { | |
| assert_eq!(is_newer("1.2.4", "1.2.3"), Some(true)); | |
| assert_eq!(is_newer("1.2.3", "1.2.4"), Some(false)); | |
| assert_eq!(is_newer("1.2.3-beta.1", "1.2.2"), None); | |
| } | |
| fn update_action_labels_install_contexts() { | |
| assert_eq!( | |
| update_action_label(&InstallContext { | |
| method: InstallMethod::Npm, | |
| package_layout: None, | |
| }), | |
| "npm install -g @openai/codex" | |
| ); | |
| assert_eq!( | |
| update_action_label(&InstallContext { | |
| method: InstallMethod::Pnpm, | |
| package_layout: None, | |
| }), | |
| "pnpm add -g @openai/codex" | |
| ); | |
| assert_eq!( | |
| update_action_label(&InstallContext { | |
| method: InstallMethod::Other, | |
| package_layout: None, | |
| }), | |
| "manual or unknown" | |
| ); | |
| } | |
| } | |