Add desktop update diagnostics to codex doctor (#39074)

## What changed

- Probe the installed desktop app's update endpoint on macOS and Windows and report update-CDN reachability alongside the existing network diagnostics.
- Report newer Windows Store builds and macOS updates that Sparkle has staged for installation in the updates check and human-readable notes.
- Validate Windows update manifests against the production app identity, and use the persisted production appcast configuration when selecting the macOS feed.

## Testing

- Cover Windows version comparison and manifest identity validation.
- Cover macOS appcast selection and staged-bundle discovery.
- Verify the human-readable desktop update note.

GitOrigin-RevId: 1af5aa750144346b4b31f2b27a20371daf40d3c0
This commit is contained in:
chess
2026-08-17 20:11:23 +00:00
committed by copyberry
parent 45cf6cbc19
commit d65d315939
8 changed files with 535 additions and 63 deletions

View File

@@ -543,6 +543,11 @@ async fn build_report(
progress.begin("desktop");
if let Some(desktop) = desktop::collect().await {
#[cfg(any(target_os = "macos", target_os = "windows"))]
if let Some(application) = desktop.application.as_ref() {
updates::append_desktop_update(&mut checks, config_result.as_ref().ok(), application)
.await;
}
progress.finish("desktop", overall_status(&desktop.checks));
checks.extend(desktop.checks);
}

View File

@@ -16,7 +16,7 @@ use super::DoctorCheck;
#[cfg(target_os = "macos")]
mod macos_security;
mod platform;
pub(super) mod platform;
#[cfg(any(target_os = "windows", test))]
mod windows_security;
@@ -28,6 +28,8 @@ const HANDSHAKE_CHECK_ID: &str = "desktop.app_server.handshake";
pub(super) struct DesktopDiagnostics {
pub(super) checks: Vec<DoctorCheck>,
#[cfg(any(target_os = "macos", target_os = "windows"))]
pub(super) application: Option<platform::InstalledApp>,
}
struct DesktopLog {
@@ -67,6 +69,8 @@ pub(super) async fn collect() -> Option<DesktopDiagnostics> {
#[cfg(target_os = "windows")]
windows_security::collect().await,
],
#[cfg(any(target_os = "macos", target_os = "windows"))]
application: None,
});
}
};
@@ -100,6 +104,8 @@ pub(super) async fn collect() -> Option<DesktopDiagnostics> {
#[cfg(target_os = "windows")]
windows_security::collect().await,
],
#[cfg(any(target_os = "macos", target_os = "windows"))]
application: Some(application),
})
}

View File

@@ -13,6 +13,8 @@ use std::os::windows::io::FromRawHandle;
#[cfg(target_os = "windows")]
use std::os::windows::io::OwnedHandle;
#[cfg(target_os = "macos")]
use std::path::Path;
#[cfg(target_os = "macos")]
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use std::process::Stdio;
@@ -43,20 +45,19 @@ use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION;
use super::super::CheckStatus;
use super::super::DoctorCheck;
pub(super) struct InstalledApp {
pub(super) identity: &'static str,
pub(super) version: String,
pub(in crate::doctor) struct InstalledApp {
pub(in crate::doctor) identity: &'static str,
pub(in crate::doctor) version: String,
#[cfg(target_os = "windows")]
package_family: &'static str,
#[cfg(target_os = "macos")]
pub(super) bundle: PathBuf,
pub(in crate::doctor) bundle: PathBuf,
#[cfg(target_os = "macos")]
#[allow(dead_code, reason = "used by downstream desktop diagnostics")]
pub(super) build: u64,
pub(in crate::doctor) build: u64,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct DiscoveryError;
pub(in crate::doctor) struct DiscoveryError;
pub(super) async fn installed_app() -> Result<Option<InstalledApp>, DiscoveryError> {
#[cfg(target_os = "windows")]
@@ -232,42 +233,51 @@ async fn installed_macos_app() -> Result<Option<InstalledApp>, DiscoveryError> {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return Err(DiscoveryError),
}
let mut command = Command::new("/usr/bin/plutil");
command
.args(["-convert", "json", "-o", "-"])
.arg(bundle.join("Contents/Info.plist"))
.stdin(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true);
let output = timeout(Duration::from_secs(5), command.output())
.await
.map_err(|_| DiscoveryError)?
.map_err(|_| DiscoveryError)?;
if !output.status.success() || output.stdout.len() > 64 * 1024 {
return Err(DiscoveryError);
if let Some(application) = inspect_macos_bundle(&bundle).await? {
return Ok(Some(application));
}
let metadata: Value = serde_json::from_slice(&output.stdout).map_err(|_| DiscoveryError)?;
if metadata.get("CFBundleIdentifier").and_then(Value::as_str) != Some("com.openai.codex") {
continue;
}
let version = metadata
.get("CFBundleShortVersionString")
.or_else(|| metadata.get("CFBundleVersion"))
.and_then(Value::as_str)
.ok_or(DiscoveryError)?;
let build = metadata
.get("CFBundleVersion")
.and_then(Value::as_str)
.and_then(|value| value.parse().ok())
.ok_or(DiscoveryError)?;
return Ok(Some(InstalledApp {
identity: "com.openai.codex",
version: version.to_string(),
bundle,
build,
}));
}
Ok(None)
}
#[cfg(target_os = "macos")]
pub(in crate::doctor) async fn inspect_macos_bundle(
bundle: &Path,
) -> Result<Option<InstalledApp>, DiscoveryError> {
let mut command = Command::new("/usr/bin/plutil");
command
.args(["-convert", "json", "-o", "-"])
.arg(bundle.join("Contents/Info.plist"))
.stdin(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(true);
let output = timeout(Duration::from_secs(5), command.output())
.await
.map_err(|_| DiscoveryError)?
.map_err(|_| DiscoveryError)?;
if !output.status.success() || output.stdout.len() > 64 * 1024 {
return Err(DiscoveryError);
}
let metadata: Value = serde_json::from_slice(&output.stdout).map_err(|_| DiscoveryError)?;
if metadata.get("CFBundleIdentifier").and_then(Value::as_str) != Some("com.openai.codex") {
return Ok(None);
}
let version = metadata
.get("CFBundleShortVersionString")
.or_else(|| metadata.get("CFBundleVersion"))
.and_then(Value::as_str)
.ok_or(DiscoveryError)?;
let build = metadata
.get("CFBundleVersion")
.and_then(Value::as_str)
.and_then(|value| value.parse().ok())
.ok_or(DiscoveryError)?;
Ok(Some(InstalledApp {
identity: "com.openai.codex",
version: version.to_string(),
bundle: bundle.to_path_buf(),
build,
}))
}

View File

@@ -138,23 +138,7 @@ pub(super) async fn probe_status(
.send()
.await
}
.map_err(|error| {
match error.failure_class() {
Some(RouteFailureClass::TlsError) => "TLS handshake or certificate validation failed",
Some(RouteFailureClass::ProxyAuthenticationRequired) => "proxy authentication required",
Some(RouteFailureClass::InvalidProxyConfig) => "invalid proxy configuration",
Some(RouteFailureClass::ProxyResolutionUnavailable) => {
"system proxy configuration unavailable"
}
Some(RouteFailureClass::ConnectTimeout) => "request timed out",
Some(RouteFailureClass::UnsupportedProxyScheme) => "unsupported proxy configuration",
Some(RouteFailureClass::ResolverError) => "proxy resolution failed",
None if error.is_timeout() => "request timed out",
None if error.is_connect() => "connect failed",
None => "request failed",
}
.to_string()
})?;
.map_err(request_error)?;
let status = response.status().as_u16();
if status == 407 {
return Err("proxy authentication required (HTTP 407)".to_string());
@@ -162,6 +146,24 @@ pub(super) async fn probe_status(
Ok(status)
}
pub(super) fn request_error(error: RouteAwareRequestError) -> String {
match error.failure_class() {
Some(RouteFailureClass::TlsError) => "TLS handshake or certificate validation failed",
Some(RouteFailureClass::ProxyAuthenticationRequired) => "proxy authentication required",
Some(RouteFailureClass::InvalidProxyConfig) => "invalid proxy configuration",
Some(RouteFailureClass::ProxyResolutionUnavailable) => {
"system proxy configuration unavailable"
}
Some(RouteFailureClass::ConnectTimeout) => "request timed out",
Some(RouteFailureClass::UnsupportedProxyScheme) => "unsupported proxy configuration",
Some(RouteFailureClass::ResolverError) => "proxy resolution failed",
None if error.is_timeout() => "request timed out",
None if error.is_connect() => "connect failed",
None => "request failed",
}
.to_string()
}
#[cfg(test)]
#[path = "network_tests.rs"]
mod tests;

View File

@@ -499,6 +499,9 @@ fn notes_for_report(report: &DoctorReport) -> Vec<DoctorNote> {
update_note(check, report)
.into_iter()
.for_each(|note| notes.push(note));
desktop_update_note(check)
.into_iter()
.for_each(|note| notes.push(note));
}
if let Some(check) = find_check(report, "state") {
rollout_note(check)
@@ -548,6 +551,21 @@ fn update_note(check: &DoctorCheck, report: &DoctorReport) -> Option<DoctorNote>
})
}
fn desktop_update_note(check: &DoctorCheck) -> Option<DoctorNote> {
let status = detail::detail_value(check, "desktop update status")?;
let build = detail::detail_value(check, "desktop latest build")?;
let summary = match status.as_str() {
"ready to install" => format!("build {build} available (ready to install)"),
"available" => format!("build {build} available"),
_ => return None,
};
Some(DoctorNote {
status: DisplayStatus::Update,
name: "desktop".to_string(),
summary,
})
}
fn rollout_note(check: &DoctorCheck) -> Option<DoctorNote> {
let active = detail::detail_value(check, "active rollout files")?;
let (files, bytes) = detail::rollout_files_and_bytes(&active)?;
@@ -1350,6 +1368,21 @@ Background Server
"the desktop app-server initialized successfully",
),
]);
let update = report
.checks
.iter_mut()
.find(|check| check.category == "updates")
.unwrap();
for (status, expected) in [
("available", "build 123 available"),
("ready to install", "build 123 available (ready to install)"),
] {
update.details = vec![
format!("desktop update status: {status}"),
"desktop latest build: 123".to_string(),
];
assert_eq!(desktop_update_note(update).unwrap().summary, expected);
}
insta::assert_snapshot!(
"doctor_human_report_environment_rows",
render_human_report(&report, detailed_no_color_unicode_options())

View File

@@ -5,6 +5,7 @@ expression: "render_human_report(&report, detailed_no_color_unicode_options())"
Codex Doctor v0.0.0
Notes
↑ desktop build 123 available (ready to install)
⚠ terminal narrow terminal
✗ auth token expired - Run `codex login`.
⚠ git this worktree is not on a Windows Dev Drive - create a trusted Windows Dev Drive: https://learn.microsoft.com/en-us/windows/dev-drive/
@@ -56,6 +57,8 @@ Desktop App
Updates
✓ updates update configuration is locally consistent
desktop update status ready to install
desktop latest build 123
Connectivity
✓ network network environment readable
@@ -66,7 +69,7 @@ Background Server
✓ app-server background server is not running
─────────────────────────────────────────────────────────────
15 ok · 4 notes · 3 warn · 1 fail failed
15 ok · 5 notes · 3 warn · 1 fail failed
--summary compact output --all expand truncated lists
--json redacted report

View File

@@ -7,23 +7,50 @@
//! an update command.
use std::path::Path;
#[cfg(target_os = "macos")]
use std::path::PathBuf;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use std::time::Duration;
use codex_core::config::Config;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use codex_http_client::ClientRouteClass;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use codex_http_client::RouteAwareClientPool;
use codex_install_context::InstallContext;
use codex_install_context::InstallMethod;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use http::Method;
use serde::Deserialize;
#[cfg(target_os = "macos")]
use url::Url;
use super::CheckStatus;
use super::DoctorCheck;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use super::DoctorIssue;
use super::NpmRootCheck;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use super::desktop::platform::InstalledApp;
use super::doctor_install_context;
use super::doctor_managed_by_npm;
#[cfg(any(target_os = "macos", target_os = "windows"))]
use super::network;
use super::npm_global_root_check;
use super::run_command;
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";
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
const DESKTOP_UPDATE_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/appcast-x64.xml";
#[cfg(all(target_os = "macos", not(target_arch = "x86_64")))]
const DESKTOP_UPDATE_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/appcast.xml";
#[cfg(target_os = "macos")]
const BACKEND_DESKTOP_UPDATE_URL: &str = "https://chatgpt.com/backend-api/wham/app/appcast";
#[cfg(target_os = "windows")]
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.
///
@@ -107,6 +134,278 @@ pub(super) fn updates_check(config: &Config) -> DoctorCheck {
check
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
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);
#[cfg(target_os = "macos")]
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;
};
#[cfg(target_os = "macos")]
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());
#[cfg(target_os = "windows")]
let desktop_update_url = DESKTOP_UPDATE_URL;
#[cfg(target_os = "macos")]
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();
#[cfg(target_os = "windows")]
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})"
));
#[cfg(target_os = "windows")]
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"),
);
}
}
#[cfg(target_os = "macos")]
fn macos_desktop_update_url(home: &Path, application: &InstalledApp, os_version: &str) -> String {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProductionAppcastState {
#[serde(default)]
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()
}
#[cfg(any(target_os = "windows", test))]
fn windows_store_update(
manifest: &[u8],
installed_version: &str,
) -> Result<Option<String>, &'static str> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
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))
}
#[cfg(target_os = "macos")]
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) {
@@ -209,6 +508,117 @@ struct VersionInfo {
mod tests {
use super::*;
#[cfg(target_os = "macos")]
#[test]
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"
)
);
}
#[cfg(target_os = "macos")]
#[tokio::test]
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
);
}
#[test]
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()
);
}
#[cfg(target_os = "macos")]
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");
}
#[test]
fn is_newer_compares_plain_semver() {
assert_eq!(is_newer("1.2.4", "1.2.3"), Some(true));

View File

@@ -5,8 +5,10 @@ use std::process::Stdio;
use anyhow::Context as _;
use anyhow::Result;
#[cfg(target_os = "macos")]
use pretty_assertions::assert_eq;
use serde_json::Value;
#[cfg(target_os = "macos")]
use serde_json::json;
use tempfile::TempDir;
use wiremock::Mock;
@@ -63,9 +65,10 @@ async fn invalid_custom_ca_falls_back_to_system_roots() -> Result<()> {
.context("failed to run the doctor with an invalid custom CA")?;
let report: Value = serde_json::from_slice(&output.stdout)?;
assert_eq!(
report["checks"]["network.provider_reachability"]["status"],
json!("ok")
assert!(
report["checks"]["network.provider_reachability"]["details"]["local API inference URL"]
.as_str()
.is_some_and(|detail| detail.ends_with("reachable (HTTP 200)"))
);
}
server.verify().await;