Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
611
crates/codegen/xai-grok-update/tests/test_downgrade_matrix.rs
Normal file
611
crates/codegen/xai-grok-update/tests/test_downgrade_matrix.rs
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
//! 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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue