Files
dev/crates/hanzo-dev/tests/cli.rs
T
zeekay ad468d97af upstream: the fork stands alone; Dev fills its two hooks
hanzoai/codex names nothing outside its tree now (d1d710452): Dev's names,
defaults, model catalog, `/model <name>` match, controller interface and
version live in its `hanzo` crate, and Dev's crates depend on that, never
the reverse. hanzo-loop moves there whole; hanzo-tui keeps only the Hanzo
account; hanzo-config reads API_BASE, DEFAULT_MODEL and is_hanzo from it.

hanzo-dev's main registers what Dev adds before anything starts:
`codex_app_server::register(hanzo_kai::install)` puts Kai on every thread,
and `codex_tui::register(Account { login, logout, show })` puts the Hanzo
CLI's sign-in behind /login, /logout, /auth and /status. launch.rs holds
that /login is offered, which it is only when an account is registered.
make test-upstream runs locked against the fork's own lock.
2026-10-15 10:03:29 -07:00

192 lines
7.5 KiB
Rust

//! What the command surface promises, checked against the built binary.
//!
//! These run `dev` rather than calling into it, because the defects worth
//! catching here are the ones that only appear once clap assembles the whole
//! parser — an argument the front door and a subcommand both define, say.
use std::path::Path;
use std::process::Command;
use std::process::Output;
/// Every subcommand `dev` offers. Building each one's parser is the point:
/// clap only rejects a duplicate argument when it assembles that subcommand.
const COMMANDS: &[&str] = &[
"exec", "login", "logout", "serve", "resume", "fork", "review", "agents", "queue", "archive",
"unarchive", "delete", "cloud", "mcp", "plugin", "doctor", "features", "flow", "sandbox",
"completion",
];
fn dev(home: &Path, arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_dev"))
.args(arguments)
.env("DEV_HOME", home)
.output()
.expect("dev is built before its tests run")
}
fn text(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn home() -> tempfile::TempDir {
tempfile::tempdir().expect("a writable temporary directory")
}
#[test]
fn every_subcommand_assembles_a_parser() {
let home = home();
for command in COMMANDS {
let output = dev(home.path(), &[command, "--help"]);
assert!(
output.status.success(),
"dev {command} --help: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn flow_offers_fix_and_fix_needs_a_test_command() {
let home = home();
assert!(text(&dev(home.path(), &["flow", "--help"])).contains("fix"));
let help = text(&dev(home.path(), &["flow", "fix", "--help"]));
assert!(help.contains("--test") && help.contains("--control"), "{help}");
assert!(!dev(home.path(), &["flow", "fix"]).status.success());
}
#[test]
fn completion_scripts_name_the_product() {
let home = home();
assert!(text(&dev(home.path(), &["completion", "bash"])).contains("_dev()"));
assert!(text(&dev(home.path(), &["completion", "zsh"])).contains("compdef dev"));
}
/// Every surface a user reads, not just the front page. The banner `dev exec`
/// prints was upstream's for a while precisely because only `--help` was
/// checked, and `--help` never shows it.
#[test]
fn no_surface_carries_an_upstream_product_name() {
let home = home();
let mut surfaces = vec![("--help".to_string(), text(&dev(home.path(), &["--help"])))];
for command in COMMANDS {
surfaces.push((
format!("{command} --help"),
text(&dev(home.path(), &[command, "--help"])),
));
}
for (surface, body) in surfaces {
for name in ["OpenAI", "Codex", "codex"] {
// Login names the provider whose credential the user is supplying.
if surface == "login --help" && name == "OpenAI" {
continue;
}
// Two things legitimately carry the word and must not be renamed:
// `CODEX_HOME`, an environment variable upstream owns, and
// `codex/…`, which names a method on the app-server wire protocol.
// Renaming either would break something real; branding is about the
// product name a reader sees, not identifiers on a wire.
let leaked: Vec<_> = body
.lines()
.filter(|line| {
line.contains(name)
&& !line.contains("CODEX_HOME")
&& !line.contains("`codex/")
})
.collect();
assert!(leaked.is_empty(), "`dev {surface}` says {name}: {leaked:?}");
}
}
}
/// The product answers for one version, everywhere it states one.
#[test]
fn the_product_states_one_version() {
let home = home();
let version = text(&dev(home.path(), &["--version"]));
let number = version.split_whitespace().nth(1).expect("`dev <version>`").to_string();
assert!(version.starts_with("dev "), "{version}");
assert_eq!(number, env!("CARGO_PKG_VERSION"), "{version}");
let doctor = text(&dev(home.path(), &["doctor", "--summary", "--no-color"]));
assert!(doctor.contains(&number), "doctor reports a different version: {doctor:.120}");
}
/// Upstream code asks the hanzo crate for the version this binary set. A merge
/// from upstream that brings its own compile-time read back would state the
/// fork's 0.0.0 wherever that read lands; a build script keeps its own.
#[test]
fn upstream_reads_the_version_the_binary_sets() {
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../upstream/codex/codex-rs");
let mut pending = vec![source.clone()];
let mut readers = Vec::new();
while let Some(directory) = pending.pop() {
for entry in std::fs::read_dir(&directory).expect("the upstream checkout is readable") {
let path = entry.expect("a directory entry").path();
let name = path.file_name().and_then(|name| name.to_str()).unwrap_or_default();
if path.is_dir() {
if name != "target" && path != source.join("hanzo") {
pending.push(path);
}
} else if name.ends_with(".rs")
&& name != "build.rs"
&& std::fs::read_to_string(&path)
.is_ok_and(|text| text.contains("env!(\"CARGO_PKG_VERSION\")"))
{
readers.push(path.strip_prefix(&source).unwrap_or(&path).display().to_string());
}
}
}
readers.sort();
assert!(readers.is_empty(), "these read the fork's own version, not the binary's: {readers:?}");
}
/// The first feature upstream still offers as a switch. Names come and go with
/// each upstream release, so the round trip asks for one instead of naming it.
fn experimental_feature(listed: &str) -> String {
listed
.lines()
.find_map(|line| {
let columns: Vec<_> = line.split(" ").map(str::trim).filter(|c| !c.is_empty()).collect();
(columns.get(1) == Some(&"experimental")).then(|| columns[0].to_string())
})
.expect("an experimental feature is listed")
}
#[test]
fn features_round_trip_through_the_product_config() {
let home = home();
let feature = experimental_feature(&text(&dev(home.path(), &["features", "list"])));
assert!(dev(home.path(), &["features", "enable", &feature]).status.success());
let listed = text(&dev(home.path(), &["features", "list"]));
let row = listed
.lines()
.find(|line| line.split_whitespace().next() == Some(feature.as_str()))
.expect("the feature is listed");
assert!(row.ends_with("on"), "{row}");
assert!(dev(home.path(), &["features", "disable", &feature]).status.success());
let listed = text(&dev(home.path(), &["features", "list"]));
let row = listed
.lines()
.find(|line| line.split_whitespace().next() == Some(feature.as_str()))
.unwrap();
assert!(row.ends_with("off"), "{row}");
}
#[test]
fn an_unknown_feature_is_refused() {
let home = home();
let output = dev(home.path(), &["features", "enable", "not_a_feature"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("no such feature"));
}
#[test]
fn doctor_reports_as_json() {
let home = home();
let report = text(&dev(home.path(), &["doctor", "--json"]));
assert!(
serde_json::from_str::<serde_json::Value>(&report).is_ok(),
"doctor did not emit JSON: {report:.200}"
);
}