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).
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
//! Subprocess-based integration tests using fake `npm` / `gh` shell scripts
|
||||
//! placed first on `PATH`.
|
||||
//!
|
||||
//! `auto_update::install_npm` and `version::fetch_npm_tag` spawn `npm` by
|
||||
//! bare name (`Command::new("npm")`). To test them without touching the real
|
||||
//! npm registry, we install a tempdir-resident shell script named `npm`
|
||||
//! that logs its args and prints canned stdout, then prepend that tempdir
|
||||
//! to `PATH` for the duration of the test.
|
||||
//!
|
||||
//! Same pattern for `gh` for the `gh-release` installer paths.
|
||||
//!
|
||||
//! All tests in this file mutate `PATH` (global), so they're serialized with
|
||||
//! `#[serial]`.
|
||||
|
||||
#![cfg(unix)]
|
||||
|
||||
mod common;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serial_test::serial;
|
||||
|
||||
use common::FakeBinGuard;
|
||||
use kigi_update::auto_update::install_npm_for_test;
|
||||
use kigi_update::version::{
|
||||
fetch_gh_release_version, fetch_npm_tag_for_test, fetch_npm_version_for_test,
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// fetch_npm_tag — reads a single dist-tag from `npm view`.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_returns_string_response() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("\"0.1.181\"\n");
|
||||
|
||||
let v = fetch_npm_tag_for_test("latest", None).await.unwrap();
|
||||
assert_eq!(v, "0.1.181");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_returns_array_response_picks_last() {
|
||||
// npm view sometimes returns an array of versions for ambiguous specs.
|
||||
// The implementation picks the LAST one (rev().find_map).
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout(r#"["0.1.179", "0.1.180", "0.1.181"]"#);
|
||||
|
||||
let v = fetch_npm_tag_for_test("latest", None).await.unwrap();
|
||||
assert_eq!(v, "0.1.181");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_passes_pkg_and_tag_to_npm() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("\"0.1.181\"");
|
||||
|
||||
let _ = fetch_npm_tag_for_test("latest", None).await.unwrap();
|
||||
let log = g.args_log();
|
||||
assert_eq!(log.len(), 1, "exactly one npm invocation");
|
||||
let args = &log[0];
|
||||
assert!(args.contains("view"), "args: {args}");
|
||||
// For "latest" tag, no `@latest` suffix is appended in pkg_spec.
|
||||
assert!(args.contains("@xai-official/grok"), "args: {args}");
|
||||
assert!(!args.contains("@latest"), "args: {args}");
|
||||
assert!(args.contains("--json"), "args: {args}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_alpha_appends_at_alpha_suffix() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_alpha_stdout("\"0.1.181-alpha.1\"");
|
||||
|
||||
let v = fetch_npm_tag_for_test("alpha", None).await.unwrap();
|
||||
assert_eq!(v, "0.1.181-alpha.1");
|
||||
|
||||
let log = g.args_log();
|
||||
assert!(
|
||||
log[0].contains("@xai-official/grok@alpha"),
|
||||
"args: {}",
|
||||
log[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_passes_registry_flag_when_set() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("\"0.1.181\"");
|
||||
|
||||
let _ = fetch_npm_tag_for_test("latest", Some("https://npm.example.com"))
|
||||
.await
|
||||
.unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(
|
||||
log[0].contains("--registry=https://npm.example.com"),
|
||||
"args: {}",
|
||||
log[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_no_registry_flag_when_unset() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("\"0.1.181\"");
|
||||
|
||||
let _ = fetch_npm_tag_for_test("latest", None).await.unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(!log[0].contains("--registry"), "args: {}", log[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_propagates_npm_failure() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_exit_code(1);
|
||||
|
||||
let err = fetch_npm_tag_for_test("latest", None).await.unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(msg.contains("npm view"), "msg: {msg}");
|
||||
assert!(msg.contains("failed"), "msg: {msg}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_invalid_json_returns_err() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("not valid json {");
|
||||
|
||||
let err = fetch_npm_tag_for_test("latest", None).await.unwrap_err();
|
||||
// serde_json should error on this.
|
||||
let msg = format!("{err:#}");
|
||||
assert!(!msg.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_unexpected_json_shape_returns_err() {
|
||||
// npm view can return null, an object, etc. The function expects string
|
||||
// or array of strings — anything else is an error.
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("42");
|
||||
|
||||
let err = fetch_npm_tag_for_test("latest", None).await.unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(msg.contains("unexpected JSON"), "msg: {msg}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_tag_empty_array_returns_err() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("[]");
|
||||
|
||||
let err = fetch_npm_tag_for_test("latest", None).await.unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(msg.contains("empty"), "msg: {msg}");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// fetch_npm_version — alpha channel calls both tags and returns the max.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_version_stable_calls_only_latest() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("\"0.1.181\"");
|
||||
|
||||
let v = fetch_npm_version_for_test("stable", None).await.unwrap();
|
||||
assert_eq!(v, "0.1.181");
|
||||
assert_eq!(g.args_log().len(), 1, "stable should make one call");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_version_alpha_returns_max_of_alpha_and_latest_when_alpha_higher() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("\"0.1.181\""); // latest tag → stable
|
||||
g.set_alpha_stdout("\"0.1.182-alpha.1\""); // alpha tag
|
||||
|
||||
let v = fetch_npm_version_for_test("alpha", None).await.unwrap();
|
||||
assert_eq!(v, "0.1.182-alpha.1");
|
||||
assert_eq!(g.args_log().len(), 2, "alpha should make two calls");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_npm_version_alpha_returns_stable_when_higher() {
|
||||
// Common case: stable shipped after a stale alpha tag — must not strand
|
||||
// alpha users on the older alpha.
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_stdout("\"0.1.182\"");
|
||||
g.set_alpha_stdout("\"0.1.181-alpha.1\"");
|
||||
|
||||
let v = fetch_npm_version_for_test("alpha", None).await.unwrap();
|
||||
assert_eq!(v, "0.1.182");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// install_npm — spawns `npm i -g @pkg@version`.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_calls_npm_with_version_arg() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
// No stdout/exit setup → succeeds with empty stdout.
|
||||
|
||||
install_npm_for_test(Some("0.1.181"), "stable", None).unwrap();
|
||||
let log = g.args_log();
|
||||
assert_eq!(log.len(), 1, "exactly one npm invocation");
|
||||
let args = &log[0];
|
||||
assert!(args.contains("i -g"), "args: {args}");
|
||||
assert!(args.contains("@xai-official/grok@0.1.181"), "args: {args}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_falls_back_to_dist_tag_on_no_target() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
|
||||
install_npm_for_test(None, "stable", None).unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(
|
||||
log[0].contains("@xai-official/grok@latest"),
|
||||
"stable channel uses @latest dist-tag: {}",
|
||||
log[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_falls_back_to_alpha_dist_tag_on_alpha_channel() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
|
||||
install_npm_for_test(None, "alpha", None).unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(
|
||||
log[0].contains("@xai-official/grok@alpha"),
|
||||
"alpha channel uses @alpha dist-tag: {}",
|
||||
log[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_passes_registry_flag_when_set() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
|
||||
install_npm_for_test(Some("0.1.181"), "stable", Some("https://npm.example.com")).unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(
|
||||
log[0].contains("--registry=https://npm.example.com"),
|
||||
"args: {}",
|
||||
log[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_no_registry_flag_when_unset() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
|
||||
install_npm_for_test(Some("0.1.181"), "stable", None).unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(!log[0].contains("--registry"), "args: {}", log[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_returns_err_on_npm_failure() {
|
||||
let g = FakeBinGuard::install_npm();
|
||||
g.set_exit_code(1);
|
||||
|
||||
let err = install_npm_for_test(Some("0.1.181"), "stable", None).unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(msg.contains("npm install failed"), "msg: {msg}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_with_token_passes_userconfig() {
|
||||
// SAFETY: serial_test ensures no other thread touches NPM_TOKEN.
|
||||
unsafe { std::env::set_var("NPM_TOKEN", "secrettoken") };
|
||||
let g = FakeBinGuard::install_npm();
|
||||
|
||||
install_npm_for_test(Some("0.1.181"), "stable", None).unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(
|
||||
log[0].contains("--userconfig="),
|
||||
"with NPM_TOKEN, must pass --userconfig: {}",
|
||||
log[0]
|
||||
);
|
||||
// The userconfig path should be cleaned up afterwards.
|
||||
let userconfig_arg = log[0]
|
||||
.split_whitespace()
|
||||
.find(|a| a.starts_with("--userconfig="))
|
||||
.unwrap()
|
||||
.trim_start_matches("--userconfig=");
|
||||
assert!(
|
||||
!std::path::Path::new(userconfig_arg).exists(),
|
||||
"userconfig file should be cleaned up: {userconfig_arg}"
|
||||
);
|
||||
unsafe { std::env::remove_var("NPM_TOKEN") };
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn install_npm_no_token_no_userconfig() {
|
||||
unsafe { std::env::remove_var("NPM_TOKEN") };
|
||||
let g = FakeBinGuard::install_npm();
|
||||
|
||||
install_npm_for_test(Some("0.1.181"), "stable", None).unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(!log[0].contains("--userconfig"), "args: {}", log[0]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// fetch_gh_release_version — exercises the `gh release list` shell-out.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_stable_returns_tag_stripped() {
|
||||
let g = FakeBinGuard::install_gh();
|
||||
// For stable channel, only the `--exclude-pre-releases` invocation is made.
|
||||
g.set_stable_only_stdout("v0.1.181\n");
|
||||
|
||||
let v = fetch_gh_release_version("stable").await.unwrap();
|
||||
assert_eq!(v, "0.1.181");
|
||||
|
||||
let log = g.args_log();
|
||||
assert_eq!(log.len(), 1);
|
||||
assert!(
|
||||
log[0].contains("--exclude-pre-releases"),
|
||||
"args: {}",
|
||||
log[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_stable_handles_tag_without_v_prefix() {
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_stable_only_stdout("0.1.181");
|
||||
|
||||
let v = fetch_gh_release_version("stable").await.unwrap();
|
||||
assert_eq!(v, "0.1.181");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_alpha_returns_max_of_pre_and_stable() {
|
||||
// Alpha channel makes two `gh release list` calls (with and without
|
||||
// --exclude-pre-releases) and returns the semver-max.
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_with_pre_stdout("v0.1.182-alpha.1");
|
||||
g.set_stable_only_stdout("v0.1.181");
|
||||
|
||||
let v = fetch_gh_release_version("alpha").await.unwrap();
|
||||
assert_eq!(v, "0.1.182-alpha.1");
|
||||
assert_eq!(g.args_log().len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_alpha_returns_stable_when_higher() {
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_with_pre_stdout("v0.1.180-alpha.5");
|
||||
g.set_stable_only_stdout("v0.1.181");
|
||||
|
||||
let v = fetch_gh_release_version("alpha").await.unwrap();
|
||||
assert_eq!(v, "0.1.181");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_propagates_gh_failure() {
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_exit_code(1);
|
||||
|
||||
let err = fetch_gh_release_version("stable").await.unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(msg.contains("gh release list"), "msg: {msg}");
|
||||
assert!(msg.contains("failed"), "msg: {msg}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_empty_response_returns_err() {
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_stable_only_stdout("");
|
||||
|
||||
let err = fetch_gh_release_version("stable").await.unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(msg.contains("No releases found"), "msg: {msg}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_passes_repo_flag() {
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_stable_only_stdout("v0.1.181");
|
||||
|
||||
let _ = fetch_gh_release_version("stable").await.unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(log[0].contains("--repo"), "args: {}", log[0]);
|
||||
assert!(
|
||||
log[0].contains("xai-org-shared/grok-build"),
|
||||
"args: {}",
|
||||
log[0]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_uses_jq_to_extract_tag() {
|
||||
// The function constructs `gh release list --json tagName --jq '.[0].tagName'`
|
||||
// — we verify the args include the jq filter so a refactor doesn't accidentally
|
||||
// drop it.
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_stable_only_stdout("v0.1.181");
|
||||
|
||||
let _ = fetch_gh_release_version("stable").await.unwrap();
|
||||
let log = g.args_log();
|
||||
assert!(log[0].contains("--json"), "args: {}", log[0]);
|
||||
assert!(log[0].contains("--jq"), "args: {}", log[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn fetch_gh_release_does_not_hang_on_quick_responses() {
|
||||
// Sanity: every call should return well under our test timeout.
|
||||
let g = FakeBinGuard::install_gh();
|
||||
g.set_stable_only_stdout("v0.1.181");
|
||||
|
||||
let res =
|
||||
tokio::time::timeout(Duration::from_secs(5), fetch_gh_release_version("stable")).await;
|
||||
assert!(res.is_ok(), "should not hang");
|
||||
}
|
||||
Reference in New Issue
Block a user