665 lines
23 KiB
Rust
665 lines
23 KiB
Rust
//! End-to-end tests for `install_internal` — the GCS-bucket installer used
|
|
//! when `installer = "internal"` is configured.
|
|
//!
|
|
//! Wires together a wiremock-mocked GCS bucket + an isolated `GROK_HOME`
|
|
//! tempdir so we can verify the full install pipeline:
|
|
//! fetch version → download grok binary → chmod → atomic symlink →
|
|
//! cleanup_old_downloads → persist installer config.
|
|
//!
|
|
//! The function reads `grok_home()` (a process-wide `OnceLock`), so all
|
|
//! tests in this binary share a single `GROK_HOME` and run serially via
|
|
//! `#[serial]`.
|
|
|
|
#![cfg(unix)]
|
|
|
|
mod common;
|
|
|
|
use serial_test::serial;
|
|
use wiremock::matchers::{method, path};
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
|
|
use common::{reset_home, test_home};
|
|
use xai_grok_update::UpdateConfig;
|
|
use xai_grok_update::auto_update::{install_internal_from_base, install_internal_from_bases};
|
|
|
|
fn host_platform() -> String {
|
|
let os = if cfg!(target_os = "macos") {
|
|
"macos"
|
|
} else if cfg!(target_os = "linux") {
|
|
"linux"
|
|
} else {
|
|
panic!("unsupported test platform");
|
|
};
|
|
let arch = if cfg!(target_arch = "x86_64") {
|
|
"x86_64"
|
|
} else if cfg!(target_arch = "aarch64") {
|
|
"aarch64"
|
|
} else {
|
|
panic!("unsupported test arch");
|
|
};
|
|
format!("{os}-{arch}")
|
|
}
|
|
|
|
fn make_config(channel: &str) -> UpdateConfig {
|
|
UpdateConfig {
|
|
proxy_base_url: "http://test.invalid/v1".to_string(),
|
|
auth_scope: "test".to_string(),
|
|
deployment_key: None,
|
|
alpha_test_key: None,
|
|
channel: channel.to_string(),
|
|
npm_registry: None,
|
|
}
|
|
}
|
|
|
|
/// Mount GCS endpoints for a given version. Returns the `MockServer`.
|
|
async fn mount_gcs(version: &str, platform: &str) -> MockServer {
|
|
let server = MockServer::start().await;
|
|
|
|
// Channel pointer: stable returns this version.
|
|
Mock::given(method("GET"))
|
|
.and(path("/stable"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_string(version))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
// Main grok binary download.
|
|
Mock::given(method("GET"))
|
|
.and(path(format!("/grok-{version}-{platform}")))
|
|
.respond_with(ResponseTemplate::new(200).set_body_bytes(b"#!/bin/sh\nexit 0\n".to_vec()))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
server
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Happy-path
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_pinned_version_writes_binary_and_symlink() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
let home = test_home();
|
|
let downloaded = home
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}"));
|
|
assert!(downloaded.exists(), "binary downloaded: {downloaded:?}");
|
|
assert_eq!(std::fs::read(&downloaded).unwrap(), b"#!/bin/sh\nexit 0\n");
|
|
|
|
let symlink = home.join("bin").join("grok");
|
|
assert!(symlink.is_symlink(), "grok symlink created");
|
|
let target = std::fs::read_link(&symlink).unwrap();
|
|
assert_eq!(
|
|
target.file_name().unwrap(),
|
|
format!("grok-0.1.181-{platform}").as_str()
|
|
);
|
|
|
|
// `grok` and `agent` move together — see `swap_managed_bin_links`.
|
|
let agent_link = home.join("bin").join("agent");
|
|
assert!(agent_link.is_symlink(), "agent symlink created");
|
|
let agent_target = std::fs::read_link(&agent_link).unwrap();
|
|
assert_eq!(agent_target, target, "agent and grok point at same target");
|
|
}
|
|
|
|
/// Regression: pre-existing `agent` symlink from a prior install must be
|
|
/// swapped to the new version, not left stale (the original bug).
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_updates_stale_agent_symlink_to_new_version() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
// Prior install: both links point at an older versioned binary.
|
|
let home = test_home();
|
|
let bin_dir = home.join("bin");
|
|
let download_dir = home.join("downloads");
|
|
std::fs::create_dir_all(&bin_dir).unwrap();
|
|
std::fs::create_dir_all(&download_dir).unwrap();
|
|
let old_binary = download_dir.join(format!("grok-0.1.180-{platform}"));
|
|
std::fs::write(&old_binary, b"#!/bin/sh\nexit 0\n").unwrap();
|
|
let rel_old = std::path::Path::new("..")
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.180-{platform}"));
|
|
std::os::unix::fs::symlink(&rel_old, bin_dir.join("grok")).unwrap();
|
|
std::os::unix::fs::symlink(&rel_old, bin_dir.join("agent")).unwrap();
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
let agent_link = bin_dir.join("agent");
|
|
let agent_target = std::fs::read_link(&agent_link).unwrap();
|
|
assert_eq!(
|
|
agent_target.file_name().unwrap(),
|
|
format!("grok-0.1.181-{platform}").as_str(),
|
|
"agent symlink must swap to the new version, not stay on old"
|
|
);
|
|
}
|
|
|
|
/// Rollback regression: if `agent` swap fails after `grok` succeeded,
|
|
/// `grok` must roll back to its prior target (all-or-nothing).
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_rolls_back_grok_when_agent_swap_fails() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
let home = test_home();
|
|
let bin_dir = home.join("bin");
|
|
let download_dir = home.join("downloads");
|
|
std::fs::create_dir_all(&bin_dir).unwrap();
|
|
std::fs::create_dir_all(&download_dir).unwrap();
|
|
let old_binary = download_dir.join(format!("grok-0.1.180-{platform}"));
|
|
std::fs::write(&old_binary, b"#!/bin/sh\nexit 0\n").unwrap();
|
|
let rel_old = std::path::Path::new("..")
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.180-{platform}"));
|
|
std::os::unix::fs::symlink(&rel_old, bin_dir.join("grok")).unwrap();
|
|
|
|
// Sabotage the agent swap: non-empty directory → rename fails with EISDIR.
|
|
let agent_dir = bin_dir.join("agent");
|
|
std::fs::create_dir(&agent_dir).unwrap();
|
|
std::fs::write(agent_dir.join("blocker"), b"x").unwrap();
|
|
|
|
let err = install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.expect_err("agent swap must fail when target is a non-empty dir");
|
|
drop(err);
|
|
|
|
// grok must be rolled back to the prior version.
|
|
let grok_target = std::fs::read_link(bin_dir.join("grok")).unwrap();
|
|
assert_eq!(
|
|
grok_target.file_name().unwrap(),
|
|
format!("grok-0.1.180-{platform}").as_str(),
|
|
"grok must be rolled back when agent swap fails"
|
|
);
|
|
}
|
|
|
|
/// Absent-prior rollback regression: fresh install (no prior `grok` /
|
|
/// `agent`), sabotaged `agent` swap must *remove* the just-created `grok`
|
|
/// link so we don't leave it on the new binary while `agent` is absent.
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_rollback_removes_absent_prior_grok_link() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
let home = test_home();
|
|
let bin_dir = home.join("bin");
|
|
std::fs::create_dir_all(&bin_dir).unwrap();
|
|
|
|
// No prior `grok`. Sabotage `agent` swap: non-empty directory → EISDIR.
|
|
let agent_dir = bin_dir.join("agent");
|
|
std::fs::create_dir(&agent_dir).unwrap();
|
|
std::fs::write(agent_dir.join("blocker"), b"x").unwrap();
|
|
assert!(
|
|
!bin_dir.join("grok").exists() && !bin_dir.join("grok").is_symlink(),
|
|
"precondition: grok must not exist before install",
|
|
);
|
|
|
|
let err = install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.expect_err("agent swap must fail when target is a non-empty dir");
|
|
drop(err);
|
|
|
|
let grok_path = bin_dir.join("grok");
|
|
assert!(
|
|
!grok_path.is_symlink() && !grok_path.exists(),
|
|
"grok must be removed on rollback when there was no prior link",
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_chmods_binary_executable() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
let home = test_home();
|
|
let binary = home
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}"));
|
|
let mode = std::fs::metadata(&binary).unwrap().permissions().mode();
|
|
assert!(mode & 0o111 != 0, "binary must be executable, got {mode:o}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_cleans_up_stale_pager_symlink() {
|
|
// Old installations shipped a separate grok-pager binary. Verify the
|
|
// update removes the stale symlink from ~/.grok/bin/.
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
let home = test_home();
|
|
let bin_dir = home.join("bin");
|
|
std::fs::create_dir_all(&bin_dir).unwrap();
|
|
let pager_link = bin_dir.join("grok-pager");
|
|
std::os::unix::fs::symlink("/tmp/fake-old-pager", &pager_link).unwrap();
|
|
assert!(
|
|
pager_link.is_symlink(),
|
|
"precondition: stale symlink exists"
|
|
);
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
!pager_link.exists() && !pager_link.is_symlink(),
|
|
"stale grok-pager symlink should be removed"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_persists_installer_config() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
let home = test_home();
|
|
let cfg_body = std::fs::read_to_string(home.join("config.toml")).unwrap();
|
|
assert!(
|
|
cfg_body.contains("installer = \"internal\""),
|
|
"config should set installer = internal: {cfg_body}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_resolves_version_via_channel_pointer_when_no_target() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
// No pinned version → must fetch /stable pointer to resolve.
|
|
install_internal_from_base(None, &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
let home = test_home();
|
|
assert!(
|
|
home.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}"))
|
|
.exists(),
|
|
"binary at version from /stable pointer"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_alpha_channel_resolves_max_of_alpha_and_stable() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = MockServer::start().await;
|
|
|
|
// Stable points to 0.1.181, alpha points to 0.1.180-alpha.5 — stable wins.
|
|
Mock::given(method("GET"))
|
|
.and(path("/stable"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_string("0.1.181"))
|
|
.mount(&server)
|
|
.await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/alpha"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_string("0.1.180-alpha.5"))
|
|
.mount(&server)
|
|
.await;
|
|
Mock::given(method("GET"))
|
|
.and(path(format!("/grok-0.1.181-{platform}")))
|
|
.respond_with(ResponseTemplate::new(200).set_body_bytes(b"#!/bin/sh\nexit 0\n".to_vec()))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let cfg = make_config("alpha");
|
|
install_internal_from_base(None, &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
let home = test_home();
|
|
assert!(
|
|
home.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}"))
|
|
.exists()
|
|
);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Failure paths
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_fails_on_grok_binary_404() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("GET"))
|
|
.and(path("/stable"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_string("0.1.181"))
|
|
.mount(&server)
|
|
.await;
|
|
// Main binary returns 404 — must propagate as error.
|
|
Mock::given(method("GET"))
|
|
.and(path(format!("/grok-0.1.181-{platform}")))
|
|
.respond_with(ResponseTemplate::new(404))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let cfg = make_config("stable");
|
|
let err = install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("Download failed"), "msg: {msg}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_rejects_invalid_pinned_version() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let server = MockServer::start().await;
|
|
let cfg = make_config("stable");
|
|
|
|
let err = install_internal_from_base(Some("not-a-version"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("invalid version format"), "msg: {msg}");
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Cleanup integration: install v1, then v2, verify N-1 retention.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_cleans_up_old_versions_keeping_n_minus_one() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
|
|
// Install v1, v2, v3 sequentially. After v3, only v3 (current) and v2
|
|
// (N-1) should remain on disk; v1 should be deleted.
|
|
for v in ["0.1.179", "0.1.180", "0.1.181"] {
|
|
// Age earlier installs: cleanup never deletes freshly-written
|
|
// binaries (concurrent-racer protection), so retention assertions
|
|
// need the previous installs to look like old leftovers.
|
|
common::backdate_downloads();
|
|
let server = mount_gcs(v, &platform).await;
|
|
let cfg = make_config("stable");
|
|
install_internal_from_base(Some(v), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let home = test_home();
|
|
let downloads = home.join("downloads");
|
|
assert!(
|
|
downloads.join(format!("grok-0.1.181-{platform}")).exists(),
|
|
"current"
|
|
);
|
|
assert!(
|
|
downloads.join(format!("grok-0.1.180-{platform}")).exists(),
|
|
"N-1 retained"
|
|
);
|
|
assert!(
|
|
!downloads.join(format!("grok-0.1.179-{platform}")).exists(),
|
|
"oldest deleted"
|
|
);
|
|
|
|
// Symlink updated to latest.
|
|
let target = std::fs::read_link(home.join("bin").join("grok")).unwrap();
|
|
assert!(
|
|
target
|
|
.file_name()
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.contains("0.1.181"),
|
|
"symlink points to latest: {target:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_idempotent_for_same_version() {
|
|
// Re-installing the same version should not error and should leave the
|
|
// binary at the same path with the same content.
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
let first = std::fs::read(
|
|
test_home()
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}")),
|
|
)
|
|
.unwrap();
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
let second = std::fs::read(
|
|
test_home()
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}")),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(first, second);
|
|
let target = std::fs::read_link(test_home().join("bin").join("grok")).unwrap();
|
|
assert!(target.to_string_lossy().contains("0.1.181"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_creates_grok_home_subdirs_if_missing() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
// Explicitly delete bin/ and downloads/ so install must create them.
|
|
let _ = std::fs::remove_dir_all(test_home().join("bin"));
|
|
let _ = std::fs::remove_dir_all(test_home().join("downloads"));
|
|
|
|
let platform = host_platform();
|
|
let server = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
install_internal_from_base(Some("0.1.181"), &cfg, &server.uri())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(test_home().join("bin").is_dir());
|
|
assert!(test_home().join("downloads").is_dir());
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Multi-base URL fallback: install_internal_from_bases tries each base in
|
|
// preference order, falling through to the next on failure.
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_from_bases_falls_back_to_secondary_when_primary_fails() {
|
|
// Primary server returns 500 on every endpoint (CDN outage simulation);
|
|
// fallback server serves the install successfully. Result: install
|
|
// succeeds via fallback.
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
|
|
let primary = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.respond_with(ResponseTemplate::new(500))
|
|
.mount(&primary)
|
|
.await;
|
|
|
|
let fallback = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
install_internal_from_bases(
|
|
Some("0.1.181"),
|
|
&cfg,
|
|
&[primary.uri().as_str(), fallback.uri().as_str()],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
test_home()
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}"))
|
|
.exists(),
|
|
"fallback should produce a downloaded binary"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_from_bases_uses_primary_when_it_works() {
|
|
// Both bases work; the install must use the primary (first one) and
|
|
// never touch the fallback. Verified by tearing down the fallback
|
|
// server immediately after configuration — if the install reached for
|
|
// it, the request would fail.
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
|
|
let primary = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
install_internal_from_bases(
|
|
Some("0.1.181"),
|
|
&cfg,
|
|
&[primary.uri().as_str(), "http://127.0.0.1:1"],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(
|
|
test_home()
|
|
.join("downloads")
|
|
.join(format!("grok-0.1.181-{platform}"))
|
|
.exists()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_from_bases_propagates_last_error_when_all_fail() {
|
|
// Every base returns 500 — the install must fail, surfacing the final
|
|
// base's error rather than silently succeeding.
|
|
let _ = test_home();
|
|
reset_home();
|
|
|
|
let bad1 = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.respond_with(ResponseTemplate::new(500))
|
|
.mount(&bad1)
|
|
.await;
|
|
|
|
let bad2 = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.respond_with(ResponseTemplate::new(500))
|
|
.mount(&bad2)
|
|
.await;
|
|
|
|
let cfg = make_config("stable");
|
|
let err = install_internal_from_bases(
|
|
Some("0.1.181"),
|
|
&cfg,
|
|
&[bad1.uri().as_str(), bad2.uri().as_str()],
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("Download failed"), "msg: {msg}");
|
|
}
|
|
|
|
/// Regression: a local failure after a successful download (sabotaged
|
|
/// `agent` swap) must fail the install immediately — the fallback base must
|
|
/// never be contacted for a pointless re-download.
|
|
#[tokio::test]
|
|
#[serial]
|
|
async fn install_internal_from_bases_does_not_redownload_on_local_swap_failure() {
|
|
let _ = test_home();
|
|
reset_home();
|
|
let platform = host_platform();
|
|
|
|
let primary = mount_gcs("0.1.181", &platform).await;
|
|
let fallback = mount_gcs("0.1.181", &platform).await;
|
|
let cfg = make_config("stable");
|
|
|
|
let home = test_home();
|
|
let bin_dir = home.join("bin");
|
|
std::fs::create_dir_all(&bin_dir).unwrap();
|
|
// Sabotage activation: agent as a non-empty dir fails the swap's
|
|
// rollback capture (read_link on a directory) before any rename.
|
|
let agent_dir = bin_dir.join("agent");
|
|
std::fs::create_dir(&agent_dir).unwrap();
|
|
std::fs::write(agent_dir.join("blocker"), b"x").unwrap();
|
|
|
|
install_internal_from_bases(
|
|
Some("0.1.181"),
|
|
&cfg,
|
|
&[primary.uri().as_str(), fallback.uri().as_str()],
|
|
)
|
|
.await
|
|
.expect_err("swap failure must fail the install");
|
|
|
|
let fallback_requests = fallback
|
|
.received_requests()
|
|
.await
|
|
.expect("request recording is enabled on MockServer::start()");
|
|
assert!(
|
|
fallback_requests.is_empty(),
|
|
"local swap failure must not fall through to the next base: {} request(s)",
|
|
fallback_requests.len()
|
|
);
|
|
}
|