§9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed

The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
This commit is contained in:
2026-07-18 02:48:46 -04:00
parent 86e3724310
commit 6f31415ed6
1056 changed files with 8410 additions and 18307 deletions
@@ -11,7 +11,7 @@
//! picks chain back to back, reproducing the continuous
//! `.git/index.lock` / HEAD-move churn of an agent-run rebase.
//!
//! With the fs-watch machinery on (`x.ai/hunkTracker` + `x.ai/gitHeadChanged`
//! With the fs-watch machinery on (`kigi/hunkTracker` + `kigi/gitHeadChanged`
//! advertised), fsnotify merges rapid lock cycles into one operation and the
//! session defers debounce fires while an op is in flight, so one rebase
//! costs at most a couple of `refresh_all_baselines` scans (each scoped to
@@ -425,7 +425,7 @@ fn git_rebase_refresh_storm_e2e() {
let server = mock_rt
.block_on(MockInferenceServer::start())
.expect("mock server");
let kigi_home = TempDir::new().expect("grok home");
let kigi_home = TempDir::new().expect("kigi home");
// SAFETY: the only live threads are the mock runtime's workers, which
// serve HTTP and never read the process environment.
@@ -450,8 +450,8 @@ fn git_rebase_refresh_storm_e2e() {
let on = agent_rt.block_on(run_storm(
&server,
Some(json!({
"x.ai/hunkTracker": { "mode": "agent_only" },
"x.ai/gitHeadChanged": true,
"kigi/hunkTracker": { "mode": "agent_only" },
"kigi/gitHeadChanged": true,
})),
&counter,
"machinery-on",
@@ -3,7 +3,7 @@
"""
Memory System Integration Tests — Full Suite.
Launches grok agent stdio in isolated environments ($HOME override)
Launches kigi agent stdio in isolated environments ($HOME override)
with pre-populated memory files. Tests the entire memory lifecycle:
indexing, search, embeddings, flush, session-end, compaction, pruning.
@@ -67,7 +67,7 @@ def section(t):
class AcpClient:
"""Talks ACP (JSON-RPC over NDJSON) to a grok agent stdio subprocess."""
"""Talks ACP (JSON-RPC over NDJSON) to a kigi agent stdio subprocess."""
def __init__(self, proc, cwd=None):
self.proc = proc
@@ -163,38 +163,38 @@ class AcpClient:
class IsolatedEnv:
"""Creates a fully isolated grok environment with custom $HOME.
"""Creates a fully isolated kigi environment with custom $HOME.
The agent subprocess gets a fake $HOME with:
~/.grok/auth.json (copied from real home)
~/.grok/memory/ (pre-populated by tests)
~/.grok/logs/ (memory.log appears here)
~/.kigi/auth.json (copied from real home)
~/.kigi/memory/ (pre-populated by tests)
~/.kigi/logs/ (memory.log appears here)
And a workspace directory used as cwd when spawning the agent.
"""
def __init__(self, workspace_name="test-project"):
self.root = tempfile.mkdtemp(prefix="grok-memtest-")
self.root = tempfile.mkdtemp(prefix="kigi-memtest-")
self.fake_home = os.path.join(self.root, "home")
self.workspace = os.path.join(self.root, "workspace", workspace_name)
os.makedirs(self.fake_home)
os.makedirs(self.workspace)
# Create .grok dirs in fake home
self.grok_home = os.path.join(self.fake_home, ".grok")
self.memory_dir = os.path.join(self.grok_home, "memory")
self.logs_dir = os.path.join(self.grok_home, "logs")
# Create .kigi dirs in fake home
self.kigi_home = os.path.join(self.fake_home, ".kigi")
self.memory_dir = os.path.join(self.kigi_home, "memory")
self.logs_dir = os.path.join(self.kigi_home, "logs")
os.makedirs(self.memory_dir)
os.makedirs(self.logs_dir)
# Copy auth from real home
real_auth = os.path.expanduser("~/.grok/auth.json")
real_auth = os.path.expanduser("~/.kigi/auth.json")
if os.path.isfile(real_auth):
shutil.copy2(real_auth, os.path.join(self.grok_home, "auth.json"))
shutil.copy2(real_auth, os.path.join(self.kigi_home, "auth.json"))
def write_config(self, toml_str):
"""Write global ~/.grok/config.toml (where the agent reads config)."""
with open(os.path.join(self.grok_home, "config.toml"), "w") as f:
"""Write global ~/.kigi/config.toml (where the agent reads config)."""
with open(os.path.join(self.kigi_home, "config.toml"), "w") as f:
f.write(toml_str)
def write_global_memory(self, content):
@@ -266,9 +266,9 @@ class IsolatedEnv:
return sorted(files)
def spawn_agent(self, extra_env=None):
binary = os.environ.get("KIGI_BINARY", "grok")
binary = os.environ.get("KIGI_BINARY", "kigi")
if not shutil.which(binary) and not os.path.isfile(binary):
print(f"{R}grok binary not found: {binary}{N}")
print(f"{R}kigi binary not found: {binary}{N}")
sys.exit(1)
env = os.environ.copy()
env["HOME"] = self.fake_home
@@ -671,7 +671,7 @@ def test_fts_special_characters():
* Use C++ for performance-critical code
* Configure with --enable-feature=fast_path
* Email: dev@example.com
* Path: /usr/local/bin/grok
* Path: /usr/local/bin/kigi
* Version >= 2.0.0
""")
client = env.spawn_agent()
@@ -1344,10 +1344,10 @@ def test_multiple_workspace_isolation():
beta_workspace = os.path.join(env1.root, "workspace", "project-beta")
os.makedirs(beta_workspace, exist_ok=True)
# Config is already in global ~/.grok/config.toml from env1.write_config
# Config is already in global ~/.kigi/config.toml from env1.write_config
# Start agent in workspace beta (reuse env1's fake home)
binary = os.environ.get("KIGI_BINARY", "grok")
binary = os.environ.get("KIGI_BINARY", "kigi")
env_vars = os.environ.copy()
env_vars["HOME"] = env1.fake_home
env_vars["KIGI_MEMORY"] = "1"
@@ -83,7 +83,7 @@ async fn empty_dk_response_marker_binds_the_verified_deployment_id() {
assert!(kigi_shell::managed_config::managed_policy_gate().is_ok());
}
/// A signature-rejected sync surfaces as failure in BOTH `grok setup` and the
/// A signature-rejected sync surfaces as failure in BOTH `kigi setup` and the
/// post-login sync — never as Installed/NoChange while nothing was persisted.
#[tokio::test]
#[serial]
@@ -9,7 +9,7 @@
//! - Same-type model switching (no rebuild)
//! - Session resume
//!
//! Each test spawns a real `grok agent stdio` process, speaks the full ACP
//! Each test spawns a real `kigi agent stdio` process, speaks the full ACP
//! protocol, and asserts on the inference request bodies (system prompt) and
//! stderr tracing output to verify the correct harness was used.
//!
@@ -39,7 +39,7 @@ fn invalidate_models_cache(home: &std::path::Path) {
}
}
/// Start a mock server with two models:
/// - `default-model`: no agent_type (→ defaults to "grok-build")
/// - `default-model`: no agent_type (→ defaults to "kigi")
async fn dual_model_server() -> MockInferenceServer {
MockInferenceServer::start_with_models(vec![
MockModelEntry::new("default-model"),
@@ -49,8 +49,8 @@ async fn dual_model_server() -> MockInferenceServer {
.expect("start mock server")
}
/// Start a mock server with two models that share the same agent_type:
/// - `model-a`: no agent_type (→ "grok-build")
/// - `model-b`: no agent_type (→ "grok-build")
/// - `model-a`: no agent_type (→ "kigi")
/// - `model-b`: no agent_type (→ "kigi")
async fn same_type_server() -> MockInferenceServer {
MockInferenceServer::start_with_models(vec![
MockModelEntry::new("model-a"),
@@ -60,17 +60,17 @@ async fn same_type_server() -> MockInferenceServer {
.expect("start mock server")
}
/// Session created with a model that has no `agent_type` should use the
/// `grok-build` harness. The system prompt sent to the LLM should contain
/// the grok-build identity string.
/// `kigi` harness. The system prompt sent to the LLM should contain
/// the kigi identity string.
#[tokio::test]
#[ignore]
async fn test_default_model_uses_grok_build_harness() {
async fn test_default_model_uses_kigi_harness() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let workdir = git_workdir();
let client = GrokStdioClient::spawn(&server, workdir.path()).await;
let client = KigiStdioClient::spawn(&server, workdir.path()).await;
client.initialize_with_timeout().await;
let session_id = client.create_session_with_timeout(workdir.path()).await;
let result = client.prompt_with_timeout(&session_id, "say hello").await;
@@ -79,8 +79,8 @@ async fn test_default_model_uses_grok_build_harness() {
.last_system_prompt()
.expect("should have at least one inference request");
assert!(
sys_prompt.contains("Grok") || sys_prompt.contains("grok"),
"default model should use grok-build harness\nsystem prompt preview: {}",
sys_prompt.contains("Kigi") || sys_prompt.contains("kigi"),
"default model should use kigi harness\nsystem prompt preview: {}",
&sys_prompt[..sys_prompt.len().min(500)]
);
})
@@ -94,7 +94,7 @@ async fn test_same_type_model_switch_no_rebuild() {
with_local_set(|| async {
let server = same_type_server().await;
let workdir = git_workdir();
let client = GrokStdioClient::spawn(&server, workdir.path()).await;
let client = KigiStdioClient::spawn(&server, workdir.path()).await;
client.initialize_with_timeout().await;
let session_id = client
.create_session_with_model_timeout(workdir.path(), "model-a")
@@ -128,7 +128,7 @@ async fn test_session_resume_preserves_harness() {
.await
.expect("start mock server");
let workdir = git_workdir();
let mut writer = GrokStdioClient::spawn(&server, workdir.path()).await;
let mut writer = KigiStdioClient::spawn(&server, workdir.path()).await;
writer.initialize_with_timeout().await;
let session_id = writer.create_session_with_timeout(workdir.path()).await;
let result = writer.prompt_with_timeout(&session_id, "say hello").await;
@@ -139,7 +139,7 @@ async fn test_session_resume_preserves_harness() {
let shared_home = writer.take_home();
invalidate_models_cache(shared_home.path());
drop(writer);
let reader = GrokStdioClient::spawn_with_home(&server, workdir.path(), shared_home).await;
let reader = KigiStdioClient::spawn_with_home(&server, workdir.path(), shared_home).await;
reader.initialize_with_timeout().await;
let _ = reader
.load_session_with_timeout(&session_id, workdir.path())
@@ -153,16 +153,16 @@ async fn test_session_resume_preserves_harness() {
let resumed_sys_prompt = server
.last_system_prompt()
.expect("should have captured resumed system prompt");
let original_has_grok =
original_sys_prompt.contains("Grok") || original_sys_prompt.contains("grok");
let resumed_has_grok =
resumed_sys_prompt.contains("Grok") || resumed_sys_prompt.contains("grok");
let original_has_kigi =
original_sys_prompt.contains("Kigi") || original_sys_prompt.contains("kigi");
let resumed_has_kigi =
resumed_sys_prompt.contains("Kigi") || resumed_sys_prompt.contains("kigi");
assert_eq!(
original_has_grok,
resumed_has_grok,
original_has_kigi,
resumed_has_kigi,
"resumed session should use the same harness as the original\n\
original identity markers: grok={original_has_grok}\n\
resumed identity markers: grok={resumed_has_grok}\n\
original identity markers: kigi={original_has_kigi}\n\
resumed identity markers: kigi={resumed_has_kigi}\n\
original prompt (first 300): {}\n\
resumed prompt (first 300): {}",
&original_sys_prompt[..original_sys_prompt.len().min(300)],
@@ -172,172 +172,159 @@ async fn test_session_resume_preserves_harness() {
.await;
}
/// A model that doesn't declare `agent_type` in its metadata should
/// default to `"grok-build"`. This exercises the serde default.
/// default to `"kigi"`. This exercises the serde default.
#[tokio::test]
#[ignore]
async fn test_model_without_agent_type_defaults_to_grok_build() {
async fn test_model_without_agent_type_defaults_to_kigi() {
with_local_set(|| async {
let server = MockInferenceServer::start_with_models(
vec![MockModelEntry::new("no-agent-type-model"),],
)
.await
.expect("start mock server");
let workdir = git_workdir();
let client = GrokStdioClient::spawn(&server, workdir.path()).await;
client.initialize_with_timeout().await;
let session_id = client
.create_session_with_model_timeout(workdir.path(), "no-agent-type-model")
.await;
let result = client.prompt_with_timeout(&session_id, "say hello").await;
assert!(result.is_ok(), "prompt failed: {:?}", result.err());
let sys_prompt = server
.last_system_prompt()
.expect("should have at least one inference request");
assert!(
sys_prompt.contains("Grok") || sys_prompt.contains("grok"),
"model without agent_type should default to grok-build harness\nsystem prompt preview: {}",
& sys_prompt[..sys_prompt.len().min(500)]
);
})
.await;
let server = MockInferenceServer::start_with_models(vec![MockModelEntry::new(
"no-agent-type-model",
)])
.await
.expect("start mock server");
let workdir = git_workdir();
let client = KigiStdioClient::spawn(&server, workdir.path()).await;
client.initialize_with_timeout().await;
let session_id = client
.create_session_with_model_timeout(workdir.path(), "no-agent-type-model")
.await;
let result = client.prompt_with_timeout(&session_id, "say hello").await;
assert!(result.is_ok(), "prompt failed: {:?}", result.err());
let sys_prompt = server
.last_system_prompt()
.expect("should have at least one inference request");
assert!(
sys_prompt.contains("Kigi") || sys_prompt.contains("kigi"),
"model without agent_type should default to kigi harness\nsystem prompt preview: {}",
&sys_prompt[..sys_prompt.len().min(500)]
);
})
.await;
}
/// The `KIGI_AGENT` escape hatch should override the model's agent_type.
/// Setting `KIGI_AGENT=grok-build` with an alternate-agent model should use
/// grok-build harness.
/// Setting `KIGI_AGENT=kigi` with an alternate-agent model should use
/// kigi harness.
#[tokio::test]
#[ignore]
async fn test_grok_agent_env_overrides_model_agent_type() {
async fn test_kigi_agent_env_overrides_model_agent_type() {
with_local_set(|| async {
let server = dual_model_server().await;
let workdir = git_workdir();
let binary = grok_binary();
let home = tempfile::TempDir::new().expect("create temp home");
let mut cmd = tokio::process::Command::new(&binary);
cmd.args(["agent", "stdio"])
.current_dir(workdir.path())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
kigi_test_support::env::test_env_cmd_tokio(
&mut cmd,
&server.url(),
home.path(),
);
cmd.env("KIGI_AGENT", "grok-build");
let mut child = cmd.spawn().expect("spawn grok");
let outgoing = child.stdin.take().unwrap();
let incoming = child.stdout.take().unwrap();
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
let outgoing = outgoing.compat_write();
let incoming = incoming.compat();
let incoming = kigi_acp_lib::LineBufferedRead::spawn_local(incoming);
use agent_client_protocol as acp;
struct NoopClient;
#[async_trait::async_trait(?Send)]
impl acp::Client for NoopClient {
async fn request_permission(
&self,
args: acp::RequestPermissionRequest,
) -> acp::Result<acp::RequestPermissionResponse> {
let outcome = args
.options
.iter()
.find(|o| o.kind == acp::PermissionOptionKind::AllowOnce)
.or(args.options.first())
.map(|o| acp::RequestPermissionOutcome::Selected(
let server = dual_model_server().await;
let workdir = git_workdir();
let binary = kigi_binary();
let home = tempfile::TempDir::new().expect("create temp home");
let mut cmd = tokio::process::Command::new(&binary);
cmd.args(["agent", "stdio"])
.current_dir(workdir.path())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
kigi_test_support::env::test_env_cmd_tokio(&mut cmd, &server.url(), home.path());
cmd.env("KIGI_AGENT", "kigi");
let mut child = cmd.spawn().expect("spawn kigi");
let outgoing = child.stdin.take().unwrap();
let incoming = child.stdout.take().unwrap();
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
let outgoing = outgoing.compat_write();
let incoming = incoming.compat();
let incoming = kigi_acp_lib::LineBufferedRead::spawn_local(incoming);
use agent_client_protocol as acp;
struct NoopClient;
#[async_trait::async_trait(?Send)]
impl acp::Client for NoopClient {
async fn request_permission(
&self,
args: acp::RequestPermissionRequest,
) -> acp::Result<acp::RequestPermissionResponse> {
let outcome = args
.options
.iter()
.find(|o| o.kind == acp::PermissionOptionKind::AllowOnce)
.or(args.options.first())
.map(|o| {
acp::RequestPermissionOutcome::Selected(
acp::SelectedPermissionOutcome::new(o.option_id.clone()),
))
.unwrap_or(acp::RequestPermissionOutcome::Cancelled);
Ok(acp::RequestPermissionResponse::new(outcome))
}
async fn session_notification(
&self,
_args: acp::SessionNotification,
) -> acp::Result<()> {
Ok(())
}
)
})
.unwrap_or(acp::RequestPermissionOutcome::Cancelled);
Ok(acp::RequestPermissionResponse::new(outcome))
}
let (conn, handle_io) = acp::ClientSideConnection::new(
NoopClient,
outgoing,
incoming,
|fut| {
tokio::task::spawn_local(fut);
},
);
tokio::task::spawn_local(handle_io);
let _init = tokio::time::timeout(
Duration::from_secs(20),
conn
.initialize(
acp::InitializeRequest::new(acp::ProtocolVersion::V1)
.client_capabilities(
acp::ClientCapabilities::new()
.fs(acp::FileSystemCapabilities::new())
.terminal(false),
)
.meta(
serde_json::json!(
{ "startupHints" : { "nonInteractive" : true,
"skipGitStatus" : true, "skipProjectLayout" : true },
"clientType" : "test-client", "clientVersion" : "0.0.0-test"
}
)
.as_object()
.cloned(),
),
),
)
.await
.expect("init timed out")
.expect("init failed");
conn.authenticate(
acp::AuthenticateRequest::new(acp::AuthMethodId::new("xai.api_key"))
.meta(
serde_json::json!({ "headless" : true }).as_object().cloned(),
),
)
.await
.expect("auth failed");
let session = tokio::time::timeout(
Duration::from_secs(20),
conn
.new_session(
acp::NewSessionRequest::new(workdir.path().to_path_buf())
.meta(
serde_json::json!({ "modelId" : "cursor-model" })
.as_object()
.cloned(),
),
),
)
.await
.expect("session/new timed out")
.expect("session/new failed");
let _prompt = tokio::time::timeout(
Duration::from_secs(30),
conn
.prompt(
acp::PromptRequest::new(
session.session_id.clone(),
vec![
acp::ContentBlock::Text(acp::TextContent::new("say hello"))
],
),
),
)
.await
.expect("prompt timed out")
.expect("prompt failed");
let sys_prompt = server
.last_system_prompt()
.expect("should have inference request");
assert!(
sys_prompt.contains("Grok") || sys_prompt.contains("grok"),
"KIGI_AGENT=grok-build should override cursor model's agent_type\nsystem prompt preview: {}",
& sys_prompt[..sys_prompt.len().min(500)]
);
})
.await;
async fn session_notification(
&self,
_args: acp::SessionNotification,
) -> acp::Result<()> {
Ok(())
}
}
let (conn, handle_io) =
acp::ClientSideConnection::new(NoopClient, outgoing, incoming, |fut| {
tokio::task::spawn_local(fut);
});
tokio::task::spawn_local(handle_io);
let _init = tokio::time::timeout(
Duration::from_secs(20),
conn.initialize(
acp::InitializeRequest::new(acp::ProtocolVersion::V1)
.client_capabilities(
acp::ClientCapabilities::new()
.fs(acp::FileSystemCapabilities::new())
.terminal(false),
)
.meta(
serde_json::json!(
{ "startupHints" : { "nonInteractive" : true,
"skipGitStatus" : true, "skipProjectLayout" : true },
"clientType" : "test-client", "clientVersion" : "0.0.0-test"
}
)
.as_object()
.cloned(),
),
),
)
.await
.expect("init timed out")
.expect("init failed");
conn.authenticate(
acp::AuthenticateRequest::new(acp::AuthMethodId::new("xai.api_key")).meta(
serde_json::json!({ "headless" : true })
.as_object()
.cloned(),
),
)
.await
.expect("auth failed");
let session = tokio::time::timeout(
Duration::from_secs(20),
conn.new_session(
acp::NewSessionRequest::new(workdir.path().to_path_buf()).meta(
serde_json::json!({ "modelId" : "cursor-model" })
.as_object()
.cloned(),
),
),
)
.await
.expect("session/new timed out")
.expect("session/new failed");
let _prompt = tokio::time::timeout(
Duration::from_secs(30),
conn.prompt(acp::PromptRequest::new(
session.session_id.clone(),
vec![acp::ContentBlock::Text(acp::TextContent::new("say hello"))],
)),
)
.await
.expect("prompt timed out")
.expect("prompt failed");
let sys_prompt = server
.last_system_prompt()
.expect("should have inference request");
assert!(
sys_prompt.contains("Kigi") || sys_prompt.contains("kigi"),
"KIGI_AGENT=kigi should override cursor model's agent_type\nsystem prompt preview: {}",
&sys_prompt[..sys_prompt.len().min(500)]
);
})
.await;
}
@@ -1,12 +1,12 @@
//! Built-binary end-to-end tests for the grok (kigi-tui) binary.
//! Built-binary end-to-end tests for the kigi (kigi-tui) binary.
//!
//! These tests verify that the built grok binary works end-to-end against a mock
//! These tests verify that the built kigi binary works end-to-end against a mock
//! inference server. They catch dynamic linking failures (libgit2/OpenSSL),
//! session initialization crashes, and protocol regressions.
//!
//! The tests exercise:
//! - **Smoke** (`grok --version`): binary loads without crashing
//! - **ACP stdio** (`grok agent stdio`): full protocol lifecycle via ClientSideConnection
//! - **Smoke** (`kigi --version`): binary loads without crashing
//! - **ACP stdio** (`kigi agent stdio`): full protocol lifecycle via ClientSideConnection
//!
//! Tests are `#[ignore]`d by default — they require a pre-built binary.
//!
@@ -17,7 +17,7 @@
//!
//! In CI, set `KIGI_BINARY` to point at the release artifact:
//! ```bash
//! KIGI_BINARY=./artifacts/grok-0.1.159-linux-x86_64 \
//! KIGI_BINARY=./artifacts/kigi-0.1.159-linux-x86_64 \
//! cargo test -p kigi-shell --test test_built_binary_e2e -- --ignored
//! ```
@@ -50,9 +50,9 @@ async fn single_model_server(model: &str, backend: &str) -> MockInferenceServer
.expect("start mock server")
}
async fn grok_build_server() -> MockInferenceServer {
async fn kigi_server() -> MockInferenceServer {
MockInferenceServer::start_with_models(vec![
MockModelEntry::with_agent_type("grok-4.5", "grok-build")
MockModelEntry::with_agent_type("kigi-4.5", "kigi")
.with_api_backend("responses")
.with_supports_backend_search(true),
])
@@ -112,7 +112,7 @@ async fn run_headless_with_env(
env: &[(&str, &str)],
) -> HeadlessResult {
let home = tempfile::TempDir::new().expect("create temp home");
let mut cmd = tokio::process::Command::new(grok_binary());
let mut cmd = tokio::process::Command::new(kigi_binary());
cmd.args(args)
.current_dir(cwd)
.stdin(std::process::Stdio::null())
@@ -133,7 +133,7 @@ async fn run_headless_with_env(
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_version_exits_zero() {
let binary = grok_binary();
let binary = kigi_binary();
let output = Command::new(&binary)
.arg("--version")
.output()
@@ -141,7 +141,7 @@ async fn test_version_exits_zero() {
assert!(
output.status.success(),
"grok --version failed (exit {:?}):\n{}",
"kigi --version failed (exit {:?}):\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
);
@@ -153,7 +153,7 @@ async fn test_version_exits_zero() {
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_version_with_crash_handler_exits_zero() {
let binary = grok_binary();
let binary = kigi_binary();
let output = Command::new(&binary)
.arg("--version")
.env("KIGI_CRASH_HANDLER", "1")
@@ -162,7 +162,7 @@ async fn test_version_with_crash_handler_exits_zero() {
assert!(
output.status.success(),
"grok --version with KIGI_CRASH_HANDLER=1 failed (exit {:?}):\n{}",
"kigi --version with KIGI_CRASH_HANDLER=1 failed (exit {:?}):\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
);
@@ -183,7 +183,7 @@ async fn test_headless_session_in_git_repo() {
let workdir = git_workdir();
let result = run_headless(&server, &["-p", "say hello", "--yolo"], workdir.path()).await;
assert_headless_success(&result, "grok -p in git repo", Some(&server));
assert_headless_success(&result, "kigi -p in git repo", Some(&server));
assert_no_crashes(&result.stderr);
assert!(
server.request_count() > 0,
@@ -197,7 +197,7 @@ async fn test_headless_session_in_git_repo() {
);
}
/// Verify grok works in a non-git directory (exercises the fallback codepath
/// Verify kigi works in a non-git directory (exercises the fallback codepath
/// where libgit2 discovers there's no repo instead of initializing one).
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
@@ -210,14 +210,14 @@ async fn test_headless_session_in_non_git_dir() {
let result = run_headless(&server, &["-p", "say hello", "--yolo"], workdir.path()).await;
assert_headless_success(&result, "grok -p in non-git dir", Some(&server));
assert_headless_success(&result, "kigi -p in non-git dir", Some(&server));
assert_no_crashes(&result.stderr);
}
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_headless_tools_allowlist_keeps_enabled_web_tools() {
let server = grok_build_server().await;
let server = kigi_server().await;
server.preset_allow_access();
let workdir = git_workdir();
@@ -235,7 +235,7 @@ async fn test_headless_tools_allowlist_keeps_enabled_web_tools() {
)
.await;
assert_headless_success(&result, "grok -p --tools with web tools", Some(&server));
assert_headless_success(&result, "kigi -p --tools with web tools", Some(&server));
assert_no_crashes(&result.stderr);
let names = inference_tool_names(&server);
for expected in ["read_file", "grep", "list_dir", "web_search", "web_fetch"] {
@@ -275,7 +275,7 @@ async fn test_headless_tools_allowlist_keeps_enabled_web_tools() {
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_headless_tools_allowlist_does_not_fail_open_for_disabled_web_fetch() {
let server = grok_build_server().await;
let server = kigi_server().await;
server.set_settings(serde_json::json!({
"allow_access": true,
"web_fetch_enabled": false,
@@ -298,7 +298,7 @@ async fn test_headless_tools_allowlist_does_not_fail_open_for_disabled_web_fetch
assert_headless_success(
&result,
"grok -p --tools with disabled web_fetch",
"kigi -p --tools with disabled web_fetch",
Some(&server),
);
assert_no_crashes(&result.stderr);
@@ -315,7 +315,7 @@ async fn test_headless_tools_allowlist_does_not_fail_open_for_disabled_web_fetch
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_headless_terminal_only_allowlist_is_foreground_only() {
let server = grok_build_server().await;
let server = kigi_server().await;
let workdir = git_workdir();
let result = run_headless(
@@ -325,7 +325,7 @@ async fn test_headless_terminal_only_allowlist_is_foreground_only() {
)
.await;
assert_headless_success(&result, "grok -p --tools run_terminal_cmd", Some(&server));
assert_headless_success(&result, "kigi -p --tools run_terminal_cmd", Some(&server));
assert_no_crashes(&result.stderr);
let request = inference_request(&server);
let terminal = request["tools"]
@@ -386,7 +386,7 @@ async fn test_headless_free_usage_exhausted_prints_paywall_message() {
assert_no_crashes(&result.stderr);
let combined = format!("{}\n{}", result.stdout, result.stderr);
assert!(
combined.contains("reached your free Grok Build usage limit"),
combined.contains("reached your free Kigi usage limit"),
"expected the free-usage paywall message\nstdout:\n{}\nstderr tail:\n{}",
result.stdout,
stderr_tail(&result.stderr, 1000)
@@ -421,7 +421,7 @@ async fn test_headless_streaming_json_output() {
assert_headless_success(
&result,
"grok -p --output-format streaming-json",
"kigi -p --output-format streaming-json",
Some(&server),
);
assert_no_crashes(&result.stderr);
@@ -468,19 +468,19 @@ async fn test_headless_streaming_json_output() {
async fn test_headless_json_reports_server_cost() {
use kigi_test_support::scripted::SseEvent;
let server = single_model_server("grok-4.5", "chat_completions").await;
let server = single_model_server("kigi-4.5", "chat_completions").await;
let chunk = |body: serde_json::Value| SseEvent::data(body.to_string());
server.enqueue_response(
"/v1/chat/completions",
kigi_test_support::scripted::ScriptedResponse::sse(vec![
chunk(serde_json::json!({
"id": "chatcmpl-cost", "object": "chat.completion.chunk", "created": 0,
"model": "grok-4.5",
"model": "kigi-4.5",
"choices": [{ "index": 0, "delta": { "content": "4" }, "finish_reason": "stop" }]
})),
chunk(serde_json::json!({
"id": "chatcmpl-cost", "object": "chat.completion.chunk", "created": 0,
"model": "grok-4.5", "choices": [],
"model": "kigi-4.5", "choices": [],
"usage": {
"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15,
"cost_in_usd_ticks": 1_234_500_000_i64
@@ -498,7 +498,7 @@ async fn test_headless_json_reports_server_cost() {
"what is 2+2",
"--yolo",
"--model",
"grok-4.5",
"kigi-4.5",
"--max-turns",
"1",
"--output-format",
@@ -508,7 +508,7 @@ async fn test_headless_json_reports_server_cost() {
)
.await;
assert_headless_success(&result, "grok -p (scripted cost)", Some(&server));
assert_headless_success(&result, "kigi -p (scripted cost)", Some(&server));
let output = parse_stdout_json(&result);
assert_eq!(output["total_cost_usd"], 0.12345);
assert_eq!(output["total_cost_usd_ticks"], 1_234_500_000_i64);
@@ -528,7 +528,7 @@ async fn test_headless_json_reports_server_cost() {
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_headless_json_reports_usage_on_max_turns() {
let server = single_model_server("grok-4.5", "chat_completions").await;
let server = single_model_server("kigi-4.5", "chat_completions").await;
server.enqueue_response(
"/v1/chat/completions",
kigi_test_support::scripted::ScriptedResponse::sse(
@@ -537,7 +537,7 @@ async fn test_headless_json_reports_usage_on_max_turns() {
"call-1",
"read_file",
r#"{"path":"README.md"}"#,
"grok-4.5",
"kigi-4.5",
),
),
);
@@ -550,7 +550,7 @@ async fn test_headless_json_reports_usage_on_max_turns() {
"read the readme",
"--yolo",
"--model",
"grok-4.5",
"kigi-4.5",
"--max-turns",
"1",
"--output-format",
@@ -569,7 +569,7 @@ async fn test_headless_json_reports_usage_on_max_turns() {
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_headless_streaming_json_usage() {
let server = single_model_server("grok-4.5", "chat_completions").await;
let server = single_model_server("kigi-4.5", "chat_completions").await;
let workdir = git_workdir();
let result = run_headless(
&server,
@@ -578,7 +578,7 @@ async fn test_headless_streaming_json_usage() {
"say hello",
"--yolo",
"--model",
"grok-4.5",
"kigi-4.5",
"--output-format",
"streaming-json",
],
@@ -604,7 +604,7 @@ async fn test_headless_streaming_json_usage() {
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn headless_json_schema_chat_completions_uses_response_format() {
let server = single_model_server("grok-4.5", "chat_completions").await;
let server = single_model_server("kigi-4.5", "chat_completions").await;
server.set_response(r#"{"name":"Alice","age":30}"#);
let workdir = git_workdir();
@@ -615,7 +615,7 @@ async fn headless_json_schema_chat_completions_uses_response_format() {
"extract name and age",
"--yolo",
"--model",
"grok-4.5",
"kigi-4.5",
"--json-schema",
r#"{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"],"additionalProperties":false}"#,
"--max-turns",
@@ -627,7 +627,7 @@ async fn headless_json_schema_chat_completions_uses_response_format() {
assert_headless_success(
&result,
"grok -p --json-schema (chat_completions)",
"kigi -p --json-schema (chat_completions)",
Some(&server),
);
assert_no_crashes(&result.stderr);
@@ -663,7 +663,7 @@ async fn headless_json_schema_chat_completions_uses_response_format() {
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn headless_json_schema_responses_uses_text_format() {
let server = single_model_server("grok-4.5", "responses").await;
let server = single_model_server("kigi-4.5", "responses").await;
server.set_response(r#"{"name":"Alice","age":30}"#);
let workdir = git_workdir();
@@ -674,7 +674,7 @@ async fn headless_json_schema_responses_uses_text_format() {
"extract name and age",
"--yolo",
"--model",
"grok-4.5",
"kigi-4.5",
"--json-schema",
NAME_AGE_SCHEMA,
"--max-turns",
@@ -684,7 +684,7 @@ async fn headless_json_schema_responses_uses_text_format() {
)
.await;
assert_headless_success(&result, "grok -p --json-schema (responses)", Some(&server));
assert_headless_success(&result, "kigi -p --json-schema (responses)", Some(&server));
assert_no_crashes(&result.stderr);
let output = parse_stdout_json(&result);
@@ -740,7 +740,7 @@ async fn headless_json_schema_messages_backend_uses_structured_output_tool() {
)
.await;
assert_headless_success(&result, "grok -p --json-schema (messages)", Some(&server));
assert_headless_success(&result, "kigi -p --json-schema (messages)", Some(&server));
assert_no_crashes(&result.stderr);
let output = parse_stdout_json(&result);
@@ -824,7 +824,7 @@ async fn headless_json_schema_messages_validates_text_when_tool_not_called() {
assert_headless_success(
&result,
"grok -p --json-schema (messages, text)",
"kigi -p --json-schema (messages, text)",
Some(&server),
);
assert_no_crashes(&result.stderr);
@@ -871,7 +871,7 @@ async fn headless_json_schema_messages_retries_on_schema_violation() {
assert_headless_success(
&result,
"grok -p --json-schema (messages, retry)",
"kigi -p --json-schema (messages, retry)",
Some(&server),
);
assert_no_crashes(&result.stderr);
@@ -887,7 +887,7 @@ async fn headless_json_schema_messages_retries_on_schema_violation() {
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn invalid_json_schema_disables_structured_output_and_surfaces_error() {
let server = single_model_server("grok-4.5", "chat_completions").await;
let server = single_model_server("kigi-4.5", "chat_completions").await;
server.set_response(r#"{"name":"Alice","age":30}"#);
let workdir = git_workdir();
@@ -898,7 +898,7 @@ async fn invalid_json_schema_disables_structured_output_and_surfaces_error() {
"extract name and age",
"--yolo",
"--model",
"grok-4.5",
"kigi-4.5",
// Valid JSON object, but `pattern` is an invalid regex → schema
// compilation (`jsonschema::validator_for`) fails.
"--json-schema",
@@ -912,7 +912,7 @@ async fn invalid_json_schema_disables_structured_output_and_surfaces_error() {
assert_headless_success(
&result,
"grok -p --json-schema (invalid schema)",
"kigi -p --json-schema (invalid schema)",
Some(&server),
);
assert_no_crashes(&result.stderr);
@@ -950,9 +950,9 @@ async fn invalid_json_schema_disables_structured_output_and_surfaces_error() {
}
// ============================================================================
// ACP stdio tests (grok agent stdio)
// ACP stdio tests (kigi agent stdio)
//
// These test the agent as a server: spawn `grok agent stdio`, speak the full
// These test the agent as a server: spawn `kigi agent stdio`, speak the full
// ACP protocol over pipes, verify the lifecycle works end-to-end.
// ============================================================================
@@ -965,7 +965,7 @@ async fn test_stdio_full_session_lifecycle() {
with_local_set(|| async {
let server = MockInferenceServer::start().await.expect("start mock server");
let workdir = git_workdir();
let client = GrokStdioClient::spawn(&server, workdir.path()).await;
let client = KigiStdioClient::spawn(&server, workdir.path()).await;
// Initialize and authenticate
let init_resp = client.initialize_with_timeout().await;
@@ -1001,7 +1001,7 @@ async fn test_stdio_full_session_lifecycle() {
.await;
}
/// Verify that x.ai/session/close frees the session.
/// Verify that kigi/session/close frees the session.
/// Creates a session, closes it via ext_method, then verifies session/info
/// returns an empty response (session no longer exists).
#[tokio::test]
@@ -1012,7 +1012,7 @@ async fn test_stdio_session_close() {
.await
.expect("start mock server");
let workdir = git_workdir();
let client = GrokStdioClient::spawn(&server, workdir.path()).await;
let client = KigiStdioClient::spawn(&server, workdir.path()).await;
client.initialize_with_timeout().await;
let session_id = client.create_session_with_timeout(workdir.path()).await;
@@ -1020,7 +1020,7 @@ async fn test_stdio_session_close() {
// Session should be alive — session/info returns data with sessionId
let info_resp = client
.ext_method(
"x.ai/session/info",
"kigi/session/info",
serde_json::json!({ "sessionId": session_id.0.as_ref() }),
)
.await;
@@ -1039,7 +1039,7 @@ async fn test_stdio_session_close() {
// Close the session
let close_resp = client
.ext_method(
"x.ai/session/close",
"kigi/session/close",
serde_json::json!({ "sessionId": session_id.0.as_ref() }),
)
.await;
@@ -1053,7 +1053,7 @@ async fn test_stdio_session_close() {
// Session should be gone — session/info returns empty result (no sessionId)
let info_after = client
.ext_method(
"x.ai/session/info",
"kigi/session/info",
serde_json::json!({ "sessionId": session_id.0.as_ref() }),
)
.await;
@@ -1074,7 +1074,7 @@ async fn test_stdio_prompt_then_immediate_load_session() {
with_local_set(|| async {
let server = MockInferenceServer::start().await.expect("start mock server");
let workdir = git_workdir();
let mut writer = GrokStdioClient::spawn(&server, workdir.path()).await;
let mut writer = KigiStdioClient::spawn(&server, workdir.path()).await;
let init_resp = writer.initialize_with_timeout().await;
assert!(
@@ -1095,7 +1095,7 @@ async fn test_stdio_prompt_then_immediate_load_session() {
let shared_home = writer.take_home();
drop(writer);
let reader = GrokStdioClient::spawn_with_home(&server, workdir.path(), shared_home).await;
let reader = KigiStdioClient::spawn_with_home(&server, workdir.path(), shared_home).await;
reader.initialize_with_timeout().await;
let _ = reader
.load_session_with_timeout(&session_id, workdir.path())
@@ -1252,7 +1252,7 @@ async fn test_stdio_xcode_escaped_slash_methods_get_responses() {
// ── Config test harness ─────────────────────────────────────────────────────
/// Isolated headless run with a custom `~/.kigi/`. Clean env (no leaked
/// host credentials). Write config files into `grok_dir()` before `run()`.
/// host credentials). Write config files into `kigi_dir()` before `run()`.
struct ConfigTestHarness {
home: tempfile::TempDir,
workdir: tempfile::TempDir,
@@ -1277,7 +1277,7 @@ impl ConfigTestHarness {
}
}
fn grok_dir(&self) -> std::path::PathBuf {
fn kigi_dir(&self) -> std::path::PathBuf {
self.home.path().join(".kigi")
}
@@ -1287,7 +1287,7 @@ impl ConfigTestHarness {
}
async fn run(&self) -> HeadlessResult {
let mut cmd = tokio::process::Command::new(grok_binary());
let mut cmd = tokio::process::Command::new(kigi_binary());
cmd.args(["-p", "say hello", "--yolo"])
.current_dir(self.workdir.path())
.stdin(std::process::Stdio::null())
@@ -1296,10 +1296,10 @@ impl ConfigTestHarness {
.kill_on_drop(true)
.env_clear()
.env("HOME", self.home.path())
// Windows resolves `~` via USERPROFILE, not HOME — pin the grok
// Windows resolves `~` via USERPROFILE, not HOME — pin the kigi
// home explicitly so the sandbox holds on all platforms (see
// `test_env_cmd_tokio`).
.env("KIGI_SHARE_DIR", self.grok_dir())
.env("KIGI_SHARE_DIR", self.kigi_dir())
.env("PATH", std::env::var("PATH").unwrap_or_default());
for (k, v) in &self.env {
cmd.env(k, v);
@@ -1310,14 +1310,14 @@ impl ConfigTestHarness {
// ── Enterprise managed config tests ────────────────────────────────────────
/// Enterprise BYOK: managed_config.toml overrides grok-build with a custom
/// Enterprise BYOK: managed_config.toml overrides kigi with a custom
/// endpoint + env_key. Mock rejects unauthenticated requests with 401.
/// Regression guard for the 0.1.220 authentication regression.
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn test_headless_managed_config_byok_sends_authorized_requests() {
let server = MockInferenceServer::start_with_required_auth(
vec![MockModelEntry::new("grok-4.5")],
vec![MockModelEntry::new("kigi-4.5")],
"test-byok-secret-token",
)
.await
@@ -1325,7 +1325,7 @@ async fn test_headless_managed_config_byok_sends_authorized_requests() {
let mut h = ConfigTestHarness::new(&server);
std::fs::write(
h.grok_dir().join("managed_config.toml"),
h.kigi_dir().join("managed_config.toml"),
format!(
r#"
[endpoints]
@@ -1337,10 +1337,10 @@ api_backend = "responses"
base_url = "{url}"
context_window = 500000
env_key = "KIGI_TEST_BYOK_TOKEN"
model = "grok-4.5"
model = "kigi-4.5"
[models]
default = "grok-4.5"
default = "kigi-4.5"
"#,
url = server.url()
),
@@ -1369,7 +1369,7 @@ default = "grok-4.5"
#[ignore] // requires pre-built binary; run with --ignored
async fn headless_reasoning_efforts_payload_parses_and_legacy_effort_rides_wire() {
let server = MockInferenceServer::start_with_models(vec![
MockModelEntry::new("grok-4.5")
MockModelEntry::new("kigi-4.5")
.with_api_backend("chat_completions")
.with_supports_reasoning_effort(true)
.with_reasoning_effort("xhigh")
@@ -1390,7 +1390,7 @@ async fn headless_reasoning_efforts_payload_parses_and_legacy_effort_rides_wire(
"hi",
"--yolo",
"--model",
"grok-4.5",
"kigi-4.5",
"--max-turns",
"1",
],
@@ -1398,7 +1398,7 @@ async fn headless_reasoning_efforts_payload_parses_and_legacy_effort_rides_wire(
)
.await;
assert_headless_success(&result, "grok -p reasoning_efforts list", Some(&server));
assert_headless_success(&result, "kigi -p reasoning_efforts list", Some(&server));
assert_no_crashes(&result.stderr);
// The legacy effort scalar rides the chat-completions request unchanged.
@@ -1527,7 +1527,7 @@ async fn test_headless_timeout_exit_kills_pending_background_task() {
assert_headless_success(
&result,
"grok -p with pending background task",
"kigi -p with pending background task",
Some(&server),
);
assert_no_crashes(&result.stderr);
@@ -1565,7 +1565,7 @@ async fn test_headless_no_wait_exit_kills_background_task() {
)
.await;
assert_headless_success(&result, "grok -p --no-wait-for-background", Some(&server));
assert_headless_success(&result, "kigi -p --no-wait-for-background", Some(&server));
assert_no_crashes(&result.stderr);
let pid = read_task_pid(&pid_file);
@@ -1630,7 +1630,7 @@ async fn test_headless_waits_for_short_background_task_and_exits_clean() {
)
.await;
assert_headless_success(&result, "grok -p with short background task", Some(&server));
assert_headless_success(&result, "kigi -p with short background task", Some(&server));
assert_no_crashes(&result.stderr);
assert!(
marker.exists(),
@@ -70,7 +70,7 @@ async fn update_config_does_not_leak_requirements_into_user_config() {
// --- Act ---
// Simulate an unrelated config write (e.g. persisting a model preference).
kigi_shell::util::config::update_config(|cfg| {
cfg.models.default = Some("grok-3".to_string());
cfg.models.default = Some("kigi-3".to_string());
})
.await
.expect("update_config should succeed");
@@ -89,7 +89,7 @@ async fn update_config_does_not_leak_requirements_into_user_config() {
);
// Also verify the unrelated write succeeded.
assert_eq!(user_cfg.models.default.as_deref(), Some("grok-3"));
assert_eq!(user_cfg.models.default.as_deref(), Some("kigi-3"));
}
#[tokio::test]
@@ -1,6 +1,6 @@
//! End-to-end tests for the `--debug` firehose file logging.
//!
//! Runs the built grok binary against the mock inference server with a
//! Runs the built kigi binary against the mock inference server with a
//! caller-owned `$KIGI_SHARE_DIR`, then inspects `~/.kigi/debug/`:
//! - the `--debug` FLAG drives the firehose end to end through the master switch:
//! a live `agent` session launched with `--debug` writes a non-empty per-session
@@ -68,7 +68,7 @@ fn firehose_txt_files(home: &Path) -> Vec<PathBuf> {
.collect()
}
/// Build a headless `grok -p` command with a pinned `$KIGI_SHARE_DIR` so the firehose
/// Build a headless `kigi -p` command with a pinned `$KIGI_SHARE_DIR` so the firehose
/// lands under `<home>/.kigi/debug`. Firehose env knobs are cleared so the test
/// is hermetic regardless of the developer's shell.
fn debug_cmd(
@@ -77,7 +77,7 @@ fn debug_cmd(
workdir: &Path,
extra: &[&str],
) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new(grok_binary());
let mut cmd = tokio::process::Command::new(kigi_binary());
cmd.args(["-p", "say hi", "--yolo", "--output-format", "json"])
.args(extra)
.arg("--cwd")
@@ -99,9 +99,9 @@ fn debug_cmd(
/// Poll up to 50×100ms for the per-session firehose at `path` to become non-empty
/// (its worker flushes asynchronously while the agent process stays alive), then
/// assert it carries first-party (`xai_grok`) content. Panics with the captured
/// assert it carries first-party (`xai_kigi`) content. Panics with the captured
/// stderr tail if it never fills. Shared by the live-agent tests.
async fn read_session_firehose_when_ready(path: &Path, client: &GrokStdioClient) -> String {
async fn read_session_firehose_when_ready(path: &Path, client: &KigiStdioClient) -> String {
let mut content = None;
for _ in 0..50 {
if let Ok(text) = std::fs::read_to_string(path)
@@ -121,7 +121,7 @@ async fn read_session_firehose_when_ready(path: &Path, client: &GrokStdioClient)
// The firehose filter routes first-party crate logs here; assert that rather
// than a bare non-empty check.
assert!(
content.contains("xai_grok"),
content.contains("xai_kigi"),
"session firehose {path:?} should contain first-party logs, got {} bytes",
content.len()
);
@@ -131,7 +131,7 @@ async fn read_session_firehose_when_ready(path: &Path, client: &GrokStdioClient)
/// `--debug` (headless) runs cleanly: arg-parsing + the master switch + tracing
/// init don't crash. Per-session routing + content is proven deterministically by
/// the live `agent` tests (incl. `debug_flag_master_switch_enables_firehose`); a
/// headless `grok -p` client is near-silent, so its lazily-opened firehose may
/// headless `kigi -p` client is near-silent, so its lazily-opened firehose may
/// legitimately stay empty here — file existence is intentionally not asserted.
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
@@ -145,7 +145,7 @@ async fn debug_flag_enables_firehose_without_crashing() {
let cmd = debug_cmd(&server, home.path(), workdir.path(), &["--debug"]);
let result = run_headless_with_cmd(cmd).await;
assert_headless_success(&result, "grok --debug headless", Some(&server));
assert_headless_success(&result, "kigi --debug headless", Some(&server));
assert_no_crashes(&result.stderr);
}
@@ -162,7 +162,7 @@ async fn no_debug_flag_writes_no_debug_dir() {
let cmd = debug_cmd(&server, home.path(), workdir.path(), &[]);
let result = run_headless_with_cmd(cmd).await;
assert_headless_success(&result, "grok headless (no --debug)", Some(&server));
assert_headless_success(&result, "kigi headless (no --debug)", Some(&server));
assert!(
firehose_txt_files(home.path()).is_empty(),
"no firehose *.txt expected without --debug, found: {:?}",
@@ -186,7 +186,7 @@ async fn agent_session_writes_named_session_file() {
let kigi_home = home.path().join(".kigi");
let kigi_home_str = kigi_home.to_string_lossy().into_owned();
let client = GrokStdioClient::spawn_with_home_and_env(
let client = KigiStdioClient::spawn_with_home_and_env(
&server,
workdir.path(),
home,
@@ -235,11 +235,11 @@ async fn debug_flag_master_switch_enables_firehose() {
let kigi_home = home.path().join(".kigi");
let kigi_home_str = kigi_home.to_string_lossy().into_owned();
// Drive `grok --debug agent stdio`: the master switch (which runs before
// Drive `kigi --debug agent stdio`: the master switch (which runs before
// the agent dispatch) must be what enables the firehose — NOT a direct
// KIGI_DEBUG_LOG env. The spawn helper clears inherited firehose toggles,
// so the `--debug` flag is the only thing that can enable logging here.
let client = GrokStdioClient::spawn_with_home_env_and_args(
let client = KigiStdioClient::spawn_with_home_env_and_args(
&server,
workdir.path(),
home,
@@ -292,7 +292,7 @@ async fn debug_file_flag_writes_single_file_and_bypasses_routing() {
);
let result = run_headless_with_cmd(cmd).await;
assert_headless_success(&result, "grok --debug-file", Some(&server));
assert_headless_success(&result, "kigi --debug-file", Some(&server));
assert_no_crashes(&result.stderr);
assert!(
explicit.exists(),
@@ -310,7 +310,7 @@ async fn debug_file_flag_writes_single_file_and_bypasses_routing() {
/// `KIGI_LOG_FILE=<path>` (no `--debug`) writes that exact file (back-compat).
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn grok_log_file_explicit_path_is_written() {
async fn kigi_log_file_explicit_path_is_written() {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
@@ -322,7 +322,7 @@ async fn grok_log_file_explicit_path_is_written() {
cmd.env("KIGI_LOG_FILE", &custom);
let result = run_headless_with_cmd(cmd).await;
assert_headless_success(&result, "grok KIGI_LOG_FILE=path", Some(&server));
assert_headless_success(&result, "kigi KIGI_LOG_FILE=path", Some(&server));
assert_no_crashes(&result.stderr);
assert!(
custom.exists(),
@@ -518,7 +518,7 @@ async fn doomed_then_reasoning_only_empty_coexist() {
/// `[doom_loop_recovery] enabled = true` in `config.toml` reaches the wire
/// through the real binary: the session TURN request (marked by
/// `x-grok-turn-idx`) carries the opt-in header. Aux side-queries the binary
/// `x-kigi-turn-idx`) carries the opt-in header. Aux side-queries the binary
/// also fires at `/v1/responses` (e.g. session-title generation) must NOT
/// carry it — they collect without the actor's retry loop, so an armed
/// abort there could only fail them, never resample. The recovery behavior
@@ -546,7 +546,7 @@ async fn headless_config_enables_doom_loop_check_header() {
)
.expect("write config.toml");
let mut cmd = tokio::process::Command::new(kigi_test_support::grok_binary());
let mut cmd = tokio::process::Command::new(kigi_test_support::kigi_binary());
cmd.args(["-p", "say hi", "--yolo", "--output-format", "json"])
.arg("--cwd")
.arg(workdir.path())
@@ -568,11 +568,11 @@ async fn headless_config_enables_doom_loop_check_header() {
.iter()
.filter(|e| e.method == "POST" && e.path.contains("/responses"))
.collect();
// The session turn carries `x-grok-turn-idx`; aux side-queries (session
// The session turn carries `x-kigi-turn-idx`; aux side-queries (session
// title, etc.) do not.
let (turns, aux): (Vec<_>, Vec<_>) = responses_posts
.into_iter()
.partition(|e| e.header("x-grok-turn-idx").is_some());
.partition(|e| e.header("x-kigi-turn-idx").is_some());
assert!(
!turns.is_empty(),
"no session turn POST /v1/responses logged; requests:\n{}",
@@ -580,7 +580,7 @@ async fn headless_config_enables_doom_loop_check_header() {
);
for turn in turns {
assert_eq!(
turn.header("x-grok-doom-loop-check"),
turn.header("x-kigi-doom-loop-check"),
Some("true"),
"[doom_loop_recovery] enabled must reach the turn request header; requests:\n{}",
server.request_log_summary()
@@ -588,7 +588,7 @@ async fn headless_config_enables_doom_loop_check_header() {
}
for side_query in aux {
assert_eq!(
side_query.header("x-grok-doom-loop-check"),
side_query.header("x-kigi-doom-loop-check"),
None::<&str>,
"the session policy must not leak into aux side-query clients; requests:\n{}",
server.request_log_summary()
@@ -29,7 +29,7 @@ async fn responses_api_reasoning_only_is_classified_as_reasoning_only() {
"/v1/responses",
ScriptedResponse::sse(responses_api_reasoning_only_events(
"let me think carefully about this",
"grok-test",
"kigi-test",
)),
);
let client = create_test_client(&server.url(), ApiBackend::Responses);
@@ -18,7 +18,7 @@ async fn create_test_session(storage: &JsonlStorageAdapter, session_id: &str, cw
cwd: cwd.to_string(),
};
let model_id = acp::ModelId::new("grok-code-fast-1");
let model_id = acp::ModelId::new("kigi-code-fast-1");
storage.init_session(&info, model_id).await.unwrap();
// Add some chat messages
@@ -58,7 +58,7 @@ async fn test_fork_session_creates_new_session_with_parent_tracking() {
let options = kigi_shell::session::storage::CopySessionOptions {
parent_session_id: Some("source-session-123".to_string()),
new_model_id: Some("grok-3".to_string()),
new_model_id: Some("kigi-3".to_string()),
target_prompt_index: None,
..Default::default()
};
@@ -77,7 +77,7 @@ async fn test_fork_session_creates_new_session_with_parent_tracking() {
assert_eq!(loaded.summary.info.id.to_string(), "fork-session-456");
assert_eq!(loaded.summary.info.cwd, "/new/path");
assert_eq!(loaded.summary.current_model_id, acp::ModelId::new("grok-3"));
assert_eq!(loaded.summary.current_model_id, acp::ModelId::new("kigi-3"));
assert_eq!(
loaded.summary.parent_session_id,
Some("source-session-123".to_string())
@@ -1,6 +1,6 @@
//! End-to-end test for the global `[models]` defaults.
//!
//! Runs the built grok binary against the mock inference server with a
//! Runs the built kigi binary against the mock inference server with a
//! caller-owned `$KIGI_SHARE_DIR` whose `config.toml` sets every global `[models]`
//! default. Asserts the turn succeeds with all of them set and that the
//! wire-observable one — `extra_headers` — reaches the `/v1/chat/completions`
@@ -47,7 +47,7 @@ stream_tool_calls = true
)
.expect("write config.toml");
let mut cmd = tokio::process::Command::new(grok_binary());
let mut cmd = tokio::process::Command::new(kigi_binary());
cmd.args(["-p", "say hi", "--yolo", "--output-format", "json"])
.arg("--cwd")
.arg(workdir.path())
@@ -4,7 +4,7 @@
//! Scenario (mirrors the field report "clients see `unknown session id` after
//! the leader dies"):
//!
//! 1. Two stdio clients (`grok agent --leader stdio`) share one leader.
//! 1. Two stdio clients (`kigi agent --leader stdio`) share one leader.
//! 2. Each creates its own session and completes a prompt round-trip.
//! 3. The leader is killed with SIGKILL (crash, no graceful shutdown).
//! 4. Each client's bridge must reconnect (re-electing / spawning a fresh
@@ -1024,7 +1024,7 @@ async fn test_session_new_valid_default_model_injected() {
ClientMode::Stdio,
ClientCapabilities {
yolo_mode: false,
default_model: Some("grok-3-fast".to_string()),
default_model: Some("kigi-3-fast".to_string()),
..Default::default()
},
)
@@ -1039,7 +1039,7 @@ async fn test_session_new_valid_default_model_injected() {
// modelId should be injected from default_model
let meta = &json["params"]["_meta"];
assert_eq!(meta["modelId"], "grok-3-fast");
assert_eq!(meta["modelId"], "kigi-3-fast");
client.cancel();
cancel.cancel();
@@ -1216,7 +1216,7 @@ async fn test_two_clients_session_isolation() {
/// Multi-client model switch: when one TUI client switches models on a
/// session shared with another TUI client, the leader must fan the
/// `x.ai/session_notification` (carrying the `ModelChanged` update) out
/// `kigi/session_notification` (carrying the `ModelChanged` update) out
/// to **every** subscriber of that session — not just the invoker — so
/// the follower client mirrors the new model in its UI.
///
@@ -1241,7 +1241,7 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
// Two TUIs connected to the same leader, sharing one session.
let mut invoker = LeaderClient::connect(
sock_path.clone(),
"grok-tui-A",
"kigi-tui-A",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -1249,7 +1249,7 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
.unwrap();
let mut follower = LeaderClient::connect(
sock_path,
"grok-tui-B",
"kigi-tui-B",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -1289,7 +1289,7 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
// Invoker sends `session/setModel` for the shared session.
invoker
.send(format!(
r#"{{"jsonrpc":"2.0","id":42,"method":"session/setModel","params":{{"sessionId":"{}","modelId":"grok-4"}}}}"#,
r#"{{"jsonrpc":"2.0","id":42,"method":"session/setModel","params":{{"sessionId":"{}","modelId":"kigi-4"}}}}"#,
shared_sid
))
.unwrap();
@@ -1297,7 +1297,7 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
let json: serde_json::Value = serde_json::from_str(&received).unwrap();
let setmodel_ns_id = json["id"].as_str().unwrap().to_string();
assert_eq!(json["method"], "session/setModel");
assert_eq!(json["params"]["modelId"], "grok-4");
assert_eq!(json["params"]["modelId"], "kigi-4");
// Simulate the agent's two outputs for a successful switch:
//
@@ -1310,12 +1310,12 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
// BEFORE the response in `model_switch::apply`, so it must arrive at
// each subscriber's recv() first.
let broadcast = format!(
r#"{{"jsonrpc":"2.0","method":"x.ai/session_notification","params":{{"sessionId":"{}","update":{{"sessionUpdate":"model_changed","model_id":"grok-4","reasoning_effort":"high"}}}}}}"#,
r#"{{"jsonrpc":"2.0","method":"kigi/session_notification","params":{{"sessionId":"{}","update":{{"sessionUpdate":"model_changed","model_id":"kigi-4","reasoning_effort":"high"}}}}}}"#,
shared_sid
);
response_tx.send(broadcast.clone()).unwrap();
let response = format!(
r#"{{"jsonrpc":"2.0","result":{{"meta":{{"model":"grok-4"}}}},"id":"{}"}}"#,
r#"{{"jsonrpc":"2.0","result":{{"meta":{{"model":"kigi-4"}}}},"id":"{}"}}"#,
setmodel_ns_id
);
response_tx.send(response).unwrap();
@@ -1335,10 +1335,10 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
.expect("timeout waiting for broadcast on invoker")
.expect("invoker channel closed");
let inv1: serde_json::Value = serde_json::from_str(&invoker_msg1).unwrap();
assert_eq!(inv1["method"], "x.ai/session_notification");
assert_eq!(inv1["method"], "kigi/session_notification");
assert_eq!(inv1["params"]["sessionId"], shared_sid);
assert_eq!(inv1["params"]["update"]["sessionUpdate"], "model_changed");
assert_eq!(inv1["params"]["update"]["model_id"], "grok-4");
assert_eq!(inv1["params"]["update"]["model_id"], "kigi-4");
assert_eq!(inv1["params"]["update"]["reasoning_effort"], "high");
let invoker_msg2 = tokio::time::timeout(Duration::from_secs(2), invoker.recv())
@@ -1350,7 +1350,7 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
inv2["id"], 42,
"response id must be restored to the invoker's original"
);
assert_eq!(inv2["result"]["meta"]["model"], "grok-4");
assert_eq!(inv2["result"]["meta"]["model"], "kigi-4");
// --- Follower: must receive the broadcast (this is the fix — before
// this notification existed, the follower's status bar / `/model`
@@ -1365,10 +1365,10 @@ async fn test_set_model_broadcasts_to_session_subscribers() {
)
.expect("follower channel closed");
let f: serde_json::Value = serde_json::from_str(&follower_msg).unwrap();
assert_eq!(f["method"], "x.ai/session_notification");
assert_eq!(f["method"], "kigi/session_notification");
assert_eq!(f["params"]["sessionId"], shared_sid);
assert_eq!(f["params"]["update"]["sessionUpdate"], "model_changed");
assert_eq!(f["params"]["update"]["model_id"], "grok-4");
assert_eq!(f["params"]["update"]["model_id"], "kigi-4");
assert_eq!(f["params"]["update"]["reasoning_effort"], "high");
// Follower must NOT see the namespaced setModel response — the
@@ -1403,7 +1403,7 @@ async fn test_capabilities_not_injected_into_non_session_new() {
ClientMode::Stdio,
ClientCapabilities {
yolo_mode: true,
default_model: Some("grok-3-fast".to_string()),
default_model: Some("kigi-3-fast".to_string()),
..Default::default()
},
)
@@ -1533,7 +1533,7 @@ async fn test_cancel_prompt_id_meta_passes_through_with_two_clients() {
let client_a = LeaderClient::connect(
sock_path.clone(),
"grok-pager",
"kigi-pager",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -1541,7 +1541,7 @@ async fn test_cancel_prompt_id_meta_passes_through_with_two_clients() {
.unwrap();
let client_b = LeaderClient::connect(
sock_path,
"grok-pager",
"kigi-pager",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -1611,14 +1611,14 @@ async fn test_extension_method_roundtrip() {
.unwrap();
// Send an extension method call (e.g., fuzzy search open)
let ext_call = r#"{"jsonrpc":"2.0","id":50,"method":"_x.ai/search/fuzzy/open","params":{"sessionId":"sess-123","hidden":false}}"#;
let ext_call = r#"{"jsonrpc":"2.0","id":50,"method":"_kigi/search/fuzzy/open","params":{"sessionId":"sess-123","hidden":false}}"#;
client.send(ext_call.to_string()).unwrap();
let received = acp_rx.recv().await.unwrap();
let json: serde_json::Value = serde_json::from_str(&received).unwrap();
// Method should be preserved, ID should be namespaced
assert_eq!(json["method"], "_x.ai/search/fuzzy/open");
assert_eq!(json["method"], "_kigi/search/fuzzy/open");
let namespaced_id = json["id"].as_str().unwrap();
assert!(namespaced_id.contains(ID_NAMESPACE_SEP));
assert!(namespaced_id.ends_with("|50"));
@@ -1795,7 +1795,7 @@ async fn test_session_ownership_cleanup_on_disconnect() {
// in sync. Also verifies the eviction was actually sent.
let eviction = acp_rx.recv().await.unwrap();
let eviction_json: serde_json::Value = serde_json::from_str(&eviction).unwrap();
assert_eq!(eviction_json["method"], "x.ai/internal/evict_sessions");
assert_eq!(eviction_json["method"], "kigi/internal/evict_sessions");
// Connect a NEW client — server should still be running
let mut client2 = LeaderClient::connect(
@@ -1824,7 +1824,7 @@ async fn test_session_ownership_cleanup_on_disconnect() {
// client2 should NOT receive the dead-session notification.
// Send a second notification without a sessionId — this one SHOULD
// arrive via fallback routing, proving client2 is alive and connected.
let probe = r#"{"jsonrpc":"2.0","method":"x.ai/probe","params":{"ping":true}}"#;
let probe = r#"{"jsonrpc":"2.0","method":"kigi/probe","params":{"ping":true}}"#;
response_tx.send(probe.to_string()).unwrap();
let recv = tokio::time::timeout(Duration::from_secs(2), client2.recv())
@@ -1857,7 +1857,7 @@ async fn test_code_nav_capable_client_gets_true_injected_into_session_new() {
// Web client that advertised code-nav capability during registration.
let web_client = LeaderClient::connect(
sock_path,
"grok-web",
"kigi-web",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: true,
@@ -1894,7 +1894,7 @@ async fn test_non_code_nav_client_gets_false_injected_into_session_new() {
// TUI client with no code-nav capability.
let tui_client = LeaderClient::connect(
sock_path,
"grok-tui",
"kigi-tui",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: false,
@@ -1933,7 +1933,7 @@ async fn test_leader_code_nav_client_isolation() {
// Web client with code-nav capability.
let web_client = LeaderClient::connect(
sock_path.clone(),
"grok-web",
"kigi-web",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: true,
@@ -1946,7 +1946,7 @@ async fn test_leader_code_nav_client_isolation() {
// TUI client without code-nav capability.
let tui_client = LeaderClient::connect(
sock_path,
"grok-tui",
"kigi-tui",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: false,
@@ -1994,7 +1994,7 @@ async fn test_code_nav_capability_injected_into_session_load() {
let web_client = LeaderClient::connect(
sock_path,
"grok-web",
"kigi-web",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: true,
@@ -2021,7 +2021,7 @@ async fn test_code_nav_capability_injected_into_session_load() {
cancel.cancel();
}
/// Verify that an `x.ai/code/status` extension request is forwarded to the
/// Verify that an `kigi/code/status` extension request is forwarded to the
/// agent with the correct method, sessionId, and cwd in the params.
///
/// This tests the routing boundary between leader and agent for the
@@ -2033,7 +2033,7 @@ async fn test_code_status_ext_request_forwarded_to_agent() {
let web_client = LeaderClient::connect(
sock_path,
"grok-web",
"kigi-web",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: true,
@@ -2043,15 +2043,15 @@ async fn test_code_status_ext_request_forwarded_to_agent() {
.await
.unwrap();
// Send x.ai/code/status with a sessionId — the leader must forward it to the agent.
let status_req = r#"{"jsonrpc":"2.0","id":42,"method":"extensions/ext","params":{"method":"x.ai/code/status","params":{"sessionId":"sess-web-1","cwd":"/repo"}}}"#;
// Send kigi/code/status with a sessionId — the leader must forward it to the agent.
let status_req = r#"{"jsonrpc":"2.0","id":42,"method":"extensions/ext","params":{"method":"kigi/code/status","params":{"sessionId":"sess-web-1","cwd":"/repo"}}}"#;
web_client.send(status_req.to_string()).unwrap();
let forwarded = acp_rx.recv().await.unwrap();
let json: serde_json::Value = serde_json::from_str(&forwarded).unwrap();
assert_eq!(json["method"], "extensions/ext");
assert_eq!(json["params"]["method"], "x.ai/code/status");
assert_eq!(json["params"]["method"], "kigi/code/status");
assert_eq!(json["params"]["params"]["sessionId"], "sess-web-1");
assert_eq!(json["params"]["params"]["cwd"], "/repo");
@@ -2284,7 +2284,7 @@ async fn test_connect_waits_for_leader_ready() {
let connect_start = tokio::time::Instant::now();
let mut client = LeaderClient::connect(
sock_path,
"grok-tui",
"kigi-tui",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -2337,7 +2337,7 @@ async fn test_connect_waits_for_leader_ready() {
// ── Version mismatch notification ────────────────────────────────────
/// Integration test: a connected client receives `x.ai/leader/version_mismatch`
/// Integration test: a connected client receives `kigi/leader/version_mismatch`
/// when its `client_version` differs from the leader's version.
///
/// Uses `leader_version_override` so the test bypasses the `"unknown"` constant
@@ -2406,7 +2406,7 @@ async fn test_version_mismatch_notification_sent_to_client() {
.expect("channel closed");
let json: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert_eq!(json["method"], "x.ai/leader/version_mismatch");
assert_eq!(json["method"], "kigi/leader/version_mismatch");
assert_eq!(json["params"]["clientVersion"], "test-client-0.1.157");
assert_eq!(json["params"]["leaderVersion"], "test-leader-0.1.150");
@@ -2729,7 +2729,7 @@ async fn test_initialize_injected_when_not_first_message() {
let client = LeaderClient::connect(
sock_path,
"grok-tui",
"kigi-tui",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -2762,7 +2762,7 @@ async fn test_initialize_injected_when_not_first_message() {
.and_then(|v| v.as_str());
assert_eq!(
client_id,
Some("grok-tui"),
Some("kigi-tui"),
"clientIdentifier must be injected into initialize even when it is not the first message"
);
@@ -2782,7 +2782,7 @@ async fn test_leader_code_nav_isolation_end_to_end() {
// Web client with code-nav capability.
let web_client = LeaderClient::connect(
sock_path.clone(),
"grok-web",
"kigi-web",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: true,
@@ -2795,7 +2795,7 @@ async fn test_leader_code_nav_isolation_end_to_end() {
// TUI client without code-nav capability.
let tui_client = LeaderClient::connect(
sock_path,
"grok-tui",
"kigi-tui",
ClientMode::Stdio,
ClientCapabilities {
code_nav_enabled: false,
@@ -2824,13 +2824,13 @@ async fn test_leader_code_nav_isolation_end_to_end() {
serde_json::json!(false)
);
// Web client sends x.ai/code/status (the primary non-starting code-nav call).
let status_with_session = r#"{"jsonrpc":"2.0","id":10,"method":"extensions/ext","params":{"method":"x.ai/code/status","params":{"sessionId":"web-session","cwd":"/repo"}}}"#;
// Web client sends kigi/code/status (the primary non-starting code-nav call).
let status_with_session = r#"{"jsonrpc":"2.0","id":10,"method":"extensions/ext","params":{"method":"kigi/code/status","params":{"sessionId":"web-session","cwd":"/repo"}}}"#;
web_client.send(status_with_session.to_string()).unwrap();
let status_fwd = acp_rx.recv().await.unwrap();
let status_json: serde_json::Value = serde_json::from_str(&status_fwd).unwrap();
assert_eq!(status_json["params"]["method"], "x.ai/code/status");
assert_eq!(status_json["params"]["method"], "kigi/code/status");
assert_eq!(status_json["params"]["params"]["sessionId"], "web-session");
web_client.cancel();
@@ -3146,7 +3146,7 @@ async fn test_hung_agent_leaves_transport_healthy_and_forwards_cancel() {
assert_eq!(cancel_json["method"], "session/cancel");
// Unrelated traffic still round-trips on the same connection.
let probe = r#"{"jsonrpc":"2.0","method":"x.ai/probe","params":{"ping":true}}"#;
let probe = r#"{"jsonrpc":"2.0","method":"kigi/probe","params":{"ping":true}}"#;
response_tx.send(probe.to_string()).unwrap();
let recv = tokio::time::timeout(Duration::from_secs(2), client.recv())
.await
@@ -3209,7 +3209,7 @@ async fn test_sever_mid_rpc_orphans_response_and_replay_recovers() {
// signal that the server processed the disconnect.
let evict = acp_rx.recv().await.unwrap();
let evict_json: serde_json::Value = serde_json::from_str(&evict).unwrap();
assert_eq!(evict_json["method"], "x.ai/internal/evict_sessions");
assert_eq!(evict_json["method"], "kigi/internal/evict_sessions");
// The agent completes the turn anyway: durable terminal notification plus
// the RPC response addressed to the dead client.
@@ -5,14 +5,14 @@
//! Binaries are resolved per role:
//! - `KIGI_BINARY_LEADER` — the binary that elects the initial leader
//! (typically the latest released stable, e.g. fetched from
//! `https://storage.googleapis.com/grok-build-public-artifacts/cli/grok-<ver>-linux-x86_64`).
//! `https://storage.googleapis.com/kigi-public-artifacts/cli/kigi-<ver>-linux-x86_64`).
//! - `KIGI_BINARY_CLIENT` — the second client (typically a freshly built main).
//!
//! All tests are `#[ignore]`d: they need two pre-built binaries and spawn real
//! leader subprocesses. On-demand today — no CI lane runs them; invoke with:
//!
//! ```bash
//! KIGI_BINARY_LEADER=/path/to/grok-old KIGI_BINARY_CLIENT=/path/to/grok-new \
//! KIGI_BINARY_LEADER=/path/to/kigi-old KIGI_BINARY_CLIENT=/path/to/kigi-new \
//! cargo test -p kigi-shell --test test_leader_version_skew -- --ignored --nocapture
//! ```
@@ -202,7 +202,7 @@ async fn old_client_adopts_new_leader_and_still_functions() {
.expect("old client prompt on new leader failed");
// The leader records the client/leader version mismatch (the
// x.ai/leader/version_mismatch notification's server-side warn).
// kigi/leader/version_mismatch notification's server-side warn).
let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
let mut saw_mismatch = false;
while tokio::time::Instant::now() < deadline {
@@ -221,7 +221,7 @@ async fn old_client_adopts_new_leader_and_still_functions() {
.await;
}
/// `grok update`'s relaunch signal against a REAL old leader: connect,
/// `kigi update`'s relaunch signal against a REAL old leader: connect,
/// require `relaunch_v1`, send `RelaunchForUpdate`, and the leader exits so
/// the surviving client re-elects. Mirrors the private
/// `signal_leaders_to_relaunch` in `kigi-bin/src/main.rs` (which is
@@ -260,7 +260,7 @@ async fn relaunch_for_update_drives_real_old_leader_to_exit() {
// The update-signal body, against the sandboxed socket.
let control = LeaderClient::connect(
home.path().join(".kigi").join("leader.sock"),
"grok-pager-update",
"kigi-pager-update",
ClientMode::Stdio,
ClientCapabilities::default(),
)
@@ -242,7 +242,7 @@ fn rule(action: RuleAction, pattern: &str) -> PermissionRule {
#[tokio::test]
#[serial]
async fn mcp_tool_grant_persists_and_short_circuits_next_request() {
run_actor_test(ClientType::GrokPager, |handle, gw, cwd| async move {
run_actor_test(ClientType::KigiPager, |handle, gw, cwd| async move {
// First request prompts; user picks tool-scope.
gw.expect_allow_always_mcp_tool("linear__list");
let d = request(&handle, mcp("linear__list"), "1").await;
@@ -300,7 +300,7 @@ async fn mcp_tool_grant_persists_and_short_circuits_next_request() {
#[tokio::test]
#[serial]
async fn fallback_client_plain_allow_always_persists_mcp_tool() {
// Regression: Generic / GrokWeb / Extension clients
// Regression: Generic / KigiWeb / Extension clients
// submit the legacy `"always-allow"` option id. The prompter maps that
// to plain `PromptOutcome::AllowAlways`, and the manager's plain arm
// must persist tool-scope into `allowed_mcp_tools`.
@@ -362,7 +362,7 @@ async fn policy_ask_suppresses_mcp_tool_allowlist() {
make_session_id(),
gw.sender.clone(),
cwd.clone(),
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
vec![], // deny_read_globs
vec![],
@@ -412,7 +412,7 @@ async fn policy_ask_suppresses_mcp_server_allowlist() {
make_session_id(),
gw.sender.clone(),
cwd.clone(),
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
vec![], // deny_read_globs
vec![],
@@ -456,7 +456,7 @@ async fn policy_deny_takes_precedence_over_mcp_allowlist() {
make_session_id(),
gw.sender.clone(),
cwd.clone(),
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
vec![], // deny_read_globs
vec![],
@@ -482,7 +482,7 @@ async fn policy_allow_short_circuits_before_mcp_allowlist() {
let policy = PermissionConfig::new(vec![rule(RuleAction::Allow, "linear__*")]);
run_actor_test_with_policy(
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
|handle, _gw, _cwd| async move {
let d = request(&handle, mcp("linear__list"), "1").await;
@@ -499,7 +499,7 @@ async fn empty_server_prefix_falls_back_to_tool_scope() {
// somehow makes it through, the prompter must downgrade to tool-scope
// and persist via `AllowAlwaysMcpTool` — never write an empty server
// prefix into `allowed_mcp_servers`.
run_actor_test(ClientType::GrokPager, |handle, gw, cwd| async move {
run_actor_test(ClientType::KigiPager, |handle, gw, cwd| async move {
let meta = serde_json::json!({
"kind": "server",
"server": "",
@@ -531,7 +531,7 @@ async fn allow_always_mcp_tool_ignores_client_supplied_tool_name() {
// only. The manager MUST persist the name from `AccessKind::MCPTool`
// so a buggy or malicious client cannot whitelist a different tool
// than the one the user saw in the prompt.
run_actor_test(ClientType::GrokPager, |handle, gw, cwd| async move {
run_actor_test(ClientType::KigiPager, |handle, gw, cwd| async move {
// Request approves `linear__list`, but the response claims a
// different tool name (e.g. `notion__fetch`).
let meta = serde_json::json!({
@@ -571,7 +571,7 @@ async fn allow_always_mcp_server_rejects_mismatched_prefix() {
// canonical server prefix derived from the access kind. On mismatch,
// the manager downgrades to tool-scope on the access-kind name -- the
// smallest blast radius the user actually approved.
run_actor_test(ClientType::GrokPager, |handle, gw, cwd| async move {
run_actor_test(ClientType::KigiPager, |handle, gw, cwd| async move {
// Approve `linear__list` (canonical server prefix is `linear`),
// but the client claims `notion` as the server.
let meta = serde_json::json!({
@@ -613,7 +613,7 @@ async fn allow_always_mcp_server_rejects_mismatched_prefix() {
async fn allow_always_mcp_server_persists_canonical_prefix_on_match() {
// Sanity: a client that supplies the correct canonical prefix
// succeeds (this is the common case post-fix).
run_actor_test(ClientType::GrokPager, |handle, gw, cwd| async move {
run_actor_test(ClientType::KigiPager, |handle, gw, cwd| async move {
let meta = serde_json::json!({
"kind": "server",
"server": "linear",
@@ -645,7 +645,7 @@ async fn allow_always_mcp_server_downgrades_when_access_has_no_separator() {
// malformed `ToolInput::MCPTool`), the canonical prefix is None and
// server-scope is unreachable. The manager downgrades to tool-scope
// on the raw access name rather than persisting the client prefix.
run_actor_test(ClientType::GrokPager, |handle, gw, cwd| async move {
run_actor_test(ClientType::KigiPager, |handle, gw, cwd| async move {
let meta = serde_json::json!({
"kind": "server",
"server": "linear",
@@ -681,7 +681,7 @@ async fn dont_ask_policy_denies_without_prompting() {
// No scripted gateway responses: if the manager tried to prompt,
// the gateway would block forever, proving dont_ask short-circuits.
run_actor_test_with_policy(
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
|handle, _gw, _cwd| async move {
let d = request(&handle, mcp("linear__list"), "1").await;
@@ -716,7 +716,7 @@ async fn deny_rule_enforced_in_yolo_mode_bash() {
}]);
run_actor_test_full(
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
true,
|handle, _gw, _cwd| async move {
@@ -752,7 +752,7 @@ async fn deny_rule_enforced_in_yolo_mode_mcp() {
}]);
run_actor_test_full(
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
true,
|handle, _gw, _cwd| async move {
@@ -783,7 +783,7 @@ async fn deny_rule_enforced_in_yolo_mode_edit() {
}]);
run_actor_test_full(
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
true,
|handle, _gw, _cwd| async move {
@@ -814,7 +814,7 @@ async fn deny_rule_enforced_in_yolo_mode_web_fetch() {
}]);
run_actor_test_full(
ClientType::GrokPager,
ClientType::KigiPager,
Some(policy),
true,
|handle, _gw, _cwd| async move {
@@ -848,7 +848,7 @@ async fn deny_rule_enforced_in_yolo_mode_web_fetch() {
#[serial]
async fn yolo_mode_without_deny_rules_approves_everything() {
run_actor_test_full(
ClientType::GrokPager,
ClientType::KigiPager,
None,
true,
|handle, _gw, _cwd| async move {
@@ -71,7 +71,7 @@ async fn test_refusal_turn_completes_with_single_messages_request() {
with_local_set(|| async {
let server = refusal_messages_server().await;
let workdir = git_workdir();
let client = GrokStdioClient::spawn(&server, workdir.path()).await;
let client = KigiStdioClient::spawn(&server, workdir.path()).await;
client.initialize_with_timeout().await;
let session_id = client
@@ -255,7 +255,7 @@ async fn test_chat_completions_streaming_tool_calls() {
let server = MockInferenceServer::start().await.unwrap();
server.enqueue_response(
"/v1/chat/completions",
ScriptedResponse::sse(chat_completion_tool_call_stream(tool_calls, "grok-test")),
ScriptedResponse::sse(chat_completion_tool_call_stream(tool_calls, "kigi-test")),
);
let client = create_test_client(&server.url(), ApiBackend::ChatCompletions);
@@ -296,7 +296,7 @@ async fn test_chat_completions_with_reasoning() {
ScriptedResponse::sse(chat_completion_with_reasoning_stream(
"Let me think about this",
"The answer is 42",
"grok-test",
"kigi-test",
)),
);
let client = create_test_client(&server.url(), ApiBackend::ChatCompletions);
@@ -344,7 +344,7 @@ async fn chat_completions_collect_synthesizes_reasoning_sibling() {
ScriptedResponse::sse(chat_completion_with_reasoning_stream(
"Let me think about this",
"The answer is 42",
"grok-test",
"kigi-test",
)),
);
let client = create_test_client(&server.url(), ApiBackend::ChatCompletions);
@@ -453,7 +453,7 @@ async fn chat_completions_upgrade_folds_reconstructed_reasoning_into_request() {
);
}
/// Upgrade path, grok-build / Responses API: a legacy session whose
/// Upgrade path, kigi / Responses API: a legacy session whose
/// assistant carries inline `reasoning: {text, encrypted, id}` must, on
/// load, reconstruct a sibling Reasoning item that round-trips back to
/// the Responses API as a **typed** `reasoning` input item — `summary`,
@@ -462,7 +462,7 @@ async fn chat_completions_upgrade_folds_reconstructed_reasoning_into_request() {
/// through `reasoning_item_text`.
#[tokio::test]
async fn responses_upgrade_roundtrips_reconstructed_reasoning_as_typed_input() {
// 1. Seed a legacy grok-build chat_history.jsonl (inline reasoning
// 1. Seed a legacy kigi chat_history.jsonl (inline reasoning
// with encrypted_content + id — the older shape).
let dir = tempfile::tempdir().unwrap();
std::fs::write(
@@ -472,7 +472,7 @@ async fn responses_upgrade_roundtrips_reconstructed_reasoning_as_typed_input() {
"\n",
r#"{"type":"user","content":[{"type":"text","text":"q1"}]}"#,
"\n",
r#"{"type":"assistant","content":"a1","reasoning":{"text":"legacy grok-build reasoning","encrypted":"ENC_BLOB_xyz","id":"rs_grokbuild_legacy"},"model_id":"grok-build"}"#,
r#"{"type":"assistant","content":"a1","reasoning":{"text":"legacy kigi reasoning","encrypted":"ENC_BLOB_xyz","id":"rs_kigibuild_legacy"},"model_id":"kigi"}"#,
"\n",
),
)
@@ -513,7 +513,7 @@ async fn responses_upgrade_roundtrips_reconstructed_reasoning_as_typed_input() {
});
assert_eq!(
reasoning.get("id").and_then(Value::as_str),
Some("rs_grokbuild_legacy"),
Some("rs_kigibuild_legacy"),
"reasoning id preserved"
);
assert_eq!(
@@ -528,7 +528,7 @@ async fn responses_upgrade_roundtrips_reconstructed_reasoning_as_typed_input() {
);
assert_eq!(
summary[0].get("text").and_then(Value::as_str),
Some("legacy grok-build reasoning")
Some("legacy kigi reasoning")
);
}
@@ -550,7 +550,7 @@ async fn messages_upgrade_emits_reconstructed_reasoning_as_thinking_block() {
"\n",
r#"{"type":"user","content":[{"type":"text","text":"q1"}]}"#,
"\n",
r#"{"type":"assistant","content":"a1","reasoning":{"text":"legacy anthropic thinking","encrypted":"SIGNATURE_abc","id":""},"model_id":"grok-4.5"}"#,
r#"{"type":"assistant","content":"a1","reasoning":{"text":"legacy anthropic thinking","encrypted":"SIGNATURE_abc","id":""},"model_id":"kigi-4.5"}"#,
"\n",
),
)
@@ -738,7 +738,7 @@ async fn test_responses_api_streaming_tool_call() {
"call_xyz789",
"bash",
r#"{"command": "ls -la"}"#,
"grok-test",
"kigi-test",
)),
);
let client = create_test_client(&server.url(), ApiBackend::Responses);
@@ -781,7 +781,7 @@ async fn test_responses_api_with_reasoning_and_encrypted_content() {
"Let me think step by step about this problem.",
Some("enc_base64_encrypted_reasoning_chain_data"),
"The answer based on my reasoning is 42.",
"grok-test",
"kigi-test",
)),
);
let client = create_test_client(&server.url(), ApiBackend::Responses);
@@ -848,7 +848,7 @@ async fn test_responses_api_reasoning_without_encrypted() {
"I need to analyze the code carefully.",
None, // No encrypted content
"Here is my analysis.",
"grok-test",
"kigi-test",
)),
);
let client = create_test_client(&server.url(), ApiBackend::Responses);
@@ -955,7 +955,7 @@ async fn test_stream_error_during_streaming() {
"id": "chatcmpl-test123",
"object": "chat.completion.chunk",
"created": 1234567890,
"model": "grok-test",
"model": "kigi-test",
"choices": [{
"index": 0,
"delta": {"role": "assistant", "content": "Hello"},
@@ -1089,8 +1089,11 @@ async fn test_request_includes_headers() {
let request = server.requests().pop().unwrap();
assert_eq!(request.header("authorization"), Some("Bearer test-api-key"));
assert_eq!(request.header("x-grok-conv-id"), Some("conv-12345"));
assert_eq!(request.header("x-grok-req-id"), Some("req-67890"));
// PRD F3: auth is a plain bearer — the legacy proxy's tracking marker
// headers are never sent on the wire, even when conv/req ids are set on
// the request (they remain client-internal plumbing).
assert_eq!(request.header("x-kigi-conv-id"), None);
assert_eq!(request.header("x-kigi-req-id"), None);
}
/// The session writes the resolved `x-compaction-at` value into
@@ -1179,7 +1182,7 @@ async fn test_responses_api_request_format() {
}
/// The sampler owns the doom-loop opt-in: setting
/// `SamplerConfig::doom_loop_recovery` puts `x-grok-doom-loop-check` on the
/// `SamplerConfig::doom_loop_recovery` puts `x-kigi-doom-loop-check` on the
/// wire AND arms the collector, and the server's named check event is
/// absorbed mid-stream without disturbing the typed event flow.
#[tokio::test]
@@ -1216,7 +1219,7 @@ async fn test_doom_loop_check_enabled_sends_header_and_absorbs_check_event() {
let logged = server.requests().pop().unwrap();
assert!(logged.path.contains("/responses"));
assert_eq!(logged.header("x-grok-doom-loop-check"), Some("true"));
assert_eq!(logged.header("x-kigi-doom-loop-check"), Some("true"));
}
/// With the check disabled no header goes on the wire, and check frames from
@@ -1257,7 +1260,7 @@ async fn test_doom_loop_check_disabled_sends_no_header_and_drops_check_frames()
let logged = server.requests().pop().unwrap();
assert!(logged.path.contains("/responses"));
assert_eq!(logged.header("x-grok-doom-loop-check"), None);
assert_eq!(logged.header("x-kigi-doom-loop-check"), None);
}
// ============================================================================
@@ -6,7 +6,7 @@
//! session's `subagents/` dir and flips any stale `running` meta (not tracked by
//! the live coordinator) to `cancelled` (mechanism A, the meta pass).
//!
//! This test spawns a real `grok agent stdio` process, seeds an orphaned
//! This test spawns a real `kigi agent stdio` process, seeds an orphaned
//! `running` meta on disk, resumes the session, and asserts the meta was
//! reconciled to `cancelled`.
//!
@@ -57,7 +57,7 @@ async fn resume_reconciles_orphaned_running_subagent() {
let workdir = git_workdir();
// Phase 1: create a real session, then take its home so we can seed it.
let mut writer = GrokStdioClient::spawn(&server, workdir.path()).await;
let mut writer = KigiStdioClient::spawn(&server, workdir.path()).await;
writer.initialize_with_timeout().await;
let session_id = writer.create_session_with_timeout(workdir.path()).await;
let shared_home = writer.take_home();
@@ -66,7 +66,7 @@ async fn resume_reconciles_orphaned_running_subagent() {
// Simulate a crash: inject a subagent meta left `running` on disk (no
// terminal write, no SubagentFinished) — exactly what a dead process
// leaves behind.
// GrokStdioClient sets HOME=<temp>; the binary uses <HOME>/.kigi as KIGI_SHARE_DIR.
// KigiStdioClient sets HOME=<temp>; the binary uses <HOME>/.kigi as KIGI_SHARE_DIR.
let kigi_home = shared_home.path().join(".kigi");
let session_dir = locate_session_dir(&kigi_home, session_id.0.as_ref());
let sub_id = "sa-orphan";
@@ -89,7 +89,7 @@ async fn resume_reconciles_orphaned_running_subagent() {
.unwrap();
// Phase 2: resume in a fresh process. `load_session` runs the reconcile.
let reader = GrokStdioClient::spawn_with_home(&server, workdir.path(), shared_home).await;
let reader = KigiStdioClient::spawn_with_home(&server, workdir.path(), shared_home).await;
reader.initialize_with_timeout().await;
let _ = reader
.load_session_with_timeout(&session_id, workdir.path())
@@ -5,7 +5,7 @@
//! creation — not only after an explicit model/effort switch (the old
//! behavior, which left the field absent for sessions that never switched).
//!
//! Each test spawns a real `grok agent stdio` process against a mock
//! Each test spawns a real `kigi agent stdio` process against a mock
//! inference server and asserts on the persisted `summary.json`.
//!
//! Run locally:
@@ -60,10 +60,10 @@ async fn test_fresh_session_persists_reasoning_effort() {
// user config override (the same path a remote settings catalog entry or
// `--effort` would populate).
let home = tempfile::TempDir::new().expect("create temp home");
let grok_dir = home.path().join(".kigi");
std::fs::create_dir_all(&grok_dir).expect("create .kigi dir");
let kigi_dir = home.path().join(".kigi");
std::fs::create_dir_all(&kigi_dir).expect("create .kigi dir");
std::fs::write(
grok_dir.join("config.toml"),
kigi_dir.join("config.toml"),
r#"
[model.test-model]
supports_reasoning_effort = true
@@ -72,7 +72,7 @@ reasoning_effort = "high"
)
.expect("write config.toml");
let client = GrokStdioClient::spawn_with_home(&server, workdir.path(), home).await;
let client = KigiStdioClient::spawn_with_home(&server, workdir.path(), home).await;
client.initialize_with_timeout().await;
let session_id = client.create_session_with_timeout(workdir.path()).await;
let result = client.prompt_with_timeout(&session_id, "say hello").await;
@@ -98,7 +98,7 @@ async fn test_fresh_session_without_effort_omits_field() {
.await
.expect("start mock server");
let workdir = git_workdir();
let client = GrokStdioClient::spawn(&server, workdir.path()).await;
let client = KigiStdioClient::spawn(&server, workdir.path()).await;
client.initialize_with_timeout().await;
let session_id = client.create_session_with_timeout(workdir.path()).await;
@@ -6,7 +6,7 @@
//! 3. Start a headless session — startup must re-copy trusted/user-home locals.
//! 4. Smoke-validate session JSON under `$KIGI_SHARE_DIR/sessions/` after exit.
//!
//! Requires a built `grok` binary (`KIGI_BINARY` or cargo-built pager) for the
//! Requires a built `kigi` binary (`KIGI_BINARY` or cargo-built pager) for the
//! ignored headless test.
//!
//! ```bash
@@ -114,7 +114,7 @@ fn trusted_local_refresh_surfaces_new_agent_via_discovery() {
let home = dunce::canonicalize(home_tmp.path()).unwrap();
let kigi_home = home.join(".kigi");
let _home_guard = EnvVarGuard::set("HOME", &home);
let _grok_guard = EnvVarGuard::set("KIGI_SHARE_DIR", &kigi_home);
let _kigi_guard = EnvVarGuard::set("KIGI_SHARE_DIR", &kigi_home);
// Live source: a user-home local plugin (mirrors a `~/.claude` local tree).
let source = home
@@ -198,7 +198,7 @@ fn trusted_local_refresh_surfaces_new_agent_via_discovery() {
/// Full binary smoke: session start runs refresh then writes session JSON.
#[tokio::test]
#[ignore = "requires pre-built grok binary; run with --ignored"]
#[ignore = "requires pre-built kigi binary; run with --ignored"]
#[serial]
async fn headless_session_refreshes_trusted_local_plugin_and_writes_session_json() {
let server = MockInferenceServer::start()
@@ -223,7 +223,7 @@ async fn headless_session_refreshes_trusted_local_plugin_and_writes_session_json
// is only for the in-process post-run discovery assertion (which resolves the
// registry via kigi_home()). `#[serial]` keeps it from racing other tests.
let _home_guard = EnvVarGuard::set("HOME", &home);
let _grok_guard = EnvVarGuard::set("KIGI_SHARE_DIR", &kigi_home);
let _kigi_guard = EnvVarGuard::set("KIGI_SHARE_DIR", &kigi_home);
let mut registry = InstallRegistry::empty(kigi_home.join("installed-plugins"));
let installed = register_local_install(&mut registry, &source);
@@ -233,7 +233,7 @@ async fn headless_session_refreshes_trusted_local_plugin_and_writes_session_json
assert!(!installed.path.join("agents/new.md").exists());
let workdir = git_workdir();
let mut cmd = tokio::process::Command::new(grok_binary());
let mut cmd = tokio::process::Command::new(kigi_binary());
cmd.args([
"-p",
"say hello",
@@ -1,12 +1,12 @@
//! Vendor-compatibility end-to-end tests.
//!
//! Each test builds a fake `$HOME` containing skills/rules/AGENTS.md under the
//! `.kigi`, `.cursor`, and `.claude` vendor dirs, spawns a real `grok agent
//! `.kigi`, `.cursor`, and `.claude` vendor dirs, spawns a real `kigi agent
//! stdio` process against the mock inference server (toggling the
//! `GROK_<VENDOR>_<SURFACE>_ENABLED` env vars via `cmd.env`), sends one prompt,
//! `KIGI_<VENDOR>_<SURFACE>_ENABLED` env vars via `cmd.env`), sends one prompt,
//! and asserts on the full inference request bodies:
//!
//! - the Grok-native skill is always present regardless of toggles
//! - the Kigi-native skill is always present regardless of toggles
//! - each of the 6 (vendor x surface) cells toggles independently
//! - a vendor-shipped default skill (`shell`) under `~/.cursor` is always
//! dropped by the denylist
@@ -33,7 +33,7 @@ where
/// Unique markers placed in skill descriptions / file contents so assertions
/// can't be fooled by incidental occurrences of a bare word like "shell".
const MARKER_GROK_SKILL: &str = "ZZ_GROK_SKILL_MARKER";
const MARKER_KIGI_SKILL: &str = "ZZ_KIGI_SKILL_MARKER";
const MARKER_CURSOR_SKILL: &str = "ZZ_CURSOR_SKILL_MARKER";
const MARKER_CURSOR_SHELL: &str = "ZZ_CURSOR_SHELL_DENYLISTED_MARKER";
const MARKER_CLAUDE_SKILL: &str = "ZZ_CLAUDE_SKILL_MARKER";
@@ -63,7 +63,7 @@ fn write_skill(home: &Path, vendor_dir: &str, name: &str, marker: &str) {
/// Populate a fake `$HOME` + repo cwd with the full vendor-compat fixture set.
fn seed_fixtures(home: &Path, cwd: &Path) {
// Skills (User scope, home-based).
write_skill(home, ".kigi", "grok-skill", MARKER_GROK_SKILL);
write_skill(home, ".kigi", "kigi-skill", MARKER_KIGI_SKILL);
write_skill(home, ".cursor", "my-cursor-skill", MARKER_CURSOR_SKILL);
// `shell` is a Cursor vendor-default → must be denylisted under ~/.cursor.
write_skill(home, ".cursor", "shell", MARKER_CURSOR_SHELL);
@@ -102,7 +102,7 @@ async fn run_scenario(env: &[(&str, &str)]) -> String {
let home = tempfile::TempDir::new().expect("create temp home");
seed_fixtures(home.path(), workdir.path());
let client = GrokStdioClient::spawn_with_home_and_env(&server, workdir.path(), home, env).await;
let client = KigiStdioClient::spawn_with_home_and_env(&server, workdir.path(), home, env).await;
client.initialize_with_timeout().await;
let session_id = client.create_session_with_timeout(workdir.path()).await;
let _ = client.prompt_with_timeout(&session_id, "hello").await;
@@ -122,7 +122,7 @@ async fn run_scenario(env: &[(&str, &str)]) -> String {
// ── Skills ──────────────────────────────────────────────────────────────────
/// Defaults (all vendors on): grok + cursor-vendor + claude-vendor skills present; the
/// Defaults (all vendors on): kigi + cursor-vendor + claude-vendor skills present; the
/// denylisted vendor builtin `shell` is dropped.
#[tokio::test]
#[ignore] // requires pre-built binary
@@ -130,8 +130,8 @@ async fn vendor_compat_defaults_include_vendor_skills_but_drop_denylisted() {
with_local_set(|| async {
let body = run_scenario(&[]).await;
assert!(
body.contains(MARKER_GROK_SKILL),
"grok-skill must always be present"
body.contains(MARKER_KIGI_SKILL),
"kigi-skill must always be present"
);
assert!(
body.contains(MARKER_CURSOR_SKILL),
@@ -149,15 +149,15 @@ async fn vendor_compat_defaults_include_vendor_skills_but_drop_denylisted() {
.await;
}
/// `KIGI_CURSOR_SKILLS_ENABLED=false` drops the cursor-vendor skill; grok stays.
/// `KIGI_CURSOR_SKILLS_ENABLED=false` drops the cursor-vendor skill; kigi stays.
#[tokio::test]
#[ignore] // requires pre-built binary
async fn vendor_compat_cursor_skills_disabled() {
with_local_set(|| async {
let body = run_scenario(&[("KIGI_CURSOR_SKILLS_ENABLED", "false")]).await;
assert!(
body.contains(MARKER_GROK_SKILL),
"grok-skill always present"
body.contains(MARKER_KIGI_SKILL),
"kigi-skill always present"
);
assert!(
!body.contains(MARKER_CURSOR_SKILL),
@@ -169,15 +169,15 @@ async fn vendor_compat_cursor_skills_disabled() {
.await;
}
/// `KIGI_CLAUDE_SKILLS_ENABLED=false` drops the claude-vendor skill; grok stays.
/// `KIGI_CLAUDE_SKILLS_ENABLED=false` drops the claude-vendor skill; kigi stays.
#[tokio::test]
#[ignore] // requires pre-built binary
async fn vendor_compat_claude_skills_disabled() {
with_local_set(|| async {
let body = run_scenario(&[("KIGI_CLAUDE_SKILLS_ENABLED", "false")]).await;
assert!(
body.contains(MARKER_GROK_SKILL),
"grok-skill always present"
body.contains(MARKER_KIGI_SKILL),
"kigi-skill always present"
);
assert!(
!body.contains(MARKER_CLAUDE_SKILL),
@@ -307,7 +307,7 @@ async fn vendor_compat_all_cursor_disabled() {
assert!(!body.contains(MARKER_CURSOR_SHELL));
assert!(!body.contains(MARKER_CURSOR_RULE));
assert!(!body.contains(MARKER_CURSOR_AGENTS));
assert!(body.contains(MARKER_GROK_SKILL), "grok always present");
assert!(body.contains(MARKER_KIGI_SKILL), "kigi always present");
assert!(body.contains(MARKER_CLAUDE_SKILL), "claude unaffected");
assert!(body.contains(MARKER_CLAUDE_RULE), "claude unaffected");
assert!(body.contains(MARKER_CLAUDE_AGENTS), "claude unaffected");
@@ -330,7 +330,7 @@ async fn vendor_compat_all_claude_disabled() {
assert!(!body.contains(MARKER_CLAUDE_SKILL));
assert!(!body.contains(MARKER_CLAUDE_RULE));
assert!(!body.contains(MARKER_CLAUDE_AGENTS));
assert!(body.contains(MARKER_GROK_SKILL), "grok always present");
assert!(body.contains(MARKER_KIGI_SKILL), "kigi always present");
assert!(body.contains(MARKER_CURSOR_SKILL), "cursor unaffected");
assert!(body.contains(MARKER_CURSOR_RULE), "cursor unaffected");
assert!(body.contains(MARKER_CURSOR_AGENTS), "cursor unaffected");
@@ -339,7 +339,7 @@ async fn vendor_compat_all_claude_disabled() {
.await;
}
/// All vendor compat OFF: only grok-native skill survives.
/// All vendor compat OFF: only kigi-native skill survives.
#[tokio::test]
#[ignore] // requires pre-built binary
async fn vendor_compat_all_vendors_disabled() {
@@ -353,7 +353,7 @@ async fn vendor_compat_all_vendors_disabled() {
("KIGI_CLAUDE_AGENTS_ENABLED", "false"),
])
.await;
assert!(body.contains(MARKER_GROK_SKILL), "grok always present");
assert!(body.contains(MARKER_KIGI_SKILL), "kigi always present");
assert!(!body.contains(MARKER_CURSOR_SKILL));
assert!(!body.contains(MARKER_CURSOR_SHELL));
assert!(!body.contains(MARKER_CURSOR_RULE));
@@ -1,4 +1,4 @@
//! Integration test for `_x.ai/session/update` notifications.
//! Integration test for `_kigi/session/update` notifications.
//!
//! This test verifies that:
//! 1. xAI session notifications (e.g., diff_review) can be sent via ext_notification
@@ -123,7 +123,7 @@ async fn test_xai_session_notification_storage_roundtrip() {
/// Test that a `TurnCompleted` terminal round-trips through storage — the
/// persistence half of the "stuck on Waiting…" fix, where the durable terminal
/// must survive `updates.jsonl` and reload as a replayable `_x.ai/session/update`.
/// must survive `updates.jsonl` and reload as a replayable `_kigi/session/update`.
#[tokio::test]
async fn test_turn_completed_round_trips_through_storage() {
let temp_dir = TempDir::new().unwrap();