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,278 @@
|
||||
//! 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}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user