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).
279 lines
9.4 KiB
Rust
279 lines
9.4 KiB
Rust
//! Replay-trace verification harness for the turn-end TodoGate.
|
|
//!
|
|
//! Reads synthetic JSON fixtures from `tests/fixtures/synthetic_*.json`,
|
|
//! walks each turn, and at every assistant `end-of-turn` snapshot asserts
|
|
//! that `evaluate_todo_gate` returns the decision the fixture declares.
|
|
//!
|
|
//! Pure-function integration test — no `SessionActor`, no completion
|
|
//! stream. Complements unit tests of the pure function with
|
|
//! data-driven trace-replay coverage.
|
|
//!
|
|
//! Data-driven: dropping a new `synthetic_*.json` fixture into
|
|
//! `tests/fixtures/` enrolls it in the harness automatically.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use serde::Deserialize;
|
|
|
|
use kigi_shell::session::{
|
|
CollectedTodoGateInput, TodoGateDecision, TodoGateReason, evaluate_todo_gate,
|
|
};
|
|
use kigi_shell::tools::todo::TodoStatus;
|
|
|
|
/// Closed canonical set of shipped fixtures. Adding a fixture
|
|
/// here is the one required Rust-side change when a new failure shape
|
|
/// is enrolled — `canonical_fixtures_present` asserts set equality
|
|
/// against this list so a missing-or-extra fixture fails the harness.
|
|
const CANONICAL_FIXTURES: &[&str] = &[
|
|
"synthetic_clean_completion.json",
|
|
"synthetic_pr_babysit_partial_backing.json",
|
|
"synthetic_stranded_narration.json",
|
|
];
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct Fixture {
|
|
name: String,
|
|
#[allow(dead_code)] // human-readable; surfaced only on assertion failure
|
|
description: String,
|
|
turns: Vec<Turn>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
enum Turn {
|
|
#[allow(dead_code)] // user turns are walked but never gate-evaluated
|
|
User(UserTurn),
|
|
Assistant(AssistantTurn),
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct UserTurn {
|
|
turn_index: usize,
|
|
content: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct AssistantTurn {
|
|
turn_index: usize,
|
|
#[allow(dead_code)] // present for fixture clarity; the gate does not consult it
|
|
tool_calls_emitted: Vec<serde_json::Value>,
|
|
todo_state_after_turn: Vec<TodoSnapshot>,
|
|
backing_task_count: usize,
|
|
expected_gate_decision: ExpectedGateDecision,
|
|
#[serde(default)]
|
|
expected_reason: Option<ExpectedReason>,
|
|
#[serde(default)]
|
|
expected_reminder_contains: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct TodoSnapshot {
|
|
#[allow(dead_code)] // id is preserved for fixture readability
|
|
id: String,
|
|
status: TodoStatus,
|
|
content: String,
|
|
}
|
|
|
|
/// Typed mirror of the fixture's `expected_gate_decision` field. A
|
|
/// closed enum (not `String`) catches typos at deserialize time rather
|
|
/// than silently passing the wrong assertion branch.
|
|
#[derive(Debug, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
enum ExpectedGateDecision {
|
|
Nudge,
|
|
Continue,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
enum ExpectedReason {
|
|
InFlight,
|
|
}
|
|
|
|
impl From<TodoGateReason> for ExpectedReason {
|
|
fn from(reason: TodoGateReason) -> Self {
|
|
match reason {
|
|
TodoGateReason::InFlight => Self::InFlight,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fixtures_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
|
|
}
|
|
|
|
/// Every `synthetic_*.json` in `tests/fixtures/`, sorted for stable
|
|
/// output. Directory-iteration IO errors panic with diagnostic context
|
|
/// rather than being silently dropped (a permissioned-out fixture must
|
|
/// not vanish from the set unnoticed).
|
|
fn discover_fixtures() -> Vec<PathBuf> {
|
|
let dir = fixtures_dir();
|
|
let entries = std::fs::read_dir(&dir)
|
|
.unwrap_or_else(|e| panic!("read fixtures dir {}: {e}", dir.display()));
|
|
let mut paths: Vec<PathBuf> = entries
|
|
.map(|entry| {
|
|
entry
|
|
.unwrap_or_else(|e| panic!("read entry in {}: {e}", dir.display()))
|
|
.path()
|
|
})
|
|
.filter(|path| {
|
|
path.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.is_some_and(|name| name.starts_with("synthetic_") && name.ends_with(".json"))
|
|
})
|
|
.collect();
|
|
paths.sort();
|
|
assert!(
|
|
!paths.is_empty(),
|
|
"no synthetic_*.json fixtures found in {}",
|
|
dir.display()
|
|
);
|
|
paths
|
|
}
|
|
|
|
fn load_fixture(path: &Path) -> Fixture {
|
|
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
|
|
serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()))
|
|
}
|
|
|
|
/// Consume a fixture's assistant-turn snapshot into a
|
|
/// `CollectedTodoGateInput`. Moves todo `content` strings out of the
|
|
/// snapshot — no clones. Calling `as_input()` on the result also
|
|
/// re-exercises the production "first N in_progress are backed"
|
|
/// partition heuristic.
|
|
fn collected_from(
|
|
snapshots: Vec<TodoSnapshot>,
|
|
backing_task_count: usize,
|
|
) -> CollectedTodoGateInput {
|
|
let todos = snapshots
|
|
.into_iter()
|
|
.map(|t| (t.id, t.content, t.status))
|
|
.collect();
|
|
CollectedTodoGateInput {
|
|
todos,
|
|
backing_task_count,
|
|
}
|
|
}
|
|
|
|
/// Run the gate against one assistant turn. `fixture_name` is only used
|
|
/// in panic messages so failures point straight at the offending JSON.
|
|
fn check_assistant_turn(fixture_name: &str, turn: AssistantTurn) {
|
|
let AssistantTurn {
|
|
turn_index,
|
|
tool_calls_emitted: _,
|
|
todo_state_after_turn,
|
|
backing_task_count,
|
|
expected_gate_decision,
|
|
expected_reason,
|
|
expected_reminder_contains,
|
|
} = turn;
|
|
|
|
let collected = collected_from(todo_state_after_turn, backing_task_count);
|
|
let input = collected.as_input();
|
|
let decision = evaluate_todo_gate(&input);
|
|
|
|
match (expected_gate_decision, decision) {
|
|
(ExpectedGateDecision::Continue, TodoGateDecision::Continue) => {
|
|
assert!(
|
|
expected_reason.is_none(),
|
|
"fixture {fixture_name} turn {turn_index} declares `continue` with \
|
|
`expected_reason` — a continue decision has no reason",
|
|
);
|
|
assert!(
|
|
expected_reminder_contains.is_empty(),
|
|
"fixture {fixture_name} turn {turn_index} declares `continue` with \
|
|
`expected_reminder_contains` — a continue decision emits no reminder",
|
|
);
|
|
}
|
|
(ExpectedGateDecision::Nudge, TodoGateDecision::Nudge { reminder, reason }) => {
|
|
if let Some(expected) = expected_reason {
|
|
assert_eq!(
|
|
expected,
|
|
ExpectedReason::from(reason),
|
|
"fixture {fixture_name} turn {turn_index}: gate reason mismatch",
|
|
);
|
|
}
|
|
for needle in &expected_reminder_contains {
|
|
assert!(
|
|
reminder.contains(needle.as_str()),
|
|
"fixture {fixture_name} turn {turn_index}: reminder missing substring \
|
|
{needle:?}.\nFull reminder:\n{reminder}",
|
|
);
|
|
}
|
|
}
|
|
(expected, TodoGateDecision::Continue) => {
|
|
panic!("fixture {fixture_name} turn {turn_index}: expected {expected:?}, got Continue",)
|
|
}
|
|
(expected, TodoGateDecision::Nudge { reason, .. }) => panic!(
|
|
"fixture {fixture_name} turn {turn_index}: expected {expected:?}, got Nudge({reason:?})",
|
|
),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn replay_all_synthetic_fixtures() {
|
|
for path in discover_fixtures() {
|
|
let fixture = load_fixture(&path);
|
|
let Fixture {
|
|
name,
|
|
description: _,
|
|
turns,
|
|
} = fixture;
|
|
let mut saw_assistant = false;
|
|
for turn in turns {
|
|
match turn {
|
|
Turn::User(_) => {}
|
|
Turn::Assistant(at) => {
|
|
saw_assistant = true;
|
|
check_assistant_turn(&name, at);
|
|
}
|
|
}
|
|
}
|
|
assert!(
|
|
saw_assistant,
|
|
"fixture {name} ({}) has no assistant turns to evaluate",
|
|
path.display(),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Set-equality guard: the on-disk fixture set must exactly equal
|
|
/// [`CANONICAL_FIXTURES`]. Adding a fixture without updating the
|
|
/// constant — or losing one — fails the harness. Closes the
|
|
/// "open-ended presence check" gap.
|
|
#[test]
|
|
fn canonical_fixtures_match_disk() {
|
|
let actual: BTreeSet<String> = discover_fixtures()
|
|
.iter()
|
|
.map(|p| {
|
|
p.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or_else(|| panic!("non-UTF8 fixture path: {}", p.display()))
|
|
.to_string()
|
|
})
|
|
.collect();
|
|
let expected: BTreeSet<String> = CANONICAL_FIXTURES
|
|
.iter()
|
|
.map(|s| (*s).to_string())
|
|
.collect();
|
|
assert_eq!(actual, expected, "fixture set drift vs CANONICAL_FIXTURES");
|
|
}
|
|
|
|
/// Compile-time guard: `ExpectedGateDecision` must stay a closed
|
|
/// two-variant enum so a fixture typo fails to load instead of
|
|
/// silently passing the wrong branch.
|
|
#[test]
|
|
fn expected_decision_is_closed() {
|
|
let parsed: ExpectedGateDecision = serde_json::from_str(r#""nudge""#).unwrap();
|
|
assert_eq!(parsed, ExpectedGateDecision::Nudge);
|
|
let parsed: ExpectedGateDecision = serde_json::from_str(r#""continue""#).unwrap();
|
|
assert_eq!(parsed, ExpectedGateDecision::Continue);
|
|
let err = serde_json::from_str::<ExpectedGateDecision>(r#""maybe""#).unwrap_err();
|
|
assert!(
|
|
err.to_string().contains("unknown variant"),
|
|
"expected unknown-variant error, got: {err}"
|
|
);
|
|
}
|