mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
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.
192 lines
7.5 KiB
Rust
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}"
|
|
);
|
|
}
|