Files
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

111 lines
4.8 KiB
Rust

//! A bare `dev` starts, and it reads only Dev's own project configuration.
//!
//! Upstream's default is a shared background server installed from a package directory
//! that Dev does not ship, so a launch has to run its server inside the process or the
//! first command ends in an error. And a project keeps Dev's settings in `.hanzo/dev`, the
//! layout of `~/.hanzo/dev`: another product's `.codex` beside it is that product's, and
//! its model must not become ours. And the binary registers the Hanzo account with the
//! TUI, which offers /login only then.
#![cfg(unix)]
use portable_pty::CommandBuilder;
use portable_pty::PtySize;
use portable_pty::native_pty_system;
use std::io::Read;
use std::io::Write;
use std::sync::mpsc;
use std::time::Duration;
use std::time::Instant;
const COMPOSER: &str = "Ask Hanzo Dev to do anything";
const PACKAGE_ERROR: &str = "no complete local package";
#[test]
fn a_bare_launch_starts_in_process_and_reads_only_the_projects_hanzo_config() {
let home = tempfile::tempdir().expect("a scratch home");
let work = tempfile::tempdir().expect("a scratch directory");
let work_path = work.path().canonicalize().expect("a real path");
std::fs::write(
home.path().join("config.toml"),
format!(
"model_provider = \"local\"\nmodel = \"local-model\"\n\n\
[model_providers.local]\nname = \"Local\"\nbase_url = \"http://127.0.0.1:9/v1\"\nwire_api = \"responses\"\n\n\
[projects.\"{}\"]\ntrust_level = \"trusted\"\n",
work_path.display()
),
)
.expect("a config");
std::fs::create_dir_all(work_path.join(".hanzo/dev")).expect("the project's directory");
std::fs::write(work_path.join(".hanzo/dev/config.toml"), "model = \"project-model\"\n")
.expect("the project's config");
std::fs::create_dir(work_path.join(".codex")).expect("another product's directory");
std::fs::write(work_path.join(".codex/config.toml"), "model = \"gpt-6-astra\"\n")
.expect("another product's config");
let pair = native_pty_system()
.openpty(PtySize { rows: 36, cols: 110, pixel_width: 0, pixel_height: 0 })
.expect("a terminal");
let mut command = CommandBuilder::new(env!("CARGO_BIN_EXE_dev"));
command.cwd(&work_path);
command.env("TERM", "xterm-256color");
command.env("DEV_HOME", home.path());
command.env("HOME", home.path());
command.env_remove("CODEX_HOME");
let mut child = pair.slave.spawn_command(command).expect("dev starts");
drop(pair.slave);
let mut reader = pair.master.try_clone_reader().expect("a reader");
let (sender, received) = mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
let mut buffer = [0u8; 8192];
while let Ok(count) = reader.read(&mut buffer) {
if count == 0 || sender.send(buffer[..count].to_vec()).is_err() {
break;
}
}
});
let mut screen = vt100::Parser::new(36, 110, 0);
let deadline = Instant::now() + Duration::from_secs(60);
let mut shown = String::new();
while Instant::now() < deadline {
if let Ok(bytes) = received.recv_timeout(Duration::from_millis(200)) {
screen.process(&bytes);
shown = screen.screen().contents();
if shown.contains(PACKAGE_ERROR) || (shown.contains(COMPOSER) && shown.contains("project-model")) {
break;
}
}
}
let mut offered = String::new();
if shown.contains(COMPOSER) {
let mut writer = pair.master.take_writer().expect("a writer");
writer.write_all(b"/log").expect("typed");
let deadline = Instant::now() + Duration::from_secs(20);
while Instant::now() < deadline && !offered.contains("sign in to Hanzo") {
if let Ok(bytes) = received.recv_timeout(Duration::from_millis(200)) {
screen.process(&bytes);
offered = screen.screen().contents();
}
}
}
let _ = child.kill();
assert!(!shown.contains(PACKAGE_ERROR), "a bare launch asked for a package:\n{shown}");
assert!(shown.contains(COMPOSER), "a bare launch never reached the composer:\n{shown}");
assert!(shown.contains("project-model"), "the project's own config was not read:\n{shown}");
// The header names the version this binary was built as, which upstream's
// TUI reads from the hanzo crate at run time.
let built = env!("CARGO_PKG_VERSION");
assert!(shown.contains(&format!("Hanzo Dev (v{built})")), "the header does not say v{built}:\n{shown}");
assert!(
!shown.to_ascii_lowercase().contains("gpt-6"),
"another product's project config set the model:\n{shown}"
);
assert!(
offered.contains("/login") && offered.contains("sign in to Hanzo"),
"no account was registered with the TUI, so /login is not offered:\n{offered}"
);
}