Files
Kigi-CLI/crates/codegen/kigi-shell/tests/test_debug_logging.rs
T
ZacharyZhang-NY 6f31415ed6 §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.
2026-07-18 02:48:46 -04:00

339 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! End-to-end tests for the `--debug` firehose file logging.
//!
//! 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
//! `~/.kigi/debug/<sessionId>.txt` with first-party content, and does NOT enable
//! sampling/instrumentation. Regression for the master switch having bundled
//! `KIGI_LOG_SAMPLING`/`KIGI_INSTRUMENTATION`, whose global `TargetFilterLayer`
//! suppressed every other target and starved the firehose.
//! - `--debug` (headless) runs cleanly without crashing arg-parsing (smoke).
//! - no `--debug` writes no firehose files.
//! - a live `agent` session (explicit `KIGI_DEBUG_LOG=1`) writes a per-session
//! `~/.kigi/debug/<sessionId>.txt` with real first-party content + `latest.txt`.
//! - `--debug-file <path>` writes one explicit file and bypasses per-session
//! routing entirely (no `~/.kigi/debug/` files).
//! - `KIGI_LOG_FILE=<path>` writes that explicit file (back-compat single file).
//!
//! Per-session content is asserted via the live `agent`, not the headless run:
//! the agent's `run_session` future runs under the `session` span (carrying
//! `session_id`), so its first-party debug events route to `<sessionId>.txt`.
//! This is the same `init_tracing_simple("agent")` path the spawned leader uses,
//! so it covers leader capture deterministically without a flaky detached
//! process. Buffered logs from runs that DO log are not lost: the firehose
//! worker guards are flushed at process exit via `debug_log::flush()` (normal +
//! signal exit paths).
//!
//! `#[ignore]` (they need a built binary). Run locally (auto-builds the pager):
//! ```bash
//! cargo test -p kigi-shell --test test_debug_logging -- --ignored
//! ```
use std::future::Future;
use std::path::{Path, PathBuf};
use std::time::Duration;
use kigi_test_support::*;
use tempfile::TempDir;
/// Run an async body inside a `LocalSet` (required by ACP's `!Send` futures).
async fn with_local_set<F, Fut>(f: F)
where
F: FnOnce() -> Fut,
Fut: Future<Output = ()>,
{
tokio::task::LocalSet::new().run_until(f()).await;
}
/// The per-session firehose directory under a pinned `$KIGI_SHARE_DIR`.
fn debug_dir(home: &Path) -> PathBuf {
home.join(".kigi").join("debug")
}
/// List firehose `*.txt` files under `~/.kigi/debug` (excluding the `latest.txt`
/// symlink). Empty if the dir is missing.
fn firehose_txt_files(home: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(debug_dir(home)) else {
return Vec::new();
};
entries
.flatten()
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.ends_with(".txt") && n != "latest.txt")
})
.collect()
}
/// 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(
server: &MockInferenceServer,
home: &Path,
workdir: &Path,
extra: &[&str],
) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new(kigi_binary());
cmd.args(["-p", "say hi", "--yolo", "--output-format", "json"])
.args(extra)
.arg("--cwd")
.arg(workdir)
.current_dir(workdir)
.stdin(std::process::Stdio::null())
.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);
// Pin the home location and drop inherited firehose toggles for determinism.
cmd.env("KIGI_SHARE_DIR", home.join(".kigi"));
cmd.env_remove("KIGI_DEBUG_LOG");
cmd.env_remove("KIGI_LOG_FILE");
cmd.env_remove("KIGI_LOG_SAMPLING");
cmd.env_remove("KIGI_HOOKS_LOG");
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_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: &KigiStdioClient) -> String {
let mut content = None;
for _ in 0..50 {
if let Ok(text) = std::fs::read_to_string(path)
&& !text.is_empty()
{
content = Some(text);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let content = content.unwrap_or_else(|| {
panic!(
"no non-empty per-session firehose {path:?}\nstderr:\n{}",
stderr_tail(&client.stderr(), 800)
)
});
// The firehose filter routes first-party crate logs here; assert that rather
// than a bare non-empty check.
assert!(
content.contains("xai_kigi"),
"session firehose {path:?} should contain first-party logs, got {} bytes",
content.len()
);
content
}
/// `--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 `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
async fn debug_flag_enables_firehose_without_crashing() {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let workdir = git_workdir();
let home = TempDir::new().expect("create temp home");
let cmd = debug_cmd(&server, home.path(), workdir.path(), &["--debug"]);
let result = run_headless_with_cmd(cmd).await;
assert_headless_success(&result, "kigi --debug headless", Some(&server));
assert_no_crashes(&result.stderr);
}
/// Without `--debug` (and no firehose env), no firehose files are written.
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn no_debug_flag_writes_no_debug_dir() {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let workdir = git_workdir();
let home = TempDir::new().expect("create temp home");
let cmd = debug_cmd(&server, home.path(), workdir.path(), &[]);
let result = run_headless_with_cmd(cmd).await;
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: {:?}",
firehose_txt_files(home.path())
);
}
/// A live `agent` session writes `~/.kigi/debug/<sessionId>.txt` with real
/// first-party content, and points `latest.txt` at it. This is the same
/// `init_tracing_simple("agent")` path the spawned leader uses, so it covers
/// leader capture deterministically without a flaky detached process.
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn agent_session_writes_named_session_file() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let workdir = git_workdir();
let home = TempDir::new().expect("create temp home");
let kigi_home = home.path().join(".kigi");
let kigi_home_str = kigi_home.to_string_lossy().into_owned();
let client = KigiStdioClient::spawn_with_home_and_env(
&server,
workdir.path(),
home,
&[("KIGI_DEBUG_LOG", "1"), ("KIGI_SHARE_DIR", &kigi_home_str)],
)
.await;
client.initialize_with_timeout().await;
let session_id = client.create_session_with_timeout(workdir.path()).await;
// New session ids are UUID v7 (filesystem-safe), so the firehose file is
// named verbatim `<sessionId>.txt`.
let sid = session_id.0.to_string();
let _ = client.prompt_with_timeout(&session_id, "say hi").await;
let session_file = kigi_home.join("debug").join(format!("{sid}.txt"));
read_session_firehose_when_ready(&session_file, &client).await;
// `latest.txt` is a sibling symlink pointing at the just-opened session
// file, so `tail -f ~/.kigi/debug/latest.txt` follows the live session.
#[cfg(unix)]
{
let link = kigi_home.join("debug").join("latest.txt");
let target = std::fs::read_link(&link)
.unwrap_or_else(|e| panic!("latest.txt should be a symlink ({link:?}): {e}"));
assert_eq!(target, Path::new(&format!("{sid}.txt")));
}
})
.await;
}
/// The `--debug` FLAG (not `KIGI_DEBUG_LOG` directly) drives the firehose end to
/// end through the master switch. Regression: the master switch used to also set
/// `KIGI_LOG_SAMPLING`/`KIGI_INSTRUMENTATION`, whose `TargetFilterLayer` globally
/// suppresses every non-matching target — starving the firehose so `--debug`
/// produced no logs. Drives a real agent session with `--debug` and asserts the
/// per-session file has first-party content (would FAIL pre-fix), and that
/// sampling/instrumentation are NOT enabled by `--debug`.
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn debug_flag_master_switch_enables_firehose() {
with_local_set(|| async {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let workdir = git_workdir();
let home = TempDir::new().expect("create temp home");
let kigi_home = home.path().join(".kigi");
let kigi_home_str = kigi_home.to_string_lossy().into_owned();
// 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 = KigiStdioClient::spawn_with_home_env_and_args(
&server,
workdir.path(),
home,
&[("KIGI_SHARE_DIR", &kigi_home_str)],
&["--debug"],
)
.await;
client.initialize_with_timeout().await;
let session_id = client.create_session_with_timeout(workdir.path()).await;
let sid = session_id.0.to_string();
let _ = client.prompt_with_timeout(&session_id, "say hi").await;
let session_file = kigi_home.join("debug").join(format!("{sid}.txt"));
read_session_firehose_when_ready(&session_file, &client).await;
// Slimming guard: `--debug` must NOT enable sampling. The agent spawn
// clears KIGI_LOG_SAMPLING (hermetic), so the sampling layer stays off and
// `~/.kigi/logs/sampling.jsonl` is never written — the `--debug`
// set-if-unset must not flip it on (the pre-fix code did, starving the
// firehose). Instrumentation isn't checked: the harness pins
// KIGI_INSTRUMENTATION=disabled, so that assertion would be vacuous.
let sampling = kigi_home.join("logs").join("sampling.jsonl");
let len = std::fs::metadata(&sampling).map(|m| m.len()).unwrap_or(0);
assert_eq!(
len, 0,
"--debug must not enable sampling, found {len} bytes at {sampling:?}"
);
})
.await;
}
/// `--debug-file <path>` writes one explicit file and bypasses per-session
/// routing entirely (no `~/.kigi/debug/` files created).
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn debug_file_flag_writes_single_file_and_bypasses_routing() {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let workdir = git_workdir();
let home = TempDir::new().expect("create temp home");
let explicit = home.path().join("explicit-firehose.txt");
let explicit_str = explicit.to_string_lossy().into_owned();
let cmd = debug_cmd(
&server,
home.path(),
workdir.path(),
&["--debug-file", &explicit_str],
);
let result = run_headless_with_cmd(cmd).await;
assert_headless_success(&result, "kigi --debug-file", Some(&server));
assert_no_crashes(&result.stderr);
assert!(
explicit.exists(),
"explicit --debug-file path not written: {explicit:?}\nstderr tail:\n{}",
stderr_tail(&result.stderr, 800)
);
// Routing bypassed: nothing should land in the per-session debug dir.
assert!(
firehose_txt_files(home.path()).is_empty(),
"--debug-file must bypass per-session routing, found: {:?}",
firehose_txt_files(home.path())
);
}
/// `KIGI_LOG_FILE=<path>` (no `--debug`) writes that exact file (back-compat).
#[tokio::test]
#[ignore] // requires pre-built binary; run with --ignored
async fn kigi_log_file_explicit_path_is_written() {
let server = MockInferenceServer::start()
.await
.expect("start mock server");
let workdir = git_workdir();
let home = TempDir::new().expect("create temp home");
let custom = home.path().join("custom-log-file.log");
let mut cmd = debug_cmd(&server, home.path(), workdir.path(), &[]);
cmd.env("KIGI_LOG_FILE", &custom);
let result = run_headless_with_cmd(cmd).await;
assert_headless_success(&result, "kigi KIGI_LOG_FILE=path", Some(&server));
assert_no_crashes(&result.stderr);
assert!(
custom.exists(),
"explicit KIGI_LOG_FILE path not written: {custom:?}\nstderr tail:\n{}",
stderr_tail(&result.stderr, 800)
);
// Single-file mode bypasses per-session routing.
assert!(
firehose_txt_files(home.path()).is_empty(),
"KIGI_LOG_FILE must bypass per-session routing, found: {:?}",
firehose_txt_files(home.path())
);
}