M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.
Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
ptyctl, ptyctl-cli, third_party/ unchanged; proto package
xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
(templates re-encrypted)
Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
module & dc_log, heap-profile uploader, auth-diagnostics uploader,
session-analytics halves of feedback; local zero-egress observability
preserved in new kigi-log crate (unified log, --debug firehose,
subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
shell util
Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted
Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean
Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
(new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
fast-worktree); RSS measurement tests serialized via serial_test
Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
notices sustained; kigi-tools ported-code notices extended; README,
CONTRIBUTING, SECURITY, AGENTS.md rewritten
Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
//! Unit tests for the `grok wrap` spawn planning in [`super`] (`wrap_cmd`),
|
||||
//! split out via `#[path]` to keep the module itself small.
|
||||
//!
|
||||
//! Everything is pure — `derive_spawn` takes the PATH lookup result and the
|
||||
//! shell as inputs — except the final test, which round-trips the rejoined
|
||||
//! command line through a real `/bin/sh -c` to prove the quoting contract end
|
||||
//! to end.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn cmd(words: &[&str]) -> Vec<String> {
|
||||
words.iter().map(|w| w.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_arg_with_whitespace_always_routes_via_shell() {
|
||||
// Wins over PATH resolution: the arg is a command line, not a program.
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["mycli ssh host-01"]),
|
||||
"/bin/zsh",
|
||||
true,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "/bin/zsh");
|
||||
assert_eq!(plan.args, cmd(&["-i", "-c", "mycli ssh host-01"]));
|
||||
|
||||
// Wins over the explicit-path rule too: a single program path with
|
||||
// spaces word-splits in the shell (quote it for the shell instead).
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["/path/with space/prog"]),
|
||||
"/bin/zsh",
|
||||
false,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.args, cmd(&["-i", "-c", "/path/with space/prog"]));
|
||||
|
||||
// Any whitespace counts, not just spaces: tabs/newlines also mark the
|
||||
// arg as a command line to hand to the shell verbatim.
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["x\tssh\nhost"]),
|
||||
"/bin/sh",
|
||||
false,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "/bin/sh");
|
||||
assert_eq!(plan.args, cmd(&["-i", "-c", "x\tssh\nhost"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolvable_program_spawns_directly() {
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["explorer", "ssh", "host"]),
|
||||
"/bin/zsh",
|
||||
true,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "explorer");
|
||||
assert_eq!(plan.args, cmd(&["ssh", "host"]));
|
||||
|
||||
// A single resolvable word is a plain program run, not an alias.
|
||||
let plan = derive_spawn(&cmd(&["ls"]), "/bin/zsh", true, ShellMode::Interactive);
|
||||
assert_eq!(plan.program, "ls");
|
||||
assert_eq!(plan.args, Vec::<String>::new());
|
||||
|
||||
// Explicit paths never route through the shell, resolvable or not —
|
||||
// relative or absolute.
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["./run.sh", "arg"]),
|
||||
"/bin/zsh",
|
||||
false,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "./run.sh");
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["/abs/prog", "arg"]),
|
||||
"/bin/zsh",
|
||||
false,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "/abs/prog");
|
||||
|
||||
// A multi-arg first word with whitespace is never an alias: keep the
|
||||
// direct spawn and its precise not-found error.
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["my prog", "arg"]),
|
||||
"/bin/zsh",
|
||||
false,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "my prog");
|
||||
assert_eq!(plan.args, cmd(&["arg"]));
|
||||
|
||||
// Same for an empty first word (unset `$PROG`): fail fast on the
|
||||
// direct spawn rather than shell-executing the tail.
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["", "rm", "-rf", "x"]),
|
||||
"/bin/zsh",
|
||||
false,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_route_passes_args_verbatim_without_quoting() {
|
||||
// Quoting exists only on the shell route's rejoined line; a resolvable
|
||||
// program must receive its argv elements byte-for-byte.
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["rsync", "a b", "don't"]),
|
||||
"/bin/zsh",
|
||||
true,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "rsync");
|
||||
assert_eq!(plan.args, cmd(&["a b", "don't"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_route_keeps_first_word_bare_and_quotes_the_tail() {
|
||||
// First word stays unquoted (aliases only expand on bare words); every
|
||||
// tail word is quoted — zsh expands a bare `=word`, so no safe set.
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["x", "ssh", "=host", "don't"]),
|
||||
"/bin/zsh",
|
||||
false,
|
||||
ShellMode::Interactive,
|
||||
);
|
||||
assert_eq!(plan.program, "/bin/zsh");
|
||||
assert_eq!(plan.args, cmd(&["-i", "-c", "x 'ssh' '=host' 'don'\\''t'"]));
|
||||
|
||||
// Bare alias with no args: the most common invocation.
|
||||
let plan = derive_spawn(&cmd(&["x"]), "/bin/zsh", false, ShellMode::Interactive);
|
||||
assert_eq!(plan.args, cmd(&["-i", "-c", "x"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_shell_mode_drops_dash_i() {
|
||||
let plan = derive_spawn(
|
||||
&cmd(&["mycli ssh host"]),
|
||||
"/bin/sh",
|
||||
false,
|
||||
ShellMode::Plain,
|
||||
);
|
||||
assert_eq!(plan.program, "/bin/sh");
|
||||
assert_eq!(plan.args, cmd(&["-c", "mycli ssh host"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_word_edge_cases() {
|
||||
assert_eq!(quote_word("don't"), "'don'\\''t'");
|
||||
assert_eq!(quote_word(""), "''");
|
||||
// A lone quote is the worst case for the close-escape-reopen idiom.
|
||||
assert_eq!(quote_word("'"), "''\\'''");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_word_neutralizes_shell_metacharacters() {
|
||||
// Expansion/control characters must come out single-quoted inert.
|
||||
for meta in ["$HOME", "`id`", "*", "!!", ";", "&&", "|", "a\nb"] {
|
||||
assert_eq!(quote_word(meta), format!("'{meta}'"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_command_line_shapes() {
|
||||
// Zero tail words: just the bare first word, no trailing space.
|
||||
assert_eq!(join_command_line(&cmd(&["x"])), "x");
|
||||
// An empty tail word survives as an explicit empty argument.
|
||||
assert_eq!(join_command_line(&cmd(&["x", ""])), "x ''");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_shell_falls_back_to_bin_sh() {
|
||||
assert_eq!(resolve_shell(None), "/bin/sh");
|
||||
assert_eq!(resolve_shell(Some("")), "/bin/sh");
|
||||
assert_eq!(resolve_shell(Some("/nonexistent/shell-xyz")), "/bin/sh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_shell_uses_existing_file() {
|
||||
let file = tempfile::NamedTempFile::new().expect("tempfile");
|
||||
let path = file.path().to_str().expect("utf8 path").to_string();
|
||||
assert_eq!(resolve_shell(Some(&path)), path);
|
||||
}
|
||||
|
||||
/// Feed the rejoined command line through a real `/bin/sh -c` and assert the
|
||||
/// child receives exactly the original words — the quoting contract proven
|
||||
/// against an actual shell, not just against expected strings.
|
||||
#[test]
|
||||
fn joined_line_roundtrips_words_through_real_sh() {
|
||||
let words = [
|
||||
"a b",
|
||||
"don't",
|
||||
"=x",
|
||||
"new\nline",
|
||||
"$HOME",
|
||||
"`id`",
|
||||
"*",
|
||||
";",
|
||||
"&&",
|
||||
"|",
|
||||
"",
|
||||
];
|
||||
// `printf` repeats the format per argument, so each word renders as `[w]`.
|
||||
let mut command = vec!["printf".to_string(), "[%s]".to_string()];
|
||||
command.extend(words.iter().map(|w| w.to_string()));
|
||||
|
||||
let out = std::process::Command::new("/bin/sh")
|
||||
.arg("-c")
|
||||
.arg(join_command_line(&command))
|
||||
.output()
|
||||
.expect("run /bin/sh");
|
||||
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"sh rejected the joined line: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let expected: String = words.iter().map(|w| format!("[{w}]")).collect();
|
||||
assert_eq!(String::from_utf8_lossy(&out.stdout), expected);
|
||||
}
|
||||
Reference in New Issue
Block a user