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,628 @@
|
||||
//! Runtime-tunable timing/threshold config for the workspace tool server.
|
||||
//!
|
||||
//! All values are read once at startup from `KIGI_WORKSPACE_*` environment
|
||||
//! variables via [`StatusConfig::from_env`]. Unset or unparseable variables
|
||||
//! fall back to the documented defaults (with a `warn!` on parse failure), so
|
||||
//! construction never fails.
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
// ── Default timing/threshold values ──────────────────────────────────────
|
||||
// Single source of truth for the `StatusConfig::default()` values and the
|
||||
// documented fallbacks for each `KIGI_WORKSPACE_*` env var.
|
||||
|
||||
/// Default interval between status/heartbeat emissions.
|
||||
const DEFAULT_HEARTBEAT_SECS: u64 = 30;
|
||||
/// Default transport keepalive interval. Should exceed `heartbeat`.
|
||||
const DEFAULT_KEEPALIVE_SECS: u64 = 60;
|
||||
/// Default WebSocket keepalive ping cadence for the server SDK connection.
|
||||
const DEFAULT_WS_PING_SECS: u64 = 30;
|
||||
/// Default number of consecutive hub reconnect failures before warning.
|
||||
const DEFAULT_HUB_WARN_THRESHOLD: u32 = 5;
|
||||
/// Default base delay (ms) for exponential backoff on failed hub sends.
|
||||
const DEFAULT_HUB_BACKOFF_BASE_MS: u64 = 100;
|
||||
/// Default idle window (s) after which an inactive session is pruned. Kept
|
||||
/// aligned with the sandbox service's idle-hibernate grace.
|
||||
const DEFAULT_SESSION_IDLE_PRUNE_SECS: u64 = 1800;
|
||||
/// Default max time (s) to wait for in-flight work to drain on shutdown.
|
||||
const DEFAULT_DRAIN_TIMEOUT_SECS: u64 = 30;
|
||||
/// Default per-call timeout (s) for agent gRPC RPCs.
|
||||
const DEFAULT_AGENT_RPC_TIMEOUT_SECS: u64 = 30;
|
||||
/// Default timeout (s) for establishing an agent connection.
|
||||
const DEFAULT_AGENT_CONNECT_TIMEOUT_SECS: u64 = 5;
|
||||
/// Default preview-activity withhold window. Sourced from the tracker's
|
||||
/// `PREVIEW_ACTIVITY_WINDOW_MS` so the two can't drift.
|
||||
const DEFAULT_PREVIEW_ACTIVITY_WINDOW_MS: u64 = crate::activity::PREVIEW_ACTIVITY_WINDOW_MS;
|
||||
/// Default preview-activity scrape cadence. Must stay below the withhold window.
|
||||
const DEFAULT_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS: u64 = 10_000;
|
||||
/// Smallest window that still leaves room for a strictly-smaller scrape; only a
|
||||
/// broken config reaches it (the normal window is 60s).
|
||||
const MIN_PREVIEW_ACTIVITY_WINDOW_MS: u64 = 2;
|
||||
/// Scrape-interval floor; `0` would busy-loop the scraper.
|
||||
const MIN_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS: u64 = 1;
|
||||
|
||||
/// Tunable timing/threshold constants for the workspace tool server.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StatusConfig {
|
||||
/// Interval between status/heartbeat emissions.
|
||||
pub heartbeat: Duration,
|
||||
/// Transport keepalive interval. Should exceed `heartbeat`.
|
||||
pub keepalive: Duration,
|
||||
/// WebSocket keepalive ping cadence for the server SDK connection.
|
||||
pub ws_ping: Duration,
|
||||
/// Reconnect backoff schedule for the server SDK connection. `None` leaves
|
||||
/// the SDK's built-in default exponential schedule in place.
|
||||
pub ws_reconnect_backoff: Option<Vec<Duration>>,
|
||||
/// Number of consecutive server reconnect failures before warning.
|
||||
pub hub_warn_threshold: u32,
|
||||
/// Base delay for exponential backoff on failed server event-notification sends.
|
||||
pub hub_backoff_base: Duration,
|
||||
/// Idle duration after which an inactive session is pruned.
|
||||
pub session_idle_prune: Duration,
|
||||
/// Legacy single-phase drain timeout (`KIGI_WORKSPACE_DRAIN_TIMEOUT_SECS`),
|
||||
/// retained for compatibility; the SIGTERM and server-evict paths now use the
|
||||
/// two-phase drain bounded by `KIGI_WORKSPACE_TERMINATION_GRACE_MS`.
|
||||
pub drain_timeout: Duration,
|
||||
/// Per-call timeout for agent RPCs.
|
||||
pub agent_rpc_timeout: Duration,
|
||||
/// Timeout for establishing an agent connection.
|
||||
pub agent_connect_timeout: Duration,
|
||||
/// Opt-in foreground-only idle (`KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS`);
|
||||
/// requires the literal `"true"` — other spellings fall back to this default.
|
||||
pub idle_ignores_background: bool,
|
||||
/// Recent preview-proxy traffic withholds idle for this window
|
||||
/// (`KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS`).
|
||||
pub preview_activity_window: Duration,
|
||||
/// Cadence at which the preview-activity scraper polls the proxy
|
||||
/// (`KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS`); kept strictly
|
||||
/// below `preview_activity_window` by [`validate`](Self::validate).
|
||||
pub preview_activity_scrape_interval: Duration,
|
||||
/// True when this container booted via the sandbox restore path, which
|
||||
/// injects `KIGI_SESSION_RESTORED=true`; a first boot never does.
|
||||
pub session_restored: bool,
|
||||
/// True when restore injects `KIGI_REVIVE_SCRIPT_CONFIGURED=true` (launchable
|
||||
/// revive configured); unset on first boot and non-launchable restores.
|
||||
pub revive_script_configured: bool,
|
||||
}
|
||||
|
||||
impl Default for StatusConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
heartbeat: Duration::from_secs(DEFAULT_HEARTBEAT_SECS),
|
||||
keepalive: Duration::from_secs(DEFAULT_KEEPALIVE_SECS),
|
||||
ws_ping: Duration::from_secs(DEFAULT_WS_PING_SECS),
|
||||
ws_reconnect_backoff: None,
|
||||
hub_warn_threshold: DEFAULT_HUB_WARN_THRESHOLD,
|
||||
hub_backoff_base: Duration::from_millis(DEFAULT_HUB_BACKOFF_BASE_MS),
|
||||
session_idle_prune: Duration::from_secs(DEFAULT_SESSION_IDLE_PRUNE_SECS),
|
||||
drain_timeout: Duration::from_secs(DEFAULT_DRAIN_TIMEOUT_SECS),
|
||||
agent_rpc_timeout: Duration::from_secs(DEFAULT_AGENT_RPC_TIMEOUT_SECS),
|
||||
agent_connect_timeout: Duration::from_secs(DEFAULT_AGENT_CONNECT_TIMEOUT_SECS),
|
||||
idle_ignores_background: false,
|
||||
preview_activity_window: Duration::from_millis(DEFAULT_PREVIEW_ACTIVITY_WINDOW_MS),
|
||||
preview_activity_scrape_interval: Duration::from_millis(
|
||||
DEFAULT_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS,
|
||||
),
|
||||
session_restored: false,
|
||||
revive_script_configured: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusConfig {
|
||||
/// Populate from `KIGI_WORKSPACE_*`. Unset or unparseable vars fall
|
||||
/// back to the default with a `warn!`. Never fails.
|
||||
pub fn from_env() -> Self {
|
||||
let defaults = Self::default();
|
||||
let (agent_rpc, agent_connect) = Self::agent_timeouts_from_env();
|
||||
let mut cfg = Self {
|
||||
heartbeat: secs_or("KIGI_WORKSPACE_HEARTBEAT_SECS", defaults.heartbeat),
|
||||
keepalive: secs_or("KIGI_WORKSPACE_KEEPALIVE_SECS", defaults.keepalive),
|
||||
ws_ping: secs_or("KIGI_WORKSPACE_WS_PING_SECS", defaults.ws_ping),
|
||||
ws_reconnect_backoff: backoff_schedule_from_env(
|
||||
"KIGI_WORKSPACE_WS_RECONNECT_BACKOFF_MS",
|
||||
),
|
||||
hub_warn_threshold: parse_or(
|
||||
"KIGI_WORKSPACE_HUB_WARN_THRESHOLD",
|
||||
defaults.hub_warn_threshold,
|
||||
),
|
||||
hub_backoff_base: ms_or(
|
||||
"KIGI_WORKSPACE_HUB_BACKOFF_BASE_MS",
|
||||
defaults.hub_backoff_base,
|
||||
),
|
||||
session_idle_prune: secs_or(
|
||||
"KIGI_WORKSPACE_SESSION_IDLE_PRUNE_SECS",
|
||||
defaults.session_idle_prune,
|
||||
),
|
||||
drain_timeout: secs_or("KIGI_WORKSPACE_DRAIN_TIMEOUT_SECS", defaults.drain_timeout),
|
||||
agent_rpc_timeout: agent_rpc,
|
||||
agent_connect_timeout: agent_connect,
|
||||
idle_ignores_background: parse_or(
|
||||
"KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS",
|
||||
defaults.idle_ignores_background,
|
||||
),
|
||||
preview_activity_window: ms_or(
|
||||
"KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS",
|
||||
defaults.preview_activity_window,
|
||||
),
|
||||
preview_activity_scrape_interval: ms_or(
|
||||
"KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS",
|
||||
defaults.preview_activity_scrape_interval,
|
||||
),
|
||||
session_restored: std::env::var("KIGI_SESSION_RESTORED").as_deref() == Ok("true"),
|
||||
revive_script_configured: std::env::var("KIGI_REVIVE_SCRIPT_CONFIGURED").as_deref()
|
||||
== Ok("true"),
|
||||
};
|
||||
cfg.validate();
|
||||
cfg
|
||||
}
|
||||
|
||||
/// Read only the agent gRPC `(request, connect)` timeouts from the
|
||||
/// environment, without parsing or validating the rest of the config.
|
||||
///
|
||||
/// Used by the btrfs delegate's env-based construction path, which has no
|
||||
/// `StatusConfig` in scope; reading just these two vars avoids re-running
|
||||
/// [`validate`](Self::validate) (and its possible duplicate `warn!`).
|
||||
pub fn agent_timeouts_from_env() -> (Duration, Duration) {
|
||||
let defaults = Self::default();
|
||||
const RPC_VAR: &str = "KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS";
|
||||
const CONNECT_VAR: &str = "KIGI_WORKSPACE_AGENT_CONNECT_TIMEOUT_SECS";
|
||||
(
|
||||
nonzero_secs_or(
|
||||
RPC_VAR,
|
||||
parse_or(RPC_VAR, defaults.agent_rpc_timeout.as_secs()),
|
||||
defaults.agent_rpc_timeout,
|
||||
),
|
||||
nonzero_secs_or(
|
||||
CONNECT_VAR,
|
||||
parse_or(CONNECT_VAR, defaults.agent_connect_timeout.as_secs()),
|
||||
defaults.agent_connect_timeout,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Warn on (and, where load-bearing, repair) inconsistent values.
|
||||
///
|
||||
/// `keepalive` can't be validated against the server's idle window (unknown
|
||||
/// here), so it only warns. The preview scraper, however, must run strictly
|
||||
/// more often than the withhold window (else the withhold lapses between
|
||||
/// scrapes) and never at a zero interval (which would busy-loop it), so any
|
||||
/// misconfiguration is repaired into `1ms <= scrape < window`.
|
||||
fn validate(&mut self) {
|
||||
if self.keepalive <= self.heartbeat {
|
||||
tracing::warn!(
|
||||
keepalive = ?self.keepalive,
|
||||
heartbeat = ?self.heartbeat,
|
||||
"KIGI_WORKSPACE keepalive <= heartbeat; transport may time out between heartbeats"
|
||||
);
|
||||
}
|
||||
let min_scrape = Duration::from_millis(MIN_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS);
|
||||
let window = self
|
||||
.preview_activity_window
|
||||
.max(Duration::from_millis(MIN_PREVIEW_ACTIVITY_WINDOW_MS));
|
||||
let scrape = self
|
||||
.preview_activity_scrape_interval
|
||||
.clamp(min_scrape, window - min_scrape);
|
||||
if window != self.preview_activity_window || scrape != self.preview_activity_scrape_interval
|
||||
{
|
||||
tracing::warn!(
|
||||
scrape_interval = ?self.preview_activity_scrape_interval,
|
||||
window = ?self.preview_activity_window,
|
||||
clamped_scrape = ?scrape,
|
||||
clamped_window = ?window,
|
||||
"KIGI_WORKSPACE preview scrape interval/window out of range; clamped to 1ms <= scrape < window"
|
||||
);
|
||||
self.preview_activity_window = window;
|
||||
self.preview_activity_scrape_interval = scrape;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read `var` and parse it as `T`. Returns `default` when unset; warns and
|
||||
/// returns `default` when present but unparseable.
|
||||
fn parse_or<T: FromStr>(var: &str, default: T) -> T {
|
||||
match std::env::var(var) {
|
||||
Err(_) => default,
|
||||
Ok(raw) => match raw.parse::<T>() {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
tracing::warn!(var, value = %raw, "Unparseable KIGI_WORKSPACE value; using default");
|
||||
default
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse `var` as a `u64` number of seconds into a [`Duration`].
|
||||
fn secs_or(var: &str, default: Duration) -> Duration {
|
||||
Duration::from_secs(parse_or(var, default.as_secs()))
|
||||
}
|
||||
|
||||
/// Parse `var` as a `u64` number of milliseconds into a [`Duration`].
|
||||
fn ms_or(var: &str, default: Duration) -> Duration {
|
||||
Duration::from_millis(parse_or(var, default.as_millis() as u64))
|
||||
}
|
||||
|
||||
/// Convert a parsed seconds value into a [`Duration`], rejecting `0`. A zero
|
||||
/// gRPC timeout makes every RPC fail immediately, so a configured `0` warns
|
||||
/// and falls back to `default` instead of being applied verbatim.
|
||||
fn nonzero_secs_or(var: &str, secs: u64, default: Duration) -> Duration {
|
||||
if secs == 0 {
|
||||
tracing::warn!(
|
||||
var,
|
||||
default = ?default,
|
||||
"KIGI_WORKSPACE agent timeout of 0s is invalid; using default"
|
||||
);
|
||||
return default;
|
||||
}
|
||||
Duration::from_secs(secs)
|
||||
}
|
||||
|
||||
/// Parse `var` as a comma-separated list of `u64` milliseconds into a reconnect
|
||||
/// backoff schedule. Returns `None` (keep the SDK's built-in default schedule)
|
||||
/// when the var is unset or yields no values, and warns + returns `None` when
|
||||
/// any element fails to parse.
|
||||
fn backoff_schedule_from_env(var: &str) -> Option<Vec<Duration>> {
|
||||
let raw = std::env::var(var).ok()?;
|
||||
let mut schedule = Vec::new();
|
||||
for part in raw.split(',').map(str::trim).filter(|p| !p.is_empty()) {
|
||||
match part.parse::<u64>() {
|
||||
Ok(ms) => schedule.push(Duration::from_millis(ms)),
|
||||
Err(_) => {
|
||||
tracing::warn!(
|
||||
var,
|
||||
value = %raw,
|
||||
"Unparseable KIGI_WORKSPACE backoff schedule; using SDK default"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
(!schedule.is_empty()).then_some(schedule)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Crate-shared env lock: every test that mutates the process environment
|
||||
// holds it for its full duration. ONE lock for the whole crate because the
|
||||
// hazard is the global `environ` array under `unsafe set_var` (not the
|
||||
// variable's value), so even disjoint vars must serialize.
|
||||
use crate::ENV_TEST_LOCK as ENV_LOCK;
|
||||
|
||||
#[test]
|
||||
fn defaults_match_documented_values() {
|
||||
let cfg = StatusConfig::default();
|
||||
assert_eq!(cfg.heartbeat, Duration::from_secs(30));
|
||||
assert_eq!(cfg.keepalive, Duration::from_secs(60));
|
||||
assert_eq!(cfg.ws_ping, Duration::from_secs(30));
|
||||
assert_eq!(cfg.ws_reconnect_backoff, None);
|
||||
assert_eq!(cfg.hub_warn_threshold, 5);
|
||||
assert_eq!(cfg.hub_backoff_base, Duration::from_millis(100));
|
||||
assert_eq!(cfg.session_idle_prune, Duration::from_secs(1800));
|
||||
assert_eq!(cfg.drain_timeout, Duration::from_secs(30));
|
||||
assert_eq!(cfg.agent_rpc_timeout, Duration::from_secs(30));
|
||||
assert_eq!(cfg.agent_connect_timeout, Duration::from_secs(5));
|
||||
assert!(!cfg.idle_ignores_background);
|
||||
assert_eq!(cfg.preview_activity_window, Duration::from_secs(60));
|
||||
assert_eq!(
|
||||
cfg.preview_activity_scrape_interval,
|
||||
Duration::from_secs(10)
|
||||
);
|
||||
assert!(!cfg.session_restored);
|
||||
assert!(!cfg.revive_script_configured);
|
||||
}
|
||||
|
||||
/// `parse_or` returns the default when the variable is unset. Uses a
|
||||
/// uniquely-named var so it never collides with other tests' env writes.
|
||||
#[test]
|
||||
fn parse_or_unset_returns_default() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_PARSE_OR_UNSET";
|
||||
unsafe { std::env::remove_var(var) };
|
||||
assert_eq!(parse_or::<u32>(var, 5), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_or_valid_parses() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_PARSE_OR_VALID";
|
||||
unsafe { std::env::set_var(var, "42") };
|
||||
assert_eq!(parse_or::<u32>(var, 5), 42);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_or_invalid_falls_back_without_panic() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_PARSE_OR_INVALID";
|
||||
unsafe { std::env::set_var(var, "not-a-number") };
|
||||
assert_eq!(parse_or::<u32>(var, 5), 5);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secs_or_parses_into_duration() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_SECS_OR_VALID";
|
||||
unsafe { std::env::set_var(var, "120") };
|
||||
assert_eq!(
|
||||
secs_or(var, Duration::from_secs(30)),
|
||||
Duration::from_secs(120)
|
||||
);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secs_or_unset_returns_default() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_SECS_OR_UNSET";
|
||||
unsafe { std::env::remove_var(var) };
|
||||
assert_eq!(
|
||||
secs_or(var, Duration::from_secs(30)),
|
||||
Duration::from_secs(30)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secs_or_invalid_falls_back_without_panic() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_SECS_OR_INVALID";
|
||||
unsafe { std::env::set_var(var, "12.5") };
|
||||
assert_eq!(
|
||||
secs_or(var, Duration::from_secs(30)),
|
||||
Duration::from_secs(30)
|
||||
);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ms_or_parses_into_duration() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_MS_OR_VALID";
|
||||
unsafe { std::env::set_var(var, "250") };
|
||||
assert_eq!(
|
||||
ms_or(var, Duration::from_millis(100)),
|
||||
Duration::from_millis(250)
|
||||
);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ms_or_invalid_falls_back_without_panic() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_MS_OR_INVALID";
|
||||
unsafe { std::env::set_var(var, "abc") };
|
||||
assert_eq!(
|
||||
ms_or(var, Duration::from_millis(100)),
|
||||
Duration::from_millis(100)
|
||||
);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
|
||||
/// With none of the `KIGI_WORKSPACE_*` vars set, `from_env` reproduces
|
||||
/// `StatusConfig::default()` field-for-field.
|
||||
///
|
||||
/// This is the one test that touches the real (non-`_TEST_`-prefixed)
|
||||
/// var names: it `remove_var`s all of them before reading them. If a future
|
||||
/// test sets these shared names, run them serialized to avoid a race.
|
||||
#[test]
|
||||
fn from_env_clean_matches_default() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for var in [
|
||||
"KIGI_WORKSPACE_HEARTBEAT_SECS",
|
||||
"KIGI_WORKSPACE_KEEPALIVE_SECS",
|
||||
"KIGI_WORKSPACE_WS_PING_SECS",
|
||||
"KIGI_WORKSPACE_WS_RECONNECT_BACKOFF_MS",
|
||||
"KIGI_WORKSPACE_HUB_WARN_THRESHOLD",
|
||||
"KIGI_WORKSPACE_HUB_BACKOFF_BASE_MS",
|
||||
"KIGI_WORKSPACE_SESSION_IDLE_PRUNE_SECS",
|
||||
"KIGI_WORKSPACE_DRAIN_TIMEOUT_SECS",
|
||||
"KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS",
|
||||
"KIGI_WORKSPACE_AGENT_CONNECT_TIMEOUT_SECS",
|
||||
"KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS",
|
||||
"KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS",
|
||||
"KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS",
|
||||
"KIGI_SESSION_RESTORED",
|
||||
"KIGI_REVIVE_SCRIPT_CONFIGURED",
|
||||
] {
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
let cfg = StatusConfig::from_env();
|
||||
let default = StatusConfig::default();
|
||||
assert_eq!(cfg.heartbeat, default.heartbeat);
|
||||
assert_eq!(cfg.keepalive, default.keepalive);
|
||||
assert_eq!(cfg.ws_ping, default.ws_ping);
|
||||
assert_eq!(cfg.ws_reconnect_backoff, default.ws_reconnect_backoff);
|
||||
assert_eq!(cfg.hub_warn_threshold, default.hub_warn_threshold);
|
||||
assert_eq!(cfg.hub_backoff_base, default.hub_backoff_base);
|
||||
assert_eq!(cfg.session_idle_prune, default.session_idle_prune);
|
||||
assert_eq!(cfg.drain_timeout, default.drain_timeout);
|
||||
assert_eq!(cfg.agent_rpc_timeout, default.agent_rpc_timeout);
|
||||
assert_eq!(cfg.agent_connect_timeout, default.agent_connect_timeout);
|
||||
assert_eq!(cfg.idle_ignores_background, default.idle_ignores_background);
|
||||
assert_eq!(cfg.preview_activity_window, default.preview_activity_window);
|
||||
assert_eq!(
|
||||
cfg.preview_activity_scrape_interval,
|
||||
default.preview_activity_scrape_interval
|
||||
);
|
||||
assert_eq!(cfg.session_restored, default.session_restored);
|
||||
assert_eq!(
|
||||
cfg.revive_script_configured,
|
||||
default.revive_script_configured
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_session_restored_true_only() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unsafe { std::env::set_var("KIGI_SESSION_RESTORED", "true") };
|
||||
let restored = StatusConfig::from_env().session_restored;
|
||||
unsafe { std::env::set_var("KIGI_SESSION_RESTORED", "1") };
|
||||
let non_canonical = StatusConfig::from_env().session_restored;
|
||||
unsafe { std::env::remove_var("KIGI_SESSION_RESTORED") };
|
||||
assert!(restored);
|
||||
assert!(!non_canonical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_revive_script_configured_true_only() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unsafe { std::env::set_var("KIGI_REVIVE_SCRIPT_CONFIGURED", "true") };
|
||||
let configured = StatusConfig::from_env().revive_script_configured;
|
||||
unsafe { std::env::set_var("KIGI_REVIVE_SCRIPT_CONFIGURED", "1") };
|
||||
let non_canonical = StatusConfig::from_env().revive_script_configured;
|
||||
unsafe { std::env::remove_var("KIGI_REVIVE_SCRIPT_CONFIGURED") };
|
||||
assert!(configured);
|
||||
assert!(!non_canonical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_idle_ignore_background_true() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unsafe { std::env::set_var("KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS", "true") };
|
||||
let cfg = StatusConfig::from_env();
|
||||
unsafe { std::env::remove_var("KIGI_WORKSPACE_IDLE_IGNORE_BACKGROUND_TASKS") };
|
||||
assert!(cfg.idle_ignores_background);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_preview_activity_window() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unsafe { std::env::set_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS", "120000") };
|
||||
let cfg = StatusConfig::from_env();
|
||||
unsafe { std::env::remove_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_WINDOW_MS") };
|
||||
assert_eq!(cfg.preview_activity_window, Duration::from_millis(120_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_env_reads_preview_activity_scrape_interval() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unsafe { std::env::set_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS", "5000") };
|
||||
let cfg = StatusConfig::from_env();
|
||||
unsafe { std::env::remove_var("KIGI_WORKSPACE_PREVIEW_ACTIVITY_SCRAPE_INTERVAL_MS") };
|
||||
assert_eq!(
|
||||
cfg.preview_activity_scrape_interval,
|
||||
Duration::from_millis(5_000)
|
||||
);
|
||||
}
|
||||
|
||||
/// `validate` must not panic regardless of the relative ordering of
|
||||
/// `keepalive` and `heartbeat`.
|
||||
#[test]
|
||||
fn validate_does_not_panic_when_keepalive_le_heartbeat() {
|
||||
let mut cfg = StatusConfig {
|
||||
keepalive: Duration::from_secs(10),
|
||||
heartbeat: Duration::from_secs(30),
|
||||
..StatusConfig::default()
|
||||
};
|
||||
cfg.validate();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_clamps_preview_scrape_into_valid_range() {
|
||||
for (window_ms, scrape_ms, exp_window_ms, exp_scrape_ms) in [
|
||||
(1_000u64, 4_000u64, 1_000u64, 999u64),
|
||||
(1_000, 1_000, 1_000, 999),
|
||||
(1_000, 200, 1_000, 200),
|
||||
(60_000, 0, 60_000, 1),
|
||||
(0, 0, 2, 1),
|
||||
(1, 5, 2, 1),
|
||||
] {
|
||||
let mut cfg = StatusConfig {
|
||||
preview_activity_window: Duration::from_millis(window_ms),
|
||||
preview_activity_scrape_interval: Duration::from_millis(scrape_ms),
|
||||
..StatusConfig::default()
|
||||
};
|
||||
cfg.validate();
|
||||
assert_eq!(
|
||||
cfg.preview_activity_window,
|
||||
Duration::from_millis(exp_window_ms)
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.preview_activity_scrape_interval,
|
||||
Duration::from_millis(exp_scrape_ms)
|
||||
);
|
||||
assert!(cfg.preview_activity_scrape_interval >= Duration::from_millis(1));
|
||||
assert!(cfg.preview_activity_scrape_interval < cfg.preview_activity_window);
|
||||
}
|
||||
}
|
||||
|
||||
/// A configured agent timeout of `0` seconds is invalid (it would make
|
||||
/// every gRPC call fail immediately), so `nonzero_secs_or` falls back to
|
||||
/// the supplied default instead of returning `Duration::ZERO`.
|
||||
#[test]
|
||||
fn nonzero_secs_or_zero_falls_back_to_default() {
|
||||
assert_eq!(
|
||||
nonzero_secs_or(
|
||||
"KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS",
|
||||
0,
|
||||
Duration::from_secs(30)
|
||||
),
|
||||
Duration::from_secs(30)
|
||||
);
|
||||
assert_eq!(
|
||||
nonzero_secs_or(
|
||||
"KIGI_WORKSPACE_AGENT_CONNECT_TIMEOUT_SECS",
|
||||
0,
|
||||
Duration::from_secs(5)
|
||||
),
|
||||
Duration::from_secs(5)
|
||||
);
|
||||
}
|
||||
|
||||
/// A positive value is honored verbatim.
|
||||
#[test]
|
||||
fn nonzero_secs_or_positive_is_passed_through() {
|
||||
assert_eq!(
|
||||
nonzero_secs_or(
|
||||
"KIGI_WORKSPACE_AGENT_RPC_TIMEOUT_SECS",
|
||||
12,
|
||||
Duration::from_secs(30)
|
||||
),
|
||||
Duration::from_secs(12)
|
||||
);
|
||||
}
|
||||
|
||||
/// An unset backoff var leaves the schedule unconfigured (`None`), so the
|
||||
/// SDK keeps its built-in default.
|
||||
#[test]
|
||||
fn backoff_schedule_unset_returns_none() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_BACKOFF_UNSET";
|
||||
unsafe { std::env::remove_var(var) };
|
||||
assert_eq!(backoff_schedule_from_env(var), None);
|
||||
}
|
||||
|
||||
/// A valid comma-separated list parses into millisecond `Duration`s in
|
||||
/// order, tolerating surrounding whitespace.
|
||||
#[test]
|
||||
fn backoff_schedule_valid_list_parses_in_order() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_BACKOFF_VALID";
|
||||
unsafe { std::env::set_var(var, "100, 200,500,1000") };
|
||||
assert_eq!(
|
||||
backoff_schedule_from_env(var),
|
||||
Some(vec![
|
||||
Duration::from_millis(100),
|
||||
Duration::from_millis(200),
|
||||
Duration::from_millis(500),
|
||||
Duration::from_millis(1000),
|
||||
])
|
||||
);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
|
||||
/// A malformed element makes the whole schedule fall back to `None` (and
|
||||
/// warns) rather than silently dropping entries.
|
||||
#[test]
|
||||
fn backoff_schedule_malformed_returns_none() {
|
||||
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let var = "KIGI_WORKSPACE_TEST_BACKOFF_MALFORMED";
|
||||
unsafe { std::env::set_var(var, "100,not-a-number,500") };
|
||||
assert_eq!(backoff_schedule_from_env(var), None);
|
||||
unsafe { std::env::remove_var(var) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user