611 lines
22 KiB
Rust
611 lines
22 KiB
Rust
|
|
//! Invariant matrix tests for the rollback/downgrade feature.
|
|||
|
|
//!
|
|||
|
|
//! Covers every combination of:
|
|||
|
|
//! - user's current version vs. channel pointer target
|
|||
|
|
//! - installer type (internal, npm, gh-release)
|
|||
|
|
//! - channel (stable, alpha, enterprise)
|
|||
|
|
//! - pointer-flip scenarios (stable bumped after user upgraded, alpha
|
|||
|
|
//! pointer rolled back, etc.)
|
|||
|
|
//!
|
|||
|
|
//! Also includes wiremock-based installation tests that verify the GCS
|
|||
|
|
//! internal installer actually downloads and symlinks an older binary
|
|||
|
|
//! when the stable pointer is rolled back.
|
|||
|
|
|
|||
|
|
#![cfg(unix)]
|
|||
|
|
|
|||
|
|
mod common;
|
|||
|
|
|
|||
|
|
use serial_test::serial;
|
|||
|
|
use wiremock::matchers::{method, path};
|
|||
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|||
|
|
|
|||
|
|
use common::{FakeBinGuard, reset_home, set_test_version, test_home};
|
|||
|
|
use xai_grok_update::UpdateConfig;
|
|||
|
|
use xai_grok_update::auto_update::{
|
|||
|
|
auto_update_target, check_update_status, ensure_latest_on_disk, install_internal_from_base,
|
|||
|
|
};
|
|||
|
|
use xai_grok_update::version::installed_on_disk_version;
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async fn mount_gcs_with_channels(
|
|||
|
|
stable_version: &str,
|
|||
|
|
alpha_version: Option<&str>,
|
|||
|
|
binary_version: &str,
|
|||
|
|
platform: &str,
|
|||
|
|
) -> MockServer {
|
|||
|
|
let server = MockServer::start().await;
|
|||
|
|
|
|||
|
|
Mock::given(method("GET"))
|
|||
|
|
.and(path("/stable"))
|
|||
|
|
.respond_with(ResponseTemplate::new(200).set_body_string(stable_version))
|
|||
|
|
.mount(&server)
|
|||
|
|
.await;
|
|||
|
|
|
|||
|
|
if let Some(alpha_v) = alpha_version {
|
|||
|
|
Mock::given(method("GET"))
|
|||
|
|
.and(path("/alpha"))
|
|||
|
|
.respond_with(ResponseTemplate::new(200).set_body_string(alpha_v))
|
|||
|
|
.mount(&server)
|
|||
|
|
.await;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Mock::given(method("GET"))
|
|||
|
|
.and(path(format!("/grok-{binary_version}-{platform}")))
|
|||
|
|
.respond_with(ResponseTemplate::new(200).set_body_bytes(b"#!/bin/sh\nexit 0\n".to_vec()))
|
|||
|
|
.mount(&server)
|
|||
|
|
.await;
|
|||
|
|
|
|||
|
|
server
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// Scenario matrix: GCS internal installer — downgrade via install
|
|||
|
|
//
|
|||
|
|
// Each test simulates a user on version X, with the stable/alpha pointer
|
|||
|
|
// now pointing to version Y. The internal installer should install Y
|
|||
|
|
// regardless of whether Y < X (rollback) or Y > X (upgrade).
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn internal_install_stable_rollback_0_2_7_to_0_2_5() {
|
|||
|
|
// User was on 0.2.7, stable pointer rolled back to 0.2.5.
|
|||
|
|
let _ = test_home();
|
|||
|
|
reset_home();
|
|||
|
|
let platform = host_platform();
|
|||
|
|
let server = mount_gcs_with_channels("0.2.5", None, "0.2.5", &platform).await;
|
|||
|
|
let cfg = make_config("stable");
|
|||
|
|
|
|||
|
|
install_internal_from_base(Some("0.2.5"), &cfg, &server.uri())
|
|||
|
|
.await
|
|||
|
|
.unwrap();
|
|||
|
|
|
|||
|
|
let home = test_home();
|
|||
|
|
let downloaded = home
|
|||
|
|
.join("downloads")
|
|||
|
|
.join(format!("grok-0.2.5-{platform}"));
|
|||
|
|
assert!(downloaded.exists(), "rolled-back binary must be downloaded");
|
|||
|
|
|
|||
|
|
let symlink = home.join("bin").join("grok");
|
|||
|
|
let target = std::fs::read_link(&symlink).unwrap();
|
|||
|
|
assert!(
|
|||
|
|
target.to_string_lossy().contains("0.2.5"),
|
|||
|
|
"symlink must point to rolled-back version: {target:?}"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn internal_install_stable_upgrade_0_2_5_to_0_2_7() {
|
|||
|
|
// Normal upgrade path: user on 0.2.5, pointer at 0.2.7.
|
|||
|
|
let _ = test_home();
|
|||
|
|
reset_home();
|
|||
|
|
let platform = host_platform();
|
|||
|
|
let server = mount_gcs_with_channels("0.2.7", None, "0.2.7", &platform).await;
|
|||
|
|
let cfg = make_config("stable");
|
|||
|
|
|
|||
|
|
install_internal_from_base(Some("0.2.7"), &cfg, &server.uri())
|
|||
|
|
.await
|
|||
|
|
.unwrap();
|
|||
|
|
|
|||
|
|
let symlink = test_home().join("bin").join("grok");
|
|||
|
|
let target = std::fs::read_link(&symlink).unwrap();
|
|||
|
|
assert!(target.to_string_lossy().contains("0.2.7"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn internal_install_rollback_then_upgrade_sequence() {
|
|||
|
|
// Simulates: install 0.2.7 → rollback to 0.2.5 → fix ships as 0.2.8.
|
|||
|
|
// All three installs must succeed sequentially.
|
|||
|
|
let _ = test_home();
|
|||
|
|
reset_home();
|
|||
|
|
let platform = host_platform();
|
|||
|
|
|
|||
|
|
for version in ["0.2.7", "0.2.5", "0.2.8"] {
|
|||
|
|
// Age the previous installs: cleanup deliberately never deletes a
|
|||
|
|
// freshly-written binary (it may be a concurrent racer's just-renamed
|
|||
|
|
// download), so the retention assertions below need the earlier
|
|||
|
|
// installs to look like real leftovers from past releases.
|
|||
|
|
common::backdate_downloads();
|
|||
|
|
let server = mount_gcs_with_channels(version, None, version, &platform).await;
|
|||
|
|
let cfg = make_config("stable");
|
|||
|
|
install_internal_from_base(Some(version), &cfg, &server.uri())
|
|||
|
|
.await
|
|||
|
|
.unwrap();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let target = std::fs::read_link(test_home().join("bin").join("grok")).unwrap();
|
|||
|
|
assert!(
|
|||
|
|
target.to_string_lossy().contains("0.2.8"),
|
|||
|
|
"final symlink must point to 0.2.8: {target:?}"
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Cleanup retains current + highest-semver non-current (N-1 by version, not install order).
|
|||
|
|
let downloads = test_home().join("downloads");
|
|||
|
|
assert!(
|
|||
|
|
downloads.join(format!("grok-0.2.8-{platform}")).exists(),
|
|||
|
|
"current"
|
|||
|
|
);
|
|||
|
|
assert!(
|
|||
|
|
downloads.join(format!("grok-0.2.7-{platform}")).exists(),
|
|||
|
|
"N-1 by semver"
|
|||
|
|
);
|
|||
|
|
assert!(
|
|||
|
|
!downloads.join(format!("grok-0.2.5-{platform}")).exists(),
|
|||
|
|
"lowest cleaned up"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn internal_install_alpha_rollback_pointer_resolves_correctly() {
|
|||
|
|
// Alpha user on 0.2.8-alpha.3. Alpha pointer rolled back to 0.2.8-alpha.1,
|
|||
|
|
// stable pointer is 0.2.7. Alpha channel returns max(alpha, stable) = 0.2.8-alpha.1.
|
|||
|
|
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.2.7"))
|
|||
|
|
.mount(&server)
|
|||
|
|
.await;
|
|||
|
|
Mock::given(method("GET"))
|
|||
|
|
.and(path("/alpha"))
|
|||
|
|
.respond_with(ResponseTemplate::new(200).set_body_string("0.2.8-alpha.1"))
|
|||
|
|
.mount(&server)
|
|||
|
|
.await;
|
|||
|
|
// The resolved version is max(0.2.7, 0.2.8-alpha.1) = 0.2.8-alpha.1.
|
|||
|
|
// Note: semver considers 0.2.8-alpha.1 < 0.2.8 but > 0.2.7.
|
|||
|
|
Mock::given(method("GET"))
|
|||
|
|
.and(path(format!("/grok-0.2.8-alpha.1-{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 downloaded = test_home()
|
|||
|
|
.join("downloads")
|
|||
|
|
.join(format!("grok-0.2.8-alpha.1-{platform}"));
|
|||
|
|
assert!(
|
|||
|
|
downloaded.exists(),
|
|||
|
|
"alpha rollback target must be installed"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn internal_install_alpha_user_gets_newer_stable_after_stable_passes_alpha() {
|
|||
|
|
// Alpha user on 0.2.6-alpha.2. Stable ships 0.2.7 (higher than alpha).
|
|||
|
|
// Alpha channel returns max(alpha=0.2.6-alpha.2, stable=0.2.7) = 0.2.7.
|
|||
|
|
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.2.7"))
|
|||
|
|
.mount(&server)
|
|||
|
|
.await;
|
|||
|
|
Mock::given(method("GET"))
|
|||
|
|
.and(path("/alpha"))
|
|||
|
|
.respond_with(ResponseTemplate::new(200).set_body_string("0.2.6-alpha.2"))
|
|||
|
|
.mount(&server)
|
|||
|
|
.await;
|
|||
|
|
Mock::given(method("GET"))
|
|||
|
|
.and(path(format!("/grok-0.2.7-{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();
|
|||
|
|
|
|||
|
|
assert!(
|
|||
|
|
test_home()
|
|||
|
|
.join("downloads")
|
|||
|
|
.join(format!("grok-0.2.7-{platform}"))
|
|||
|
|
.exists(),
|
|||
|
|
"alpha user should get the newer stable"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// Scenario matrix: check_update_status across installer × version direction
|
|||
|
|
//
|
|||
|
|
// Uses check_update_status end-to-end with fake npm/gh binaries.
|
|||
|
|
// The internal (GCS) path can't be end-to-end tested via check_update_status
|
|||
|
|
// (hardcoded URLs), so its update-detection logic is covered by the
|
|||
|
|
// needs_update unit tests and the install tests above.
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
fn setup_npm(current_version: &str) -> FakeBinGuard {
|
|||
|
|
let _ = test_home();
|
|||
|
|
reset_home();
|
|||
|
|
set_test_version(current_version);
|
|||
|
|
// SAFETY: serial_test ensures no race; reset_home clears this between tests.
|
|||
|
|
unsafe { std::env::set_var("GROK_INSTALLER", "npm") };
|
|||
|
|
FakeBinGuard::install_npm()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn setup_gh(current_version: &str) -> FakeBinGuard {
|
|||
|
|
let _ = test_home();
|
|||
|
|
reset_home();
|
|||
|
|
set_test_version(current_version);
|
|||
|
|
// SAFETY: serial_test ensures no race; reset_home clears this between tests.
|
|||
|
|
unsafe { std::env::set_var("GROK_INSTALLER", "gh-release") };
|
|||
|
|
FakeBinGuard::install_gh()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── npm: never downgrades ──
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn npm_upgrade_reports_update() {
|
|||
|
|
let g = setup_npm("0.2.5");
|
|||
|
|
g.set_stdout("\"0.2.7\"");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(status.update_available);
|
|||
|
|
assert_eq!(status.latest_version.as_deref(), Some("0.2.7"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn npm_same_version_no_update() {
|
|||
|
|
let g = setup_npm("0.2.7");
|
|||
|
|
g.set_stdout("\"0.2.7\"");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(!status.update_available);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn npm_rollback_does_not_report_update() {
|
|||
|
|
// Stable pointer rolled back 0.2.7 → 0.2.5. npm user on 0.2.7 must NOT
|
|||
|
|
// see an update — stale registries make this path unsafe.
|
|||
|
|
let g = setup_npm("0.2.7");
|
|||
|
|
g.set_stdout("\"0.2.5\"");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(
|
|||
|
|
!status.update_available,
|
|||
|
|
"npm must never report a downgrade: current={} latest={:?}",
|
|||
|
|
status.current_version, status.latest_version
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn npm_drastically_old_registry_does_not_report_update() {
|
|||
|
|
// Corporate registry returns ancient version.
|
|||
|
|
let g = setup_npm("0.2.7");
|
|||
|
|
g.set_stdout("\"0.1.4\"");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(!status.update_available);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── gh-release: --check is upgrade-only; rollback handled by auto-install ──
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn gh_release_upgrade_reports_update() {
|
|||
|
|
let g = setup_gh("0.2.5");
|
|||
|
|
g.set_stable_only_stdout("v0.2.7\n");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(status.update_available);
|
|||
|
|
assert_eq!(status.latest_version.as_deref(), Some("0.2.7"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn gh_release_rollback_not_advertised_by_check() {
|
|||
|
|
// `update --check` advertises upgrades only; a rollback still converges via
|
|||
|
|
// the auto-install path (covered by the internal_install_* tests), not here.
|
|||
|
|
let g = setup_gh("0.2.7");
|
|||
|
|
g.set_stable_only_stdout("v0.2.5\n");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(
|
|||
|
|
!status.update_available,
|
|||
|
|
"gh-release rollback must not be advertised by --check: current={} latest={:?}",
|
|||
|
|
status.current_version, status.latest_version
|
|||
|
|
);
|
|||
|
|
assert_eq!(status.latest_version.as_deref(), Some("0.2.5"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn gh_release_same_version_no_update() {
|
|||
|
|
let g = setup_gh("0.2.7");
|
|||
|
|
g.set_stable_only_stdout("v0.2.7\n");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(!status.update_available);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// auto_update_target: the leader/background auto-install decision
|
|||
|
|
//
|
|||
|
|
// Unlike the upgrade-only `check_update_status` report, this is the
|
|||
|
|
// downgrade-aware convergence decision. It gates on the installer, so
|
|||
|
|
// authoritative installers (gh-release/internal) follow a rolled-back pointer
|
|||
|
|
// while npm never downgrades. `fetch_latest_version` keeps these hermetic.
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn auto_update_target_gh_release_rollback_returns_older() {
|
|||
|
|
let g = setup_gh("0.2.26");
|
|||
|
|
g.set_stable_only_stdout("v0.2.22\n");
|
|||
|
|
|
|||
|
|
assert_eq!(
|
|||
|
|
auto_update_target(&make_config("stable")).await,
|
|||
|
|
Some(("gh-release", "0.2.22".to_string())),
|
|||
|
|
"authoritative installer must converge down on a rolled-back pointer"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn auto_update_target_gh_release_upgrade_returns_newer() {
|
|||
|
|
let g = setup_gh("0.2.5");
|
|||
|
|
g.set_stable_only_stdout("v0.2.7\n");
|
|||
|
|
|
|||
|
|
assert_eq!(
|
|||
|
|
auto_update_target(&make_config("stable")).await,
|
|||
|
|
Some(("gh-release", "0.2.7".to_string()))
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn auto_update_target_gh_release_same_version_returns_none() {
|
|||
|
|
let g = setup_gh("0.2.7");
|
|||
|
|
g.set_stable_only_stdout("v0.2.7\n");
|
|||
|
|
|
|||
|
|
assert_eq!(auto_update_target(&make_config("stable")).await, None);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn auto_update_target_npm_rollback_returns_none() {
|
|||
|
|
// npm registries can serve stale versions — never downgrade npm installs.
|
|||
|
|
let g = setup_npm("0.2.26");
|
|||
|
|
g.set_stdout("\"0.2.22\"");
|
|||
|
|
|
|||
|
|
assert_eq!(
|
|||
|
|
auto_update_target(&make_config("stable")).await,
|
|||
|
|
None,
|
|||
|
|
"npm must never be downgraded even when the registry reports an older version"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// Disk-aware convergence: ensure_latest_on_disk + installed_on_disk_version
|
|||
|
|
//
|
|||
|
|
// Concurrent updaters (TUI background download, leader hourly checker,
|
|||
|
|
// explicit `grok update`) must decide staleness from the on-disk install, not
|
|||
|
|
// their own compiled-in version — a binary another process already installed
|
|||
|
|
// is never downloaded a second time, but a stale running process still gets
|
|||
|
|
// the relaunch signal.
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/// Lay down a managed-install layout in the test GROK_HOME:
|
|||
|
|
/// `bin/grok -> ../downloads/grok-<version>-<platform>` (what
|
|||
|
|
/// `install_internal_from_base` produces).
|
|||
|
|
fn fake_managed_install(version: &str) {
|
|||
|
|
let home = test_home();
|
|||
|
|
let downloads = home.join("downloads");
|
|||
|
|
let bin = home.join("bin");
|
|||
|
|
std::fs::create_dir_all(&downloads).unwrap();
|
|||
|
|
std::fs::create_dir_all(&bin).unwrap();
|
|||
|
|
let name = format!("grok-{version}-{}", host_platform());
|
|||
|
|
std::fs::write(downloads.join(&name), b"#!/bin/sh\nexit 0\n").unwrap();
|
|||
|
|
std::os::unix::fs::symlink(
|
|||
|
|
std::path::Path::new("../downloads").join(&name),
|
|||
|
|
bin.join("grok"),
|
|||
|
|
)
|
|||
|
|
.unwrap();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn installed_on_disk_version_reads_symlink_target() {
|
|||
|
|
let _ = test_home();
|
|||
|
|
reset_home();
|
|||
|
|
assert_eq!(installed_on_disk_version(), None, "no install yet");
|
|||
|
|
|
|||
|
|
fake_managed_install("0.2.7");
|
|||
|
|
assert_eq!(installed_on_disk_version().as_deref(), Some("0.2.7"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn ensure_latest_skips_download_when_disk_current_but_still_relaunches() {
|
|||
|
|
// Running 0.2.5, pointer 0.2.7, disk already at 0.2.7 (another process
|
|||
|
|
// downloaded it): no download, but the stale running process must relaunch.
|
|||
|
|
let g = setup_gh("0.2.5");
|
|||
|
|
g.set_stable_only_stdout("v0.2.7\n");
|
|||
|
|
fake_managed_install("0.2.7");
|
|||
|
|
|
|||
|
|
let outcome = ensure_latest_on_disk(&make_config("stable")).await.unwrap();
|
|||
|
|
assert_eq!(outcome.installed, None, "must not re-download");
|
|||
|
|
assert!(outcome.relaunch_needed, "running 0.2.5 < disk 0.2.7");
|
|||
|
|
assert!(
|
|||
|
|
!g.args_log().iter().any(|l| l.contains("release download")),
|
|||
|
|
"no gh download invocation expected, got: {:?}",
|
|||
|
|
g.args_log()
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn ensure_latest_noop_when_running_and_disk_current() {
|
|||
|
|
let g = setup_gh("0.2.7");
|
|||
|
|
g.set_stable_only_stdout("v0.2.7\n");
|
|||
|
|
fake_managed_install("0.2.7");
|
|||
|
|
|
|||
|
|
let outcome = ensure_latest_on_disk(&make_config("stable")).await.unwrap();
|
|||
|
|
assert_eq!(outcome.installed, None);
|
|||
|
|
assert!(!outcome.relaunch_needed);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn ensure_latest_relaunches_onto_rolled_back_disk() {
|
|||
|
|
// Pointer rolled back to 0.2.22 and the disk already converged; a running
|
|||
|
|
// 0.2.26 leader must relaunch onto the older binary (gh-release is an
|
|||
|
|
// authoritative installer → downgrades allowed).
|
|||
|
|
let g = setup_gh("0.2.26");
|
|||
|
|
g.set_stable_only_stdout("v0.2.22\n");
|
|||
|
|
fake_managed_install("0.2.22");
|
|||
|
|
|
|||
|
|
let outcome = ensure_latest_on_disk(&make_config("stable")).await.unwrap();
|
|||
|
|
assert_eq!(outcome.installed, None, "disk already at pointer");
|
|||
|
|
assert!(outcome.relaunch_needed, "downgrade relaunch expected");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// Pointer-flip timing scenarios
|
|||
|
|
//
|
|||
|
|
// These test the race between a user opening grok (which caches the version)
|
|||
|
|
// and a pointer flip happening. The 30-min TTL means the user won't see the
|
|||
|
|
// new pointer until the cache expires, but once it does, the correct behavior
|
|||
|
|
// must kick in.
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn npm_user_upgraded_then_stable_rolled_back_stays_on_newer() {
|
|||
|
|
// User ran `grok update` and got 0.2.7. Then stable was rolled back to
|
|||
|
|
// 0.2.5. Next check_update_status sees 0.2.5 from npm. npm installer
|
|||
|
|
// must NOT report a downgrade.
|
|||
|
|
let g = setup_npm("0.2.7");
|
|||
|
|
g.set_stdout("\"0.2.5\"");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(!status.update_available);
|
|||
|
|
assert_eq!(status.latest_version.as_deref(), Some("0.2.5"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn gh_release_user_ahead_of_pointer_check_reports_no_update() {
|
|||
|
|
// User manually installed 0.2.26 (ahead of the stable pointer 0.2.22);
|
|||
|
|
// `update --check` must not present the older pointer as a new version.
|
|||
|
|
let g = setup_gh("0.2.26");
|
|||
|
|
g.set_stable_only_stdout("v0.2.22\n");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
assert!(
|
|||
|
|
!status.update_available,
|
|||
|
|
"ahead-of-pointer must not be advertised as an update: current={} latest={:?}",
|
|||
|
|
status.current_version, status.latest_version
|
|||
|
|
);
|
|||
|
|
assert_eq!(status.latest_version.as_deref(), Some("0.2.22"));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn npm_alpha_user_upgrade_after_stable_surpasses_alpha() {
|
|||
|
|
// Alpha user on 0.2.6-alpha.2. Stable ships 0.2.7. npm returns 0.2.7
|
|||
|
|
// for the @latest tag. User should upgrade.
|
|||
|
|
let g = setup_npm("0.2.6-alpha.2");
|
|||
|
|
g.set_stdout("\"0.2.7\"");
|
|||
|
|
|
|||
|
|
let status = check_update_status(&make_config("stable")).await;
|
|||
|
|
// Pre-release current on stable channel forces install.
|
|||
|
|
assert!(
|
|||
|
|
status.update_available,
|
|||
|
|
"alpha user should upgrade to stable when stable surpasses alpha"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
// Double-rollback scenario
|
|||
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
#[tokio::test]
|
|||
|
|
#[serial]
|
|||
|
|
async fn internal_install_double_rollback() {
|
|||
|
|
// Ship 0.2.7 → rollback to 0.2.5 → rollback further to 0.2.3.
|
|||
|
|
// The installer must handle multiple sequential downgrades.
|
|||
|
|
let _ = test_home();
|
|||
|
|
reset_home();
|
|||
|
|
let platform = host_platform();
|
|||
|
|
|
|||
|
|
for version in ["0.2.7", "0.2.5", "0.2.3"] {
|
|||
|
|
let server = mount_gcs_with_channels(version, None, version, &platform).await;
|
|||
|
|
let cfg = make_config("stable");
|
|||
|
|
install_internal_from_base(Some(version), &cfg, &server.uri())
|
|||
|
|
.await
|
|||
|
|
.unwrap();
|
|||
|
|
|
|||
|
|
let target = std::fs::read_link(test_home().join("bin").join("grok")).unwrap();
|
|||
|
|
assert!(
|
|||
|
|
target.to_string_lossy().contains(version),
|
|||
|
|
"symlink must point to {version} after install: {target:?}"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|