Files
Kigi-CLI/crates/codegen/kigi-update/tests/test_install_internal.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

666 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 `KIGI_SHARE_DIR`
//! 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 `kigi_home()` (a process-wide `OnceLock`), so all
//! tests in this binary share a single `KIGI_SHARE_DIR` 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 kigi_update::UpdateConfig;
use kigi_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 ~/.kigi/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_kigi_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()
);
}