feat(dev): Enso by default, /login /auth /model <name>, Dev in every string, codex 11da6b9

This commit is contained in:
2026-10-07 16:04:54 -07:00
parent 2cdddb0b00
commit 63a0bb114d
13 changed files with 1651 additions and 111 deletions
Generated
+47 -102
View File
@@ -252,27 +252,6 @@ version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "appcontainer_common"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
dependencies = [
"flatbuffers",
"getrandom 0.2.17",
"learning_mode_core",
"learning_mode_windows",
"process_security_environment_spec",
"sandbox_spec",
"serde",
"serde_json",
"thiserror 2.0.18",
"widestring",
"windows 0.62.2",
"windows-core 0.62.2",
"winreg 0.55.0",
"wxc_common",
]
[[package]]
name = "approx"
version = "0.5.1"
@@ -3458,19 +3437,17 @@ name = "codex-mxc-sandbox"
version = "0.7.0-dev.1"
dependencies = [
"anyhow",
"appcontainer_common",
"codex-network-proxy",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-path-uri",
"codex-windows-sandbox",
"learning_mode_windows",
"mxc-sdk",
"serde",
"serde_json",
"thiserror 2.0.18",
"tracelogging",
"windows-sys 0.61.2",
"wxc_common",
]
[[package]]
@@ -4615,6 +4592,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "const_format"
version = "0.2.35"
@@ -5254,7 +5237,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"const-oid 0.9.6",
"pem-rfc7468",
"zeroize",
]
@@ -5390,7 +5373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"const-oid",
"const-oid 0.9.6",
"crypto-common 0.1.7",
"subtle",
]
@@ -5402,6 +5385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.0",
"const-oid 0.10.2",
"crypto-common 0.2.2",
"ctutils",
]
@@ -7456,6 +7440,9 @@ dependencies = [
[[package]]
name = "hanzo-tui"
version = "0.7.0-dev.1"
dependencies = [
"hanzo-config",
]
[[package]]
name = "hash32"
@@ -8680,32 +8667,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "learning_mode_core"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
dependencies = [
"same-file",
"serde",
"serde_json",
"sha2 0.10.9",
"tempfile",
"thiserror 2.0.18",
]
[[package]]
name = "learning_mode_windows"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
dependencies = [
"learning_mode_core",
"sha2 0.10.9",
"thiserror 2.0.18",
"windows 0.62.2",
"windows-core 0.62.2",
"wxc_common",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -9116,22 +9077,37 @@ dependencies = [
]
[[package]]
name = "mxc_config_contract"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
name = "mxc-sdk"
version = "1.0.0"
source = "git+https://github.com/microsoft/mxc?rev=a846837d827b496fc4a08dfe60554515758c6647#a846837d827b496fc4a08dfe60554515758c6647"
dependencies = [
"anyhow",
"base64 0.22.1",
"cc",
"chrono",
"cidr",
"flatbuffers",
"getrandom 0.2.17",
"libc",
"nix 0.29.0",
"same-file",
"semver",
"serde",
"serde_json",
"serde_path_to_error",
"sha2 0.11.0",
"tempfile",
"thiserror 2.0.18",
]
[[package]]
name = "mxc_telemetry"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
dependencies = [
"tokio",
"tracelogging",
"unicode-general-category",
"url",
"uuid",
"widestring",
"windows 0.62.2",
"windows-core 0.62.2",
"winreg 0.55.0",
"winresource",
]
[[package]]
@@ -10451,14 +10427,6 @@ dependencies = [
"windows 0.62.2",
]
[[package]]
name = "process_security_environment_spec"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
dependencies = [
"flatbuffers",
]
[[package]]
name = "prodash"
version = "31.0.0"
@@ -11807,14 +11775,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "sandbox_spec"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
dependencies = [
"flatbuffers",
]
[[package]]
name = "schannel"
version = "0.1.28"
@@ -15273,6 +15233,15 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winresource"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087"
dependencies = [
"version_check",
]
[[package]]
name = "winsafe"
version = "0.0.19"
@@ -15420,30 +15389,6 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "wxc_common"
version = "0.8.0"
source = "git+https://github.com/microsoft/mxc?rev=6cd3d58f05d3447e67109cfb75e042803b843ca4#6cd3d58f05d3447e67109cfb75e042803b843ca4"
dependencies = [
"base64 0.22.1",
"cidr",
"getrandom 0.2.17",
"libc",
"mxc_config_contract",
"mxc_telemetry",
"semver",
"serde",
"serde_json",
"serde_path_to_error",
"thiserror 2.0.18",
"unicode-general-category",
"url",
"widestring",
"windows 0.62.2",
"windows-core 0.62.2",
"winreg 0.55.0",
]
[[package]]
name = "x11rb"
version = "0.13.2"
+1 -3
View File
@@ -172,12 +172,9 @@ codex-windows-sandbox-test-support = { path = "upstream/codex/codex-rs/windows-s
core_test_support = { path = "upstream/codex/codex-rs/core/tests/common" }
# External
learning_mode_windows = { git = "https://github.com/microsoft/mxc", rev = "6cd3d58f05d3447e67109cfb75e042803b843ca4" }
wxc_common = { git = "https://github.com/microsoft/mxc", rev = "6cd3d58f05d3447e67109cfb75e042803b843ca4" }
age = "0.12.1"
ansi-to-tui = "8.0.1"
anyhow = "1"
appcontainer_common = { git = "https://github.com/microsoft/mxc", rev = "6cd3d58f05d3447e67109cfb75e042803b843ca4" }
arboard = { version = "3", features = ["wayland-data-control"] }
arc-swap = "1.9.0"
assert_cmd = "2"
@@ -264,6 +261,7 @@ maplit = "1.0.2"
memchr = "2.7.6"
mime_guess = "2.0.5"
multimap = "0.10.0"
mxc-sdk = { git = "https://github.com/microsoft/mxc", rev = "a846837d827b496fc4a08dfe60554515758c6647" }
notify = "8.2.0"
nucleo = { git = "https://github.com/helix-editor/nucleo.git", rev = "4253de9faabb4e5c6d81d946a5e35a90f87347ee" }
once_cell = "1.20.2"
+1
View File
@@ -9,6 +9,7 @@ pub mod kai;
mod provider;
pub use provider::activate_provider;
pub use provider::clear_hanzo_api_key;
pub use provider::default_model_override;
pub use provider::hanzo_credential;
pub use provider::save_hanzo_api_key;
pub use provider::set_feature;
+17 -2
View File
@@ -181,7 +181,8 @@ pub(super) fn load_hanzo_credentials(home: &Path) {
/// Move a profile still on the startup-only credential to the refreshing one.
///
/// Earlier releases wrote the Hanzo provider with `env_key = "HANZO_USER_KEY"`,
/// Earlier releases wrote the Hanzo provider with `env_key = "HANZO_USER_KEY"`
/// (or `HANZO_API_KEY`, which nothing here ever read),
/// read once when the agent starts, so a session outlived its sign-in token and
/// failed with 401 an hour in. A provider block still holding exactly that
/// default is rewritten to ask `dev token` instead; a provider the user changed
@@ -201,7 +202,10 @@ pub(super) fn upgrade_auth(home: &Path) {
return;
};
if provider.get("auth").is_some()
|| provider.get("env_key").and_then(toml_edit::Item::as_str) != Some("HANZO_USER_KEY")
|| !matches!(
provider.get("env_key").and_then(toml_edit::Item::as_str),
Some("HANZO_USER_KEY" | "HANZO_API_KEY")
)
|| provider.get("base_url").and_then(toml_edit::Item::as_str) != Some(super::API_BASE)
{
return;
@@ -219,6 +223,17 @@ pub(super) fn upgrade_auth(home: &Path) {
});
}
/// The model a profile gets when it uses Hanzo and names none: Enso, whatever
/// the catalog lists first. A model the profile names, or another provider, is
/// the profile's own.
pub fn default_model_override(home: &Path) -> Option<String> {
let text = std::fs::read_to_string(home.join("config.toml")).ok()?;
let config: toml::Table = text.parse().ok()?;
(config.get("model_provider").and_then(toml::Value::as_str) == Some("hanzo")
&& !config.contains_key("model"))
.then(|| format!("model=\"{}\"", super::DEFAULT_MODEL))
}
#[cfg(test)]
#[path = "provider_tests.rs"]
mod tests;
+33
View File
@@ -98,3 +98,36 @@ fn a_provider_the_user_changed_is_left_alone() {
upgrade_auth(home.path());
assert_eq!(std::fs::read_to_string(&path).unwrap(), theirs);
}
#[test]
fn a_hanzo_profile_without_a_model_gets_enso() {
let home = tempfile::tempdir().expect("tempdir");
std::fs::write(home.path().join("config.toml"), "model_provider = \"hanzo\"\n").expect("write");
assert_eq!(
default_model_override(home.path()).as_deref(),
Some("model=\"enso-auto\"")
);
}
#[test]
fn a_model_the_profile_names_or_another_provider_is_left_alone() {
let home = tempfile::tempdir().expect("tempdir");
let path = home.path().join("config.toml");
std::fs::write(&path, "model_provider = \"hanzo\"\nmodel = \"mine\"\n").expect("write");
assert_eq!(default_model_override(home.path()), None);
std::fs::write(&path, "model_provider = \"pool\"\n").expect("write");
assert_eq!(default_model_override(home.path()), None);
std::fs::remove_file(&path).expect("remove");
assert_eq!(default_model_override(home.path()), None);
}
#[test]
fn the_api_key_name_nothing_reads_is_upgraded_too() {
let home = tempfile::tempdir().expect("tempdir");
let stale = OLD_PROVIDER.replace("HANZO_USER_KEY", "HANZO_API_KEY");
std::fs::write(home.path().join("config.toml"), stale).expect("write");
upgrade_auth(home.path());
let text = std::fs::read_to_string(home.path().join("config.toml")).expect("read");
assert!(text.contains("command = \"dev\""), "{text}");
assert!(!text.contains("HANZO_API_KEY"), "{text}");
}
+5 -1
View File
@@ -203,9 +203,13 @@ fn main() -> Result<()> {
async fn run(paths: Arg0DispatchPaths) -> Result<()> {
let Dev {
mut interactive,
overrides,
mut overrides,
command,
} = Dev::parse();
// A flag the user passes comes later, so it wins.
if let Some(model) = hanzo_config::default_model_override(&hanzo_config::home()) {
overrides.raw_overrides.insert(0, model);
}
let sandbox = paths.codex_linux_sandbox_exe.clone();
match command {
+3
View File
@@ -5,3 +5,6 @@ edition = "2021"
license = "Apache-2.0"
description = "Hanzo Dev tui integration"
repository = "https://github.com/hanzoai/dev"
[dependencies]
hanzo-config = { path = "../hanzo-config", version = "0.7.0-dev.1" }
+161
View File
@@ -0,0 +1,161 @@
//! The Hanzo sign-in as the interface shows it. The credential lives in the
//! Hanzo CLI, so each call here asks that CLI and hands back what it printed,
//! one line at a time.
use std::io::BufRead;
use std::io::BufReader;
use std::io::ErrorKind;
use std::process::Command;
use std::process::Stdio;
use std::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
const PROGRAM: &str = "hanzo";
const MISSING: &str =
"The Hanzo CLI is not installed. Install it with `curl -fsSL https://hanzo.sh | sh`.";
const PATIENCE: Duration = Duration::from_secs(15);
/// Who is signed in, as `hanzo auth show` reports it.
pub fn show(mut emit: impl FnMut(String)) {
let text = capture(PROGRAM, &["auth", "show"]);
if text.to_ascii_lowercase().contains("not signed in") {
emit("Not signed in. Use /login to sign in.".to_string());
return;
}
each_line(&text, &mut emit);
}
/// Sign out of the active Hanzo identity.
pub fn logout(mut emit: impl FnMut(String)) {
each_line(&capture(PROGRAM, &["auth", "logout"]), &mut emit);
}
/// Sign in through Hanzo IAM without leaving the interface. The sign-in runs
/// beside it, and every line it prints, such as the address to open, reaches
/// `emit` as it arrives.
pub fn login(emit: impl Fn(String) + Send + Sync + 'static) {
std::thread::spawn(move || stream(PROGRAM, &["auth", "login", "--provider", "hanzo"], emit));
}
fn each_line(text: &str, emit: &mut impl FnMut(String)) {
text.lines()
.map(str::trim_end)
.filter(|line| !line.trim().is_empty())
.for_each(|line| emit(line.to_string()));
}
/// What `program args` printed, or why it could not run.
fn capture(program: &str, args: &[&str]) -> String {
let mut command = Command::new(program);
command.args(args).stdin(Stdio::null());
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
let _ = sender.send(command.output());
});
match receiver.recv_timeout(PATIENCE) {
Ok(Ok(output)) => {
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
text.push('\n');
text.push_str(&String::from_utf8_lossy(&output.stderr));
text
}
Ok(Err(error)) if error.kind() == ErrorKind::NotFound => MISSING.to_string(),
Ok(Err(error)) => format!("Could not run the Hanzo CLI: {error}"),
Err(_) => "The Hanzo CLI did not answer.".to_string(),
}
}
/// Run `program args` to the end, passing along each line of output as it is
/// printed, then say how it ended.
fn stream(program: &str, args: &[&str], emit: impl Fn(String) + Send + Sync + 'static) {
let emit = Arc::new(emit);
let mut child = match Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(error) if error.kind() == ErrorKind::NotFound => return emit(MISSING.to_string()),
Err(error) => return emit(format!("Could not run the Hanzo CLI: {error}")),
};
let readers: Vec<_> = [
child.stdout.take().map(|pipe| Box::new(pipe) as Box<dyn std::io::Read + Send>),
child.stderr.take().map(|pipe| Box::new(pipe) as Box<dyn std::io::Read + Send>),
]
.into_iter()
.flatten()
.map(|pipe| {
let emit = Arc::clone(&emit);
std::thread::spawn(move || {
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
if !line.trim().is_empty() {
emit(line);
}
}
})
})
.collect();
let status = child.wait();
for reader in readers {
let _ = reader.join();
}
match status {
Ok(status) if status.success() => emit("Signed in to Hanzo.".to_string()),
Ok(status) => emit(format!("Sign-in did not complete ({status}).")),
Err(error) => emit(format!("Sign-in did not complete: {error}")),
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::Mutex;
/// A stand-in for the Hanzo CLI that prints a fixed script and exits.
fn cli(name: &str, body: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!("hanzo-tui-{}-{name}", std::process::id()));
std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).expect("write the stand-in");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
.expect("make it executable");
path
}
#[test]
fn a_missing_cli_says_how_to_install_it() {
assert_eq!(capture("hanzo-tui-no-such-program", &[]), MISSING);
}
#[test]
fn lines_arrive_in_order_and_the_end_is_reported() {
let program = cli("login", "echo 'Open https://hanzo.id/device'; echo 'Code: ABCD' >&2");
let seen = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&seen);
stream(program.to_str().expect("utf-8 path"), &[], move |line| {
sink.lock().expect("lock").push(line);
});
let mut seen = seen.lock().expect("lock").clone();
assert_eq!(seen.pop().as_deref(), Some("Signed in to Hanzo."));
seen.sort();
assert_eq!(seen, ["Code: ABCD", "Open https://hanzo.id/device"]);
std::fs::remove_file(program).expect("clean up");
}
#[test]
fn a_failed_sign_in_is_not_reported_as_one() {
let program = cli("fail", "echo 'denied'; exit 3");
let seen = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&seen);
stream(program.to_str().expect("utf-8 path"), &[], move |line| {
sink.lock().expect("lock").push(line);
});
let seen = seen.lock().expect("lock").clone();
assert_eq!(seen.first().map(String::as_str), Some("denied"));
assert!(seen.last().is_some_and(|line| line.starts_with("Sign-in did not complete")));
std::fs::remove_file(program).expect("clean up");
}
}
+4 -1
View File
@@ -1,11 +1,14 @@
//! Hanzo-owned presentation; the upstream TUI owns interaction and animation.
pub mod account;
pub mod model;
pub const PRODUCT_NAME: &str = "Hanzo Dev";
pub const INPUT_PLACEHOLDER: &str = "Ask Hanzo Dev to do anything";
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const STARTUP_TIP: &str = "Use **/mcp** to inspect connected tools. Hanzo MCP provides cloud controls and local development tools.";
/// Keep light terminals readable and dark input bars much subtler than Codex.
/// Keep light terminals readable and dark input bars much subtler than the upstream default.
pub fn input_background(background: (u8, u8, u8), light: bool) -> (u8, u8, u8) {
let (target, alpha) = if light { (0.0, 0.04) } else { (255.0, 0.055) };
let mix = |channel: u8| (channel as f32 * (1.0 - alpha) + target * alpha).round() as u8;
+82
View File
@@ -0,0 +1,82 @@
//! Finding the model a user means by `/model <name>`.
/// What a typed name came to.
#[derive(Debug, PartialEq, Eq)]
pub enum Pick {
One(String),
/// More than one model fits; the user names one of these.
Many(Vec<String>),
None,
}
/// Match `wanted` against the models the session offers and the ones Hanzo
/// serves: the exact name, then `<name>-auto`, then the only model whose name
/// contains it. Case does not matter.
pub fn pick(wanted: &str, offered: &[String]) -> Pick {
let wanted = wanted.trim().to_ascii_lowercase();
if wanted.is_empty() {
return Pick::None;
}
let mut names: Vec<String> = offered.to_vec();
for served in hanzo_config::catalog::MODELS {
if !names.iter().any(|name| name == served.slug) {
names.push(served.slug.to_string());
}
}
let lower = |name: &String| name.to_ascii_lowercase();
let exact = |target: &str| names.iter().find(|name| lower(name) == target).cloned();
if let Some(name) = exact(&wanted).or_else(|| exact(&format!("{wanted}-auto"))) {
return Pick::One(name);
}
let mut near: Vec<String> = names
.iter()
.filter(|name| lower(name).contains(&wanted))
.cloned()
.collect();
match near.len() {
0 => Pick::None,
1 => Pick::One(near.remove(0)),
_ => Pick::Many(near),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn offered() -> Vec<String> {
["gpt-6-astra", "gpt-6-sol"].map(String::from).to_vec()
}
#[test]
fn a_served_name_is_taken_as_it_is() {
assert_eq!(pick("enso", &offered()), Pick::One("enso".into()));
assert_eq!(pick("ENSO-PRO", &offered()), Pick::One("enso-pro".into()));
}
#[test]
fn a_family_name_finds_its_auto_model() {
assert_eq!(pick("zen6", &[]), Pick::One("zen6".into()));
assert_eq!(pick("gpt-6-astra", &offered()), Pick::One("gpt-6-astra".into()));
}
#[test]
fn the_only_model_containing_the_name_is_the_one() {
assert_eq!(pick("astra", &offered()), Pick::One("gpt-6-astra".into()));
}
#[test]
fn a_name_that_fits_several_asks_which() {
let Pick::Many(names) = pick("flash", &offered()) else {
panic!("flash fits several");
};
assert!(names.contains(&"enso-flash".to_string()));
assert!(names.contains(&"zen5-flash".to_string()));
}
#[test]
fn a_name_that_fits_nothing_is_not_guessed() {
assert_eq!(pick("nonesuch", &offered()), Pick::None);
assert_eq!(pick(" ", &offered()), Pick::None);
}
}
+18 -1
View File
@@ -30,7 +30,15 @@ impl Edit {
/// still there" cannot mean "not yet applied". Finding the *result* is what
/// means done, and it is what stops a second run from stacking the edit.
fn apply(&self, text: &str) -> Result<Option<String>> {
if text.contains(&self.new) {
// An edit that keeps its anchor is done once its result is there. One
// that replaces its anchor is done once the anchor is gone: a short
// result such as "Dev" is in most files before any edit has run.
let done = if self.new.contains(&self.old) {
text.contains(&self.new)
} else {
text.contains(&self.new) && !text.contains(&self.old)
};
if done {
return Ok(None);
}
let found = text.matches(&self.old).count();
@@ -102,6 +110,15 @@ mod tests {
assert!(e.apply("Hanzo Dev").unwrap().is_none());
}
/// A replacement whose result is shorter than its anchor, or common, must
/// still run: "Dev" is in most files before the first edit.
#[test]
fn a_result_already_present_elsewhere_does_not_hide_the_anchor() {
let e = edit("Codex", "Dev", 1);
assert_eq!(e.apply("Dev tools, Codex").unwrap().unwrap(), "Dev tools, Dev");
assert!(e.apply("Dev tools, Dev").unwrap().is_none(), "applied once the anchor is gone");
}
/// The whole point of the count: upstream moving the ground under an anchor
/// must stop preparation, not brand whatever happens to match.
#[test]
File diff suppressed because it is too large Load Diff