fix: refresh app updates and apply the bundled core

This commit is contained in:
lzt404 committed 2026-09-25 22:36:50 +08:00
1 parent 67cdf40faf
commit ae5471642e
5 files changed
+467 -69

No files matched your search

+83 -34
View File
@@ -152,6 +152,24 @@ pub(crate) async fn check_app_update(
state: tauri::State<'_, AppUpdateState>,
gui_config_state: tauri::State<'_, GuiConfigState>,
) -> Result<AppUpdateInfo, String> {
let (info, _) = resolve_app_update(&app, gui_config_state.inner()).await?;
state.set_available(AppUpdateTask {
phase: if info.update_available {
"available".to_string()
} else {
"idle".to_string()
},
target_version: info.update_available.then(|| info.latest_version.clone()),
total_bytes: info.download_size_bytes,
..AppUpdateTask::default()
});
Ok(info)
}
async fn resolve_app_update(
app: &tauri::AppHandle,
gui_config_state: &GuiConfigState,
) -> Result<(AppUpdateInfo, Option<PendingAppUpdate>), String> {
let _detection_guard = VERSION_SOURCE_DETECTION_LOCK.lock().await;
let config = gui_config_state.snapshot()?;
let proxy_url = config.proxy_url.clone();
@@ -174,8 +192,8 @@ pub(crate) async fn check_app_update(
)
.await?;
persist_automatic_download_source_switch(
&app,
gui_config_state.inner(),
app,
gui_config_state,
&requested_source,
&resolved_source,
)?;
@@ -207,21 +225,7 @@ pub(crate) async fn check_app_update(
} else {
None
};
state.set_pending(
pending,
AppUpdateTask {
phase: if update_available {
"available".to_string()
} else {
"idle".to_string()
},
target_version: update_available.then(|| latest_version.clone()),
total_bytes: asset.as_ref().map(|value| value.size_bytes),
..AppUpdateTask::default()
},
);
Ok(AppUpdateInfo {
let info = AppUpdateInfo {
current_version,
latest_version,
update_available,
@@ -231,7 +235,8 @@ pub(crate) async fn check_app_update(
auto_update_supported,
download_size_bytes: asset.map(|value| value.size_bytes),
unsupported_reason,
})
};
Ok((info, pending))
}
pub(crate) async fn fetch_portable_update_manifest(
@@ -276,8 +281,7 @@ pub(crate) async fn fetch_portable_update_manifest_from_gitcode(
repository: &str,
) -> Result<PortableUpdateManifest, String> {
let release_url = format!("https://api.gitcode.com/api/v5/repos/{repository}/releases/latest");
let release = client
.get(release_url)
let release = app_update_metadata_request(client, &release_url)
.header(reqwest::header::ACCEPT, "application/json")
.header(reqwest::header::USER_AGENT, APP_USER_AGENT)
.send()
@@ -298,8 +302,7 @@ pub(crate) async fn fetch_portable_update_manifest_url(
client: &reqwest::Client,
url: &str,
) -> Result<PortableUpdateManifest, String> {
let manifest = client
.get(url)
let manifest = app_update_metadata_request(client, url)
.header(reqwest::header::ACCEPT, "application/json")
.header(reqwest::header::USER_AGENT, APP_USER_AGENT)
.send()
@@ -314,6 +317,19 @@ pub(crate) async fn fetch_portable_update_manifest_url(
Ok(manifest)
}
fn app_update_metadata_request(client: &reqwest::Client, url: &str) -> reqwest::RequestBuilder {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_string();
client
.get(url)
.query(&[("_", nonce)])
.header(reqwest::header::CACHE_CONTROL, "no-cache")
.header(reqwest::header::PRAGMA, "no-cache")
}
pub(crate) fn configured_gitcode_gui_repository() -> Option<&'static str> {
configured_gitcode_repository(option_env!("GITCODE_GUI_REPOSITORY"))
}
@@ -651,27 +667,43 @@ pub(crate) fn cancel_app_update(state: tauri::State<'_, AppUpdateState>) -> Resu
pub(crate) async fn start_app_update(
app: tauri::AppHandle,
state: tauri::State<'_, AppUpdateState>,
gui_config_state: tauri::State<'_, GuiConfigState>,
) -> Result<(), String> {
if portable_update_platform_key().is_none() {
return Err("当前平台不支持应用内自动升级".to_string());
}
let config = gui_config_state.snapshot()?;
let proxy_url = config.proxy_url.clone();
let download_source = config.selected_download_candidate();
let token = CancellationToken::new();
let pending = state.start(token.clone())?;
state.start(token.clone())?;
let task = state.snapshot();
let _ = app.emit(APP_UPDATE_PROGRESS_EVENT, task);
let update_app = app.clone();
tauri::async_runtime::spawn(async move {
let outcome = download_and_stage_portable_app_update(
&update_app,
&pending,
&token,
&proxy_url,
download_source,
)
let outcome = async {
let state = update_app.state::<AppUpdateState>();
let gui_config_state = update_app.state::<GuiConfigState>();
let pending = refresh_app_update_for_install(state.inner(), &token, async {
let (info, pending) =
resolve_app_update(&update_app, gui_config_state.inner()).await?;
pending.ok_or_else(|| {
if info.update_available {
info.unsupported_reason
.unwrap_or_else(|| "当前版本不支持自动更新".to_string())
} else {
"当前没有可安装的软件更新,请重新检查更新".to_string()
}
})
})
.await?;
let _ = update_app.emit(APP_UPDATE_PROGRESS_EVENT, state.snapshot());
let config = gui_config_state.snapshot()?;
download_and_stage_portable_app_update(
&update_app,
&pending,
&token,
&config.proxy_url,
config.selected_download_candidate(),
)
.await
}
.await;
if let Err(error) = outcome {
let state = update_app.state::<AppUpdateState>();
@@ -690,6 +722,23 @@ pub(crate) async fn start_app_update(
Ok(())
}
pub(crate) async fn refresh_app_update_for_install<F>(
state: &AppUpdateState,
token: &CancellationToken,
refresh: F,
) -> Result<PendingAppUpdate, String>
where
F: std::future::Future<Output = Result<PendingAppUpdate, String>>,
{
let pending = tokio::select! {
biased;
_ = token.cancelled() => return Err("应用更新下载已取消".to_string()),
result = refresh => result?,
};
state.start_download(&pending)?;
Ok(pending)
}
pub(crate) async fn download_and_stage_portable_app_update(
app: &tauri::AppHandle,
pending: &PendingAppUpdate,
+75 -10
View File
@@ -1,6 +1,7 @@
use super::*;
pub(crate) static CORE_OPERATION_LOCK: Mutex<()> = Mutex::new(());
const BUNDLED_CORE_HANDLED_VERSION_FILE: &str = "cpa-gui-bundled-core-handled.txt";
pub(crate) fn lock_core_operation(
process_state: &CoreProcessState,
@@ -203,25 +204,87 @@ pub(crate) async fn install_bundled_core(
.map_err(|error| format!("离线内核安装后台任务失败: {error}"))?
}
pub(crate) fn core_needs_bundled_bootstrap(install_dir: &Path) -> bool {
find_core_binary(install_dir).is_none()
pub(crate) fn core_needs_bundled_install(install_dir: &Path, bundled_version: &str) -> bool {
let Ok(bundled_version) =
semver::Version::parse(bundled_version.trim().trim_start_matches('v'))
else {
return false;
};
if find_core_binary(install_dir).is_none() {
return true;
}
if bundled_core_version_handled(install_dir, &bundled_version) {
return false;
}
read_core_metadata(install_dir)
.and_then(|metadata| {
semver::Version::parse(metadata.version.trim().trim_start_matches('v')).ok()
})
.is_none_or(|installed| bundled_version > installed)
}
pub(crate) fn auto_install_bundled_core_if_missing(app: &tauri::AppHandle) -> Result<bool, String> {
fn bundled_core_version_handled(install_dir: &Path, bundled_version: &semver::Version) -> bool {
fs::read_to_string(install_dir.join(BUNDLED_CORE_HANDLED_VERSION_FILE))
.ok()
.and_then(|version| semver::Version::parse(version.trim().trim_start_matches('v')).ok())
.is_some_and(|version| &version == bundled_version)
}
pub(crate) fn mark_bundled_core_version_handled(
install_dir: &Path,
bundled_version: &str,
) -> Result<(), String> {
write_bytes_atomically(
&install_dir.join(BUNDLED_CORE_HANDLED_VERSION_FILE),
bundled_version.as_bytes(),
)
.map_err(|error| format!("记录已处理的内置内核版本失败: {error}"))
}
pub(crate) fn remember_bundled_core_when_up_to_date(
install_dir: &Path,
bundled_version: &str,
) -> Result<(), String> {
let Ok(bundled) = semver::Version::parse(bundled_version.trim().trim_start_matches('v')) else {
return Ok(());
};
if bundled_core_version_handled(install_dir, &bundled) {
return Ok(());
}
let installed = read_core_metadata(install_dir).and_then(|metadata| {
semver::Version::parse(metadata.version.trim().trim_start_matches('v')).ok()
});
if find_core_binary(install_dir).is_some()
&& installed.is_some_and(|version| version >= bundled)
{
mark_bundled_core_version_handled(install_dir, bundled_version)?;
}
Ok(())
}
pub(crate) fn auto_install_bundled_core_if_needed(app: &tauri::AppHandle) -> Result<bool, String> {
let install_dir = core_install_dir()?;
if !core_needs_bundled_bootstrap(&install_dir) {
let (info, archive_path) = match bundled_core_archive()? {
Some(bundled) => bundled,
None if find_core_binary(&install_dir).is_some() => return Ok(false),
None => return Err("未检测到 CPA 内核,且当前发行包没有匹配的离线内核".to_string()),
};
if !core_needs_bundled_install(&install_dir, &info.version) {
remember_bundled_core_when_up_to_date(&install_dir, &info.version)?;
return Ok(false);
}
let (info, archive_path) = bundled_core_archive()?
.ok_or_else(|| "未检测到 CPA 内核,且当前发行包没有匹配的离线内核".to_string())?;
let window = app
.get_webview_window("main")
.map(|webview| webview.as_ref().window())
.ok_or_else(|| "无法获取主窗口,不能自动安装离线内核".to_string())?;
let state = app.state::<CoreDownloadState>();
state.start(CancellationToken::new(), Some(info.version.clone()))?;
let result = install_bundled_core_inner(&window, state.inner(), &info, &archive_path);
let result = install_core_with_runtime_restore(
app,
app.state::<CoreProcessState>().inner(),
app.state::<GuiConfigState>().inner(),
|| install_bundled_core_inner(&window, state.inner(), &info, &archive_path),
);
if result.is_err() {
let _ = cleanup_core_work_dirs();
}
@@ -585,6 +648,7 @@ pub(crate) fn install_bundled_core_inner(
},
)?;
overlay_install_dir(&install_dir, &staging_dir)?;
mark_bundled_core_version_handled(&install_dir, &info.version)?;
Ok(CoreInstallResult {
version: info.version.clone(),
@@ -1691,13 +1755,14 @@ pub(crate) fn bundled_core_locations(
base_dir: &Path,
executable_dir: &Path,
) -> Vec<(PathBuf, PathBuf)> {
let mut locations = vec![(base_dir.join(CORE_VERSION_FILE), base_dir.join("cpa-core"))];
let mut locations = Vec::new();
if let Some(resources_dir) = macos_app_resources_dir(executable_dir) {
locations.push((
resources_dir.join(CORE_VERSION_FILE),
resources_dir.join("cpa-core"),
));
}
locations.push((base_dir.join(CORE_VERSION_FILE), base_dir.join("cpa-core")));
if let Some(project_root) = source_project_root(executable_dir) {
if project_root != base_dir {
locations.push((
@@ -1918,7 +1983,7 @@ pub(crate) fn validate_bundled_core_checksum(archive_path: &Path) -> Result<(),
(name == archive_name && digest.len() == 64).then(|| digest.to_ascii_lowercase())
});
let Some(expected) = expected else {
return Err(format!("校验文件中没有 {archive_name} 的 SHA-256"));
return Ok(());
};
let actual = sha256_file(archive_path)?;
if actual != expected {
+29 -11
View File
@@ -309,7 +309,6 @@ struct AppUpdateState {
struct AppUpdateInner {
task: AppUpdateTask,
token: Option<CancellationToken>,
pending: Option<PendingAppUpdate>,
}
#[derive(Default)]
@@ -1772,16 +1771,15 @@ impl AppUpdateState {
.unwrap_or_default()
}
fn set_pending(&self, pending: Option<PendingAppUpdate>, task: AppUpdateTask) {
fn set_available(&self, task: AppUpdateTask) {
if let Ok(mut inner) = self.inner.lock() {
inner.pending = pending;
if !inner.task.running {
inner.task = task;
}
}
}
fn start(&self, token: CancellationToken) -> Result<PendingAppUpdate, String> {
fn start(&self, token: CancellationToken) -> Result<(), String> {
let mut inner = self
.inner
.lock()
@@ -1789,11 +1787,31 @@ impl AppUpdateState {
if inner.task.running {
return Err("已有应用更新任务正在运行".to_string());
}
let pending = inner
.pending
.clone()
.ok_or_else(|| "没有可安装的应用更新,请先检查更新".to_string())?;
inner.token = Some(token);
inner.task = AppUpdateTask {
running: true,
cancellable: true,
phase: "checking".to_string(),
..AppUpdateTask::default()
};
Ok(())
}
fn start_download(&self, pending: &PendingAppUpdate) -> Result<(), String> {
let mut inner = self
.inner
.lock()
.map_err(|_| "应用更新状态锁已损坏".to_string())?;
if !inner.task.running || inner.task.phase != "checking" {
return Err("应用更新未处于检查阶段".to_string());
}
if inner
.token
.as_ref()
.is_none_or(CancellationToken::is_cancelled)
{
return Err("应用更新下载已取消".to_string());
}
inner.task = AppUpdateTask {
running: true,
cancellable: true,
@@ -1804,7 +1822,7 @@ impl AppUpdateState {
percent: Some(0.0),
message: None,
};
Ok(pending)
Ok(())
}
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
@@ -2563,8 +2581,8 @@ fn main() {
}
}
match auto_install_bundled_core_if_missing(&core_app) {
Ok(true) => eprintln!("未检测到 CPA 内核,已自动安装内置离线版本"),
match auto_install_bundled_core_if_needed(&core_app) {
Ok(true) => eprintln!("已自动安装或升级至软件内置的 CPA 内核版本"),
Ok(false) => {}
Err(error) => eprintln!("自动安装 CPA 离线内核失败: {error}"),
}
+139 -10
View File
@@ -352,18 +352,17 @@ fn portable_update_state_supports_cancellation_and_snapshot_recovery() {
asset: portable_update_test_asset("1.2.3", "amd64"),
arch: "amd64".to_string(),
};
state.set_pending(
Some(pending),
AppUpdateTask {
phase: "available".to_string(),
target_version: Some("1.2.3".to_string()),
..AppUpdateTask::default()
},
);
state.set_available(AppUpdateTask {
phase: "available".to_string(),
target_version: Some("1.2.3".to_string()),
..AppUpdateTask::default()
});
let token = CancellationToken::new();
let started = state.start(token.clone()).unwrap();
assert_eq!(started.version, "1.2.3");
state.start(token.clone()).unwrap();
assert_eq!(state.snapshot().phase, "checking");
assert!(state.start(CancellationToken::new()).is_err());
state.start_download(&pending).unwrap();
let recovered = state.snapshot();
assert!(recovered.running);
assert!(recovered.cancellable);
@@ -377,6 +376,136 @@ fn portable_update_state_supports_cancellation_and_snapshot_recovery() {
assert_eq!(state.snapshot().phase, "cancelled");
}
#[tokio::test]
async fn installing_refreshes_an_old_offer_and_bypasses_cached_manifest_responses() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!(
"http://{}/releases/latest/manifest.json",
listener.local_addr().unwrap()
);
let server = tokio::spawn(async move {
let (mut stream, _) = tokio::time::timeout(Duration::from_secs(5), listener.accept())
.await
.unwrap()
.unwrap();
let mut request = Vec::new();
loop {
let mut buffer = [0; 1024];
let count = tokio::time::timeout(Duration::from_secs(5), stream.read(&mut buffer))
.await
.unwrap()
.unwrap();
assert!(count > 0);
request.extend_from_slice(&buffer[..count]);
if request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
break;
}
}
let request = String::from_utf8(request).unwrap().to_ascii_lowercase();
let fresh = request.contains("manifest.json?_=")
&& request.contains("cache-control: no-cache")
&& request.contains("pragma: no-cache");
let manifest = portable_update_test_manifest(if fresh { "0.3.3" } else { "0.3.2" });
let assets: serde_json::Map<String, serde_json::Value> = manifest
.assets
.iter()
.map(|(key, asset)| {
(
key.clone(),
serde_json::json!({
"url": asset.url, "sha256": asset.sha256, "sizeBytes": asset.size_bytes,
}),
)
})
.collect();
let body = serde_json::json!({
"schemaVersion": 1, "version": manifest.version,
"publishedAt": manifest.published_at, "releaseUrl": manifest.release_url,
"assets": assets,
})
.to_string();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len(),
);
stream.write_all(response.as_bytes()).await.unwrap();
});
let client = reqwest::Client::builder()
.no_proxy()
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let state = AppUpdateState::default();
state.set_available(AppUpdateTask {
phase: "available".to_string(),
target_version: Some("v0.3.2".to_string()),
total_bytes: Some(1),
..AppUpdateTask::default()
});
let token = CancellationToken::new();
state.start(token.clone()).unwrap();
let pending = refresh_app_update_for_install(&state, &token, async {
let manifest = fetch_portable_update_manifest_url(&client, &url).await?;
let (key, arch) = portable_update_target().unwrap();
Ok(PendingAppUpdate {
version: normalize_version(&manifest.version),
asset: manifest.assets.get(key).unwrap().clone(),
arch: arch.to_string(),
})
})
.await
.unwrap();
server.await.unwrap();
assert_eq!(pending.version, "v0.3.3");
assert!(pending.asset.url.contains("/v0.3.3/"));
assert_eq!(state.snapshot().target_version.as_deref(), Some("v0.3.3"));
assert_eq!(state.snapshot().total_bytes, Some(pending.asset.size_bytes));
assert_eq!(state.snapshot().phase, "downloading");
state.set_available(AppUpdateTask::default());
assert_eq!(state.snapshot().target_version.as_deref(), Some("v0.3.3"));
}
#[tokio::test]
async fn failed_or_cancelled_refresh_never_downloads_the_previous_offer() {
let state = AppUpdateState::default();
state.set_available(AppUpdateTask {
phase: "available".to_string(),
target_version: Some("v0.3.2".to_string()),
..AppUpdateTask::default()
});
let token = CancellationToken::new();
state.start(token.clone()).unwrap();
let result = refresh_app_update_for_install(&state, &token, async {
Err("version check failed".to_string())
})
.await;
assert_eq!(result.err().as_deref(), Some("version check failed"));
assert_eq!(state.snapshot().phase, "checking");
assert!(state.snapshot().target_version.is_none());
state.finish("failed", None);
let token = CancellationToken::new();
state.start(token.clone()).unwrap();
let refresh = refresh_app_update_for_install(
&state,
&token,
std::future::pending::<Result<PendingAppUpdate, String>>(),
);
let (result, _) = tokio::join!(refresh, async {
state.cancel();
});
assert_eq!(result.err().as_deref(), Some("应用更新下载已取消"));
assert!(state.snapshot().target_version.is_none());
let pending = PendingAppUpdate {
version: "v0.3.3".to_string(),
asset: portable_update_test_asset("0.3.3", "amd64"),
arch: "amd64".to_string(),
};
assert!(state.start_download(&pending).is_err());
}
#[test]
fn sha256_file_hashes_exact_portable_asset_bytes() {
let root = agent_test_home("portable-sha256");
+141 -4
View File
@@ -656,17 +656,153 @@ fn replacing_a_core_rejects_locked_config_without_using_defaults() {
}
#[test]
fn bundled_core_bootstrap_runs_only_when_no_core_binary_exists() {
fn bundled_core_install_handles_missing_and_unversioned_binaries() {
let root = agent_test_home("bundled-bootstrap-detection");
let install_dir = root.join("cpa-core");
assert!(core_needs_bundled_bootstrap(&install_dir));
assert!(core_needs_bundled_install(&install_dir, "v7.3.17"));
let existing_version = install_dir.join("existing-version");
fs::create_dir_all(&existing_version).unwrap();
fs::write(existing_version.join(core_binary_name()), b"existing core").unwrap();
assert!(!core_needs_bundled_bootstrap(&install_dir));
assert!(core_needs_bundled_install(&install_dir, "v7.3.17"));
assert!(!core_needs_bundled_install(&install_dir, "invalid"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn bundled_core_install_replaces_older_installs_without_downgrading() {
let root = agent_test_home("bundled-upgrade-detection");
let install_dir = root.join("cpa-core");
fs::create_dir_all(&install_dir).unwrap();
fs::write(install_dir.join(core_binary_name()), b"installed core").unwrap();
for (installed, bundled, should_install) in [
("v7.3.9", "v7.3.17", true),
("7.3.15", "v7.3.17", true),
("v7.3.17-beta.1", "v7.3.17", true),
("v7.3.17", "7.3.17", false),
("v7.3.18", "v7.3.17", false),
("unknown", "v7.3.17", true),
("v7.3.15", "invalid", false),
] {
write_core_metadata(
&install_dir,
&CoreMetadata {
version: installed.to_string(),
asset_name: "installed-core.zip".to_string(),
installed_at_unix: 0,
},
)
.unwrap();
assert_eq!(
core_needs_bundled_install(&install_dir, bundled),
should_install,
"installed {installed}, bundled {bundled}"
);
}
fs::remove_file(install_dir.join(core_binary_name())).unwrap();
assert!(core_needs_bundled_install(&install_dir, "v7.3.17"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn handled_bundled_core_does_not_undo_a_manual_rollback_on_restart() {
let root = agent_test_home("bundled-manual-rollback");
let install_dir = root.join("cpa-core");
fs::create_dir_all(&install_dir).unwrap();
let binary = install_dir.join(core_binary_name());
fs::write(&binary, b"installed core").unwrap();
write_core_metadata(
&install_dir,
&CoreMetadata {
version: "v7.3.15".to_string(),
asset_name: "installed-core.zip".to_string(),
installed_at_unix: 0,
},
)
.unwrap();
assert!(core_needs_bundled_install(&install_dir, "v7.3.17"));
remember_bundled_core_when_up_to_date(&install_dir, "v7.3.17").unwrap();
assert!(core_needs_bundled_install(&install_dir, "v7.3.17"));
mark_bundled_core_version_handled(&install_dir, "v7.3.17").unwrap();
assert!(!core_needs_bundled_install(&install_dir, "v7.3.17"));
assert!(core_needs_bundled_install(&install_dir, "v7.3.18"));
fs::remove_file(binary).unwrap();
assert!(core_needs_bundled_install(&install_dir, "v7.3.17"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn an_already_newer_core_satisfies_the_bundle_after_manual_rollback() {
let root = agent_test_home("bundled-newer-manual-rollback");
let install_dir = root.join("cpa-core");
fs::create_dir_all(&install_dir).unwrap();
fs::write(install_dir.join(core_binary_name()), b"installed core").unwrap();
write_core_metadata(
&install_dir,
&CoreMetadata {
version: "v7.3.18".to_string(),
asset_name: "installed-core.zip".to_string(),
installed_at_unix: 0,
},
)
.unwrap();
assert!(!core_needs_bundled_install(&install_dir, "v7.3.17"));
remember_bundled_core_when_up_to_date(&install_dir, "v7.3.17").unwrap();
write_core_metadata(
&install_dir,
&CoreMetadata {
version: "v7.3.15".to_string(),
asset_name: "manual-rollback.zip".to_string(),
installed_at_unix: 1,
},
)
.unwrap();
assert!(!core_needs_bundled_install(&install_dir, "v7.3.17"));
assert!(core_needs_bundled_install(&install_dir, "v7.3.19"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn stale_core_checksums_do_not_block_a_new_bundled_archive() {
let root = agent_test_home("bundled-stale-checksums");
let archive = root.join("CLIProxyAPI_7.3.17_windows_amd64.zip");
fs::write(&archive, b"new bundled core").unwrap();
fs::write(
root.join(CORE_CHECKSUMS_FILE),
format!("{} CLIProxyAPI_7.3.15_windows_amd64.zip\n", "a".repeat(64)),
)
.unwrap();
validate_bundled_core_checksum(&archive).unwrap();
fs::write(
root.join(CORE_CHECKSUMS_FILE),
format!(
"{} {}\n",
"a".repeat(64),
archive.file_name().unwrap().to_string_lossy()
),
)
.unwrap();
assert!(validate_bundled_core_checksum(&archive).is_err());
fs::write(
root.join(CORE_CHECKSUMS_FILE),
format!(
"{} {}\n",
sha256_file(&archive).unwrap(),
archive.file_name().unwrap().to_string_lossy()
),
)
.unwrap();
validate_bundled_core_checksum(&archive).unwrap();
fs::remove_dir_all(root).unwrap();
}
@@ -686,7 +822,8 @@ fn bundled_core_locations_include_macos_app_resources() {
macos_app_resources_dir(&executable_dir),
Some(contents_dir.join("Resources"))
);
assert!(bundled_core_locations(&base_dir, &executable_dir).contains(&resource_location));
let locations = bundled_core_locations(&base_dir, &executable_dir);
assert_eq!(locations.first(), Some(&resource_location));
}
#[test]