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,124 @@
|
||||
//! Cross-suite e2e flow helpers over [`PtyHarness`] / [`ContentController`].
|
||||
//!
|
||||
//! The single canonical home for driving/seeding helpers shared by the
|
||||
//! pager's `pty_e2e` and `leader_pty_e2e` test targets (both depend on this
|
||||
//! crate); suite-local constants (sizes, sentinels, timeouts) stay in each
|
||||
//! suite's `common.rs`.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::{ContentController, PtyHarness};
|
||||
|
||||
/// Pump PTY output until every label is absent from the visible screen.
|
||||
pub fn wait_for_labels_absent(h: &mut PtyHarness, labels: &[&str], timeout: Duration) {
|
||||
let _ = h.wait_until("screen labels to disappear", timeout, |h| {
|
||||
labels.iter().all(|label| !h.contains_text(label))
|
||||
});
|
||||
}
|
||||
|
||||
/// Submit `prompt` from `h`, then keep re-pressing Enter until the turn
|
||||
/// actually starts streaming (`sentinel` appears) or `timeout` elapses.
|
||||
///
|
||||
/// In a heavy multi-client leader cluster the driver's submit Enter can be
|
||||
/// dropped when it races the other client attaching / replaying on the shared
|
||||
/// leader: the typed prompt is left sitting unsubmitted in the composer, the
|
||||
/// turn never starts, and a plain `wait_for_text` then times out (the observed
|
||||
/// `leader_two_clients_shared_session` flake — A idle with `again` still in the
|
||||
/// composer at 75s). Re-pressing Enter is safe and idempotent: submitting takes
|
||||
/// the composer draft synchronously (`std::mem::take` in `dispatch`), so once a
|
||||
/// turn has really been sent the composer is empty and an extra Enter is a
|
||||
/// no-op. It can only submit a still-stuck prompt, never double-submit a sent
|
||||
/// one (which would break exactly-once scrollback asserts).
|
||||
pub fn submit_turn(h: &mut PtyHarness, prompt: &str, sentinel: &str, timeout: Duration) {
|
||||
h.inject_keys(format!("{prompt}\r").as_bytes())
|
||||
.expect("inject prompt submit");
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
// Per-attempt sub-budget, generous enough that a genuinely in-flight
|
||||
// submit resolves before we re-nudge (so the re-nudge only ever fires
|
||||
// on an empty composer, where it is a no-op).
|
||||
if h.wait_for_text(sentinel, Duration::from_secs(10).min(remaining))
|
||||
.is_ok()
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"timed out after {timeout:?} waiting for {sentinel:?}\nscreen:\n{}",
|
||||
h.screen_contents()
|
||||
);
|
||||
let _ = h.inject_keys(b"\r");
|
||||
}
|
||||
}
|
||||
|
||||
/// Count only inference requests (chat completions / responses / messages),
|
||||
/// ignoring incidental GETs like /v1/models and /v1/settings, so a replay
|
||||
/// invariant means "no turn was re-driven" rather than "no HTTP at all".
|
||||
pub fn inference_request_count(content: &ContentController) -> usize {
|
||||
content
|
||||
.requests()
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
e.path.contains("/chat/completions")
|
||||
|| e.path.contains("/responses")
|
||||
|| e.path.contains("/messages")
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Seed a fake xAI OAuth entry into the isolated home's `auth.json` so the
|
||||
/// shell has session auth (the harness's `XAI_API_KEY` is ApiKey/BYOK mode
|
||||
/// and never enters the auth manager). Load-bearing details: the scope key
|
||||
/// must be `<issuer>::<client_id>`, `auth_mode` must be `oidc`, and
|
||||
/// `expires_at` must be far-future so no network refresh is attempted; the
|
||||
/// mock server accepts any bearer. Pair with [`oauth_env_for_pager`].
|
||||
pub fn seed_fake_oauth(content: &ContentController, user: &str) {
|
||||
let kigi_home = content.home().join(".kigi");
|
||||
std::fs::create_dir_all(&kigi_home).expect("create temp .kigi");
|
||||
std::fs::write(
|
||||
kigi_home.join("auth.json"),
|
||||
format!(
|
||||
r#"{{
|
||||
"https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828": {{
|
||||
"key": "pty-test-oauth-token",
|
||||
"auth_mode": "oidc",
|
||||
"create_time": "2026-01-01T00:00:00Z",
|
||||
"user_id": "{user}",
|
||||
"email": "{user}@test.invalid",
|
||||
"expires_at": "2030-01-01T00:00:00Z",
|
||||
"refresh_token": "pty-test-refresh-token",
|
||||
"oidc_issuer": "https://auth.x.ai",
|
||||
"oidc_client_id": "b1a00492-073a-47ea-816f-4c329264a828"
|
||||
}}
|
||||
}}"#
|
||||
),
|
||||
)
|
||||
.expect("seed fake oauth auth.json");
|
||||
}
|
||||
|
||||
/// [`ContentController::env_for_pager`] minus `XAI_API_KEY`, so the entry
|
||||
/// written by [`seed_fake_oauth`] is the active credential.
|
||||
pub fn oauth_env_for_pager(content: &ContentController) -> Vec<(String, String)> {
|
||||
let mut env = content.env_for_pager();
|
||||
env.retain(|(k, _)| k != "XAI_API_KEY");
|
||||
env
|
||||
}
|
||||
|
||||
/// Drive `/new` until `model` shows on screen. Campaigns apply to **new
|
||||
/// sessions only** and the pager's settings prefetch is deliberately 2s-capped,
|
||||
/// so on a loaded runner the first session can legitimately open pre-campaign;
|
||||
/// each `/new` after the settings fetch lands re-resolves with the campaign.
|
||||
pub fn wait_for_model_via_new_sessions(h: &mut PtyHarness, model: &str, timeout: Duration) -> bool {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
if h.contains_text(model) {
|
||||
return true;
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
let _ = h.inject_keys(b"/new\r");
|
||||
h.update(Duration::from_millis(3000));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user