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,242 @@
|
||||
//! E2E: a submitted prompt is durably recorded in the per-CWD
|
||||
//! `prompt_history.jsonl` and survives quitting the TUI — via a fast double
|
||||
//! Ctrl+C (the reported repro, recalled after a `--continue` resume) and via a
|
||||
//! real OS SIGINT routed through the same graceful quit.
|
||||
//!
|
||||
//! Drives the real pager binary through a PTY against the shared mock
|
||||
//! inference server (isolated `$HOME`), exercising the full
|
||||
//! pager -> shell -> queue_input -> append path plus the graceful-quit teardown.
|
||||
//!
|
||||
//! Coverage note: both paths wait for the turn to land before quitting, so
|
||||
//! `queue_input` (and its now-awaited append) has already run. This is an
|
||||
//! end-to-end durability + recall check, not a probe of the old detached-append
|
||||
//! race — that race is closed structurally by awaiting the append in
|
||||
//! `queue_input` and is covered by the `prompt_history` unit test. The
|
||||
//! deterministic regression catch here is the SIGINT path exiting 0 (pre-fix it
|
||||
//! was `process::exit(130)`).
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo test -p kigi-pager-pty-harness --test prompt_history_durable_quit \
|
||||
//! -- --ignored --nocapture
|
||||
//! ```
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use kigi_pager_pty_harness::{ContentController, PtyHarness, keys, pager_binary};
|
||||
|
||||
const ROWS: u16 = 50;
|
||||
const COLS: u16 = 120;
|
||||
const CANARY: &str = "REGRESSIONCANARY42";
|
||||
#[cfg(unix)]
|
||||
const SIGINT_CANARY: &str = "SIGINTCANARY7";
|
||||
const ACK: &str = "ACKSENTINEL";
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore] // opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
|
||||
async fn prompt_history_durable_after_double_ctrl_c_and_recallable_on_resume() {
|
||||
run().await.expect("prompt-history durable-quit e2e");
|
||||
}
|
||||
|
||||
/// A real OS SIGINT (not an injected Ctrl+C key byte) must route through the
|
||||
/// same graceful quit: the prompt stays durable and the process exits 0.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore] // opt-in: spawns the real pager binary in a PTY (CI runs with --ignored)
|
||||
async fn prompt_history_durable_after_real_sigint_graceful_quit() {
|
||||
run_sigint().await.expect("sigint graceful-quit e2e");
|
||||
}
|
||||
|
||||
async fn run() -> Result<()> {
|
||||
let content = ContentController::start()
|
||||
.await
|
||||
.context("start mock server")?;
|
||||
content.set_response(format!("{ACK} acknowledged."));
|
||||
|
||||
let project = tempfile::tempdir().context("project dir")?;
|
||||
std::fs::create_dir_all(project.path().join(".git")).context("create .git")?;
|
||||
|
||||
let binary = pager_binary().context("resolve pager binary")?;
|
||||
|
||||
// 1) Submit a prompt and let the turn settle (proves the shell reached
|
||||
// queue_input, where the append happens), then quit with double Ctrl+C.
|
||||
let mut first = submit_and_settle(&binary, &content, project.path(), CANARY)
|
||||
.context("submit canary in first pager")?;
|
||||
|
||||
// Double Ctrl+C: first arms the quit confirmation on the empty prompt, the
|
||||
// second confirms -> same graceful Action::Quit as `/exit`.
|
||||
let pre = first.raw_output().len();
|
||||
first.inject_keys(keys::CTRL_C).context("ctrl-c arm")?;
|
||||
first.update(Duration::from_millis(250));
|
||||
first.inject_keys(keys::CTRL_C).context("ctrl-c confirm")?;
|
||||
|
||||
// Drain output until the child exits so the post-`pre` suffix holds the full
|
||||
// graceful teardown (incl. the show-cursor restore) for the assertions below.
|
||||
first.update(Duration::from_secs(10));
|
||||
|
||||
let code = first.wait_exit_code(Duration::from_secs(10));
|
||||
assert_eq!(
|
||||
code,
|
||||
Some(0),
|
||||
"double Ctrl+C should exit via the graceful quit (exit 0), got {code:?}"
|
||||
);
|
||||
assert!(
|
||||
terminal_restored(&first, pre),
|
||||
"terminal not restored (no show-cursor after quit) on the double-Ctrl+C path"
|
||||
);
|
||||
drop(first);
|
||||
|
||||
// 2) Durability: the prompt must be on disk after the quit.
|
||||
assert_prompt_durable(content.home(), CANARY)?;
|
||||
|
||||
// 3) Recall across restart: `--continue` resumes the session and replays it.
|
||||
let mut resumed = PtyHarness::spawn_with_content_in_dir(
|
||||
&binary,
|
||||
ROWS,
|
||||
COLS,
|
||||
&content,
|
||||
&["--continue"],
|
||||
Some(project.path()),
|
||||
)
|
||||
.context("spawn resumed pager")?;
|
||||
resumed
|
||||
.wait_for_text(CANARY, Duration::from_secs(20))
|
||||
.context("resumed session replayed the prior prompt")?;
|
||||
assert!(
|
||||
!resumed.contains_text("panicked"),
|
||||
"pager panicked on resume:\n{}",
|
||||
resumed.screen_contents()
|
||||
);
|
||||
|
||||
// Up-arrow opens the history overlay; smoke-check it doesn't crash and the
|
||||
// recalled prompt stays reachable.
|
||||
resumed.inject_keys(keys::UP).context("press Up")?;
|
||||
resumed.update(Duration::from_millis(500));
|
||||
assert!(
|
||||
resumed.contains_text(CANARY),
|
||||
"canary not reachable after Up:\n{}",
|
||||
resumed.screen_contents()
|
||||
);
|
||||
assert!(
|
||||
!resumed.contains_text("panicked"),
|
||||
"pager panicked after Up:\n{}",
|
||||
resumed.screen_contents()
|
||||
);
|
||||
|
||||
resumed.quit().context("quit resumed pager")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Real-SIGINT variant of [`run`]: deliver an OS signal to the pager child and
|
||||
/// assert the same graceful-quit outcome (durable prompt, clean exit).
|
||||
#[cfg(unix)]
|
||||
async fn run_sigint() -> Result<()> {
|
||||
let content = ContentController::start()
|
||||
.await
|
||||
.context("start mock server")?;
|
||||
content.set_response(format!("{ACK} acknowledged."));
|
||||
|
||||
let project = tempfile::tempdir().context("project dir")?;
|
||||
std::fs::create_dir_all(project.path().join(".git")).context("create .git")?;
|
||||
|
||||
let binary = pager_binary().context("resolve pager binary")?;
|
||||
|
||||
let mut first = submit_and_settle(&binary, &content, project.path(), SIGINT_CANARY)
|
||||
.context("submit canary before SIGINT")?;
|
||||
|
||||
// A real SIGINT, not an injected 0x03 key byte (raw mode delivers that as a
|
||||
// key event — the double-Ctrl+C path above), drives the OS-signal path.
|
||||
let pre = first.raw_output().len();
|
||||
first.send_signal(libc::SIGINT).context("send SIGINT")?;
|
||||
|
||||
// Drain output until the child exits so the post-`pre` suffix holds the full
|
||||
// graceful teardown (incl. the show-cursor restore) for the assertions below.
|
||||
first.update(Duration::from_secs(10));
|
||||
|
||||
// Pre-fix the SIGINT handler called std::process::exit(130); routing it
|
||||
// through the graceful quit exits 0 — the deterministic Part-B regression catch.
|
||||
let code = first.wait_exit_code(Duration::from_secs(10));
|
||||
assert_eq!(
|
||||
code,
|
||||
Some(0),
|
||||
"real SIGINT should route through the graceful quit (exit 0), got {code:?}"
|
||||
);
|
||||
assert!(
|
||||
terminal_restored(&first, pre),
|
||||
"terminal not restored (no show-cursor after SIGINT) on the SIGINT path"
|
||||
);
|
||||
drop(first);
|
||||
|
||||
// Durability: the prompt must be on disk despite the signal-driven quit.
|
||||
assert_prompt_durable(content.home(), SIGINT_CANARY)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn the pager in `project`, submit `canary`, then wait for the turn to
|
||||
/// render + settle to idle. Waiting past `queue_input` is deliberate: it
|
||||
/// guarantees the prompt reached the shell (so the durability assertion is
|
||||
/// meaningful), at the cost of not reproducing the sub-millisecond
|
||||
/// detached-append race (closed by awaiting the append; see the unit test).
|
||||
fn submit_and_settle(
|
||||
binary: &Path,
|
||||
content: &ContentController,
|
||||
project: &Path,
|
||||
canary: &str,
|
||||
) -> Result<PtyHarness> {
|
||||
let mut pager =
|
||||
PtyHarness::spawn_with_content_in_dir(binary, ROWS, COLS, content, &[], Some(project))
|
||||
.context("spawn pager")?;
|
||||
pager
|
||||
.wait_for_text("Quit", Duration::from_secs(20))
|
||||
.context("welcome screen")?;
|
||||
pager
|
||||
.inject_keys(canary.as_bytes())
|
||||
.context("type canary")?;
|
||||
pager.inject_keys(keys::ENTER).context("submit prompt")?;
|
||||
pager
|
||||
.wait_for_text(ACK, Duration::from_secs(30))
|
||||
.context("turn response rendered")?;
|
||||
pager.update(Duration::from_millis(1000)); // let the short turn finish (idle)
|
||||
Ok(pager)
|
||||
}
|
||||
|
||||
/// Whether the pager emitted the show-cursor restore (`ESC [ ?25h`) after byte
|
||||
/// offset `since`. Scanning only the post-quit suffix avoids matching the
|
||||
/// show-cursor that normal rendering emits mid-session.
|
||||
fn terminal_restored(h: &PtyHarness, since: usize) -> bool {
|
||||
const SHOW_CURSOR: &[u8] = b"\x1b[?25h";
|
||||
let raw = h.raw_output();
|
||||
raw[since.min(raw.len())..]
|
||||
.windows(SHOW_CURSOR.len())
|
||||
.any(|w| w == SHOW_CURSOR)
|
||||
}
|
||||
|
||||
/// Assert the per-CWD `prompt_history.jsonl` durably recorded `canary`.
|
||||
fn assert_prompt_durable(home: &Path, canary: &str) -> Result<()> {
|
||||
let hist = find_prompt_history(home).context("locate prompt_history.jsonl")?;
|
||||
let body =
|
||||
std::fs::read_to_string(&hist).with_context(|| format!("read {}", hist.display()))?;
|
||||
eprintln!("[e2e] prompt_history.jsonl @ {}:\n{body}", hist.display());
|
||||
assert!(
|
||||
body.contains(canary),
|
||||
"prompt_history.jsonl is missing the submitted prompt after the quit:\n{body}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The per-CWD history file lives at `<home>/.kigi/sessions/<enc-cwd>/prompt_history.jsonl`.
|
||||
fn find_prompt_history(home: &Path) -> Result<PathBuf> {
|
||||
let root = home.join(".kigi").join("sessions");
|
||||
for cwd_ent in std::fs::read_dir(&root).with_context(|| format!("read {}", root.display()))? {
|
||||
let cwd_ent = cwd_ent?;
|
||||
if !cwd_ent.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let candidate = cwd_ent.path().join("prompt_history.jsonl");
|
||||
if candidate.is_file() {
|
||||
return Ok(candidate);
|
||||
}
|
||||
}
|
||||
bail!("no prompt_history.jsonl found under {}", root.display())
|
||||
}
|
||||
Reference in New Issue
Block a user