Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
198 changes: 105 additions & 93 deletions crates/tui/src/core/engine/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use super::turn_loop::{
};
use crate::config::ApiProvider;
use crate::models::{SystemBlock, Usage};
use crate::prompts::{
PromptSessionContext, system_prompt_flat_text,
system_prompt_for_mode_with_context_skills_and_session,
};
use crate::test_support::{EnvVarGuard, lock_test_env};
use crate::tools::plan::{PlanItemArg, PlanSnapshot, StepStatus};
use crate::tools::spec::ToolCapability;
Expand Down Expand Up @@ -4887,39 +4891,37 @@ fn core_native_tools_stay_loaded_in_yolo_mode() {
// #4625: `Bash` is the model-facing shell tool; legacy exec_shell names
// are hidden compat aliases whose defer state no longer matters.
assert!(!should_default_defer_tool("Bash", &always_load));
// git_blame remains deferred (read-only git history beyond log/show/diff).
for canonical in ["File", "Git", "Run"] {
assert!(!should_default_defer_tool(canonical, &always_load));
}
assert!(default_active_native_tool_names().len() <= 10);
// Legacy spellings remain registered for replay but are no longer eager.
assert!(should_default_defer_tool("git_blame", &always_load));
}

#[test]
fn non_yolo_mode_retains_default_defer_policy() {
let always_load = HashSet::new();
assert!(!should_default_defer_tool("Bash", &always_load));
assert!(!should_default_defer_tool("edit_file", &always_load));
assert!(!should_default_defer_tool("apply_patch", &always_load));
assert!(!should_default_defer_tool("fetch_url", &always_load));
assert!(!should_default_defer_tool("git_diff", &always_load));
// #2654: read-only git history joins the active set.
assert!(!should_default_defer_tool("git_log", &always_load));
assert!(!should_default_defer_tool("git_show", &always_load));
assert!(!should_default_defer_tool("git_status", &always_load));
assert!(!should_default_defer_tool("run_tests", &always_load));
for canonical in ["Bash", "File", "Git", "Run"] {
assert!(!should_default_defer_tool(canonical, &always_load));
}
assert!(!should_default_defer_tool("agent", &always_load));
assert!(!should_default_defer_tool("read_file", &always_load));
assert!(!should_default_defer_tool("remember", &always_load));
assert!(!should_default_defer_tool(
"wait_for_dev_server",
&always_load
));
assert!(!should_default_defer_tool("web_search", &always_load));
assert!(!should_default_defer_tool("write_file", &always_load));
assert!(should_default_defer_tool(
REQUEST_USER_INPUT_NAME,
&always_load
));
assert!(should_default_defer_tool("task_shell_start", &always_load));
assert!(should_default_defer_tool("task_shell_wait", &always_load));
assert!(should_default_defer_tool("git_blame", &always_load));
for legacy in [
"read_file",
"edit_file",
"apply_patch",
"git_status",
"git_blame",
"run_tests",
"web_search",
] {
assert!(should_default_defer_tool(legacy, &always_load));
}
}

#[test]
Expand Down Expand Up @@ -4962,10 +4964,10 @@ fn model_tool_catalog_applies_native_and_mcp_deferral() {
let always_load = HashSet::new();
let catalog = build_model_tool_catalog(
vec![
api_tool("read_file"),
api_tool("write_file"),
api_tool("File"),
api_tool("Bash"),
api_tool("edit_file"),
api_tool("Git"),
api_tool("Run"),
api_tool("remember"),
api_tool("project_map"),
],
Expand All @@ -4981,10 +4983,10 @@ fn model_tool_catalog_applies_native_and_mcp_deferral() {
.and_then(|tool| tool.defer_loading)
};

assert_eq!(defer_loading("read_file"), Some(false));
assert_eq!(defer_loading("write_file"), Some(false));
assert_eq!(defer_loading("File"), Some(false));
assert_eq!(defer_loading("Bash"), Some(false));
assert_eq!(defer_loading("edit_file"), Some(false));
assert_eq!(defer_loading("Git"), Some(false));
assert_eq!(defer_loading("Run"), Some(false));
assert_eq!(defer_loading("remember"), Some(false));
assert_eq!(defer_loading("project_map"), Some(true));
assert_eq!(defer_loading("list_mcp_resources"), Some(false));
Expand All @@ -4997,13 +4999,12 @@ fn capability_compact_surface_defers_nonessential_core_tools() {
let catalog = build_model_tool_catalog_with_surface(
vec![
api_tool("agent"),
api_tool("grep_files"),
api_tool("read_file"),
api_tool("run_tests"),
api_tool("File"),
api_tool("Git"),
api_tool("Run"),
api_tool(TOOL_SEARCH_NAME),
api_tool("update_plan"),
api_tool("web_search"),
api_tool("write_file"),
api_tool("Web"),
],
vec![api_tool("list_mcp_resources"), api_tool("mcp_server_write")],
AppMode::Agent,
Expand All @@ -5018,34 +5019,29 @@ fn capability_compact_surface_defers_nonessential_core_tools() {
.and_then(|tool| tool.defer_loading)
};

assert_eq!(defer_loading("read_file"), Some(false));
assert_eq!(defer_loading("grep_files"), Some(false));
assert_eq!(defer_loading("File"), Some(false));
assert_eq!(defer_loading("Git"), Some(false));
assert_eq!(defer_loading("update_plan"), Some(false));
assert_eq!(defer_loading("write_file"), Some(false));
assert_eq!(defer_loading(TOOL_SEARCH_NAME), Some(false));
assert_eq!(defer_loading("list_mcp_resources"), Some(false));
assert_eq!(defer_loading("agent"), Some(true));
assert_eq!(defer_loading("run_tests"), Some(true));
assert_eq!(defer_loading("web_search"), Some(true));
assert_eq!(defer_loading("Run"), Some(true));
assert_eq!(defer_loading("Web"), Some(true));
assert_eq!(defer_loading("mcp_server_write"), Some(true));
}

#[test]
fn capability_full_surface_preserves_default_core_tools() {
let always_load = HashSet::new();
let catalog = build_model_tool_catalog_with_surface(
vec![
api_tool("agent"),
api_tool("read_file"),
api_tool("run_tests"),
],
vec![api_tool("agent"), api_tool("File"), api_tool("Run")],
Vec::new(),
AppMode::Agent,
&always_load,
crate::model_profile::ToolSurfaceBudget::Full,
);

for name in ["agent", "read_file", "run_tests"] {
for name in ["agent", "File", "Run"] {
assert_eq!(
catalog
.iter()
Expand All @@ -5061,7 +5057,7 @@ fn capability_full_surface_preserves_default_core_tools() {
fn plugin_or_benchmark_tools_marked_loaded_stay_active() {
let always_load = HashSet::new();
let mut catalog = build_model_tool_catalog(
vec![api_tool("KB_search"), api_tool("read_file")],
vec![api_tool("KB_search"), api_tool("File")],
Vec::new(),
AppMode::Agent,
&always_load,
Expand All @@ -5082,11 +5078,11 @@ fn plugin_or_benchmark_tools_marked_loaded_stay_active() {
active.contains("KB_search"),
"plugin/benchmark tools marked loaded must be callable on turn 1"
);
assert!(active.contains("read_file"));
assert!(active.contains("File"));
}

#[test]
fn agent_catalog_keeps_edit_file_loaded_when_fuzz_is_omitted() {
fn agent_catalog_keeps_canonical_file_tool_loaded() {
let (engine, _handle) = Engine::new(EngineConfig::default(), &Config::default());
let registry = engine
.build_turn_tool_registry_builder(
Expand All @@ -5102,39 +5098,34 @@ fn agent_catalog_keeps_edit_file_loaded_when_fuzz_is_omitted() {
AppMode::Agent,
&always_load,
);
let edit = catalog
let file = catalog
.iter()
.find(|tool| tool.name == "edit_file")
.expect("edit_file registered");
.find(|tool| tool.name == "File")
.expect("File registered");

assert_eq!(edit.defer_loading, Some(false));
let required = edit.input_schema["required"]
assert_eq!(file.defer_loading, Some(false));
let required = file.input_schema["required"]
.as_array()
.expect("edit_file schema should include required fields");
assert!(required.iter().any(|field| field.as_str() == Some("path")));
.expect("File schema should include required fields");
assert!(
required
.iter()
.any(|field| field.as_str() == Some("search"))
);
assert!(
required
.iter()
.any(|field| field.as_str() == Some("replace"))
.any(|field| field.as_str() == Some("action"))
);
assert!(!required.iter().any(|field| field.as_str() == Some("fuzz")));
assert_eq!(
edit.input_schema["properties"]["fuzz"]["type"].as_str(),
Some("boolean")
file.input_schema["properties"]["fuzz"]["oneOf"][0]["type"].as_str(),
Some("boolean"),
);

let active_at_batch_start = initial_active_tools(&catalog);
assert!(active_at_batch_start.contains("edit_file"));
assert!(active_at_batch_start.contains("File"));
let mut hydrated_this_batch = HashSet::new();
assert!(
maybe_hydrate_requested_deferred_tool(
"edit_file",
"File",
&json!({
"action": "edit",
"path": "src/foo.rs",
"search": "before",
"replace": "after"
Expand All @@ -5144,7 +5135,7 @@ fn agent_catalog_keeps_edit_file_loaded_when_fuzz_is_omitted() {
&mut hydrated_this_batch,
)
.is_none(),
"loaded edit_file calls without fuzz should execute instead of hydrating the schema"
"loaded File calls without fuzz should execute instead of hydrating the schema"
);
assert!(hydrated_this_batch.is_empty());
}
Expand Down Expand Up @@ -5178,9 +5169,7 @@ fn agent_catalog_advertises_and_searches_core_action_tools() {
.iter()
.map(|tool| tool.name.as_str())
.collect::<HashSet<_>>();
// #4625: the model-facing shell tool is `Bash`; `exec_shell` remains a
// hidden compat alias and must NOT appear in the advertised catalog.
for tool_name in ["Bash", "write_file", "edit_file", "apply_patch"] {
for tool_name in ["Bash", "File", "Git", "Run"] {
assert!(
names.contains(tool_name),
"{tool_name} must be advertised in Agent mode"
Expand Down Expand Up @@ -5353,6 +5342,44 @@ fn print_agent_tool_catalog_metrics() {
);
}

#[test]
#[ignore = "one-shot metric for scripts/measure-runtime-contract.py"]
#[allow(clippy::print_stdout)]
fn print_agent_runtime_contract_metrics() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().to_path_buf();
let session_context = PromptSessionContext::default();
let prompt = system_prompt_for_mode_with_context_skills_and_session(
&workspace,
None,
None,
None,
session_context,
);
let flat_prompt = system_prompt_flat_text(&prompt);
let prompt_bytes = flat_prompt.len();
let prompt_tokens_est = prompt_bytes.div_ceil(4);

// Mode runtime instructions are delivered as per-turn metadata; measure
// them separately so prompt-size work does not forget the volatile surface.
let mode_bytes = crate::prompts::AGENT_MODE.len();
let mode_tokens_est = mode_bytes.div_ceil(4);

println!(
"RUNTIME_CONTRACT_METRICS {}",
serde_json::json!({
"system_prompt_bytes": prompt_bytes,
"system_prompt_tokens_est": prompt_tokens_est,
"system_prompt_blocks": match prompt {
crate::models::SystemPrompt::Blocks(blocks) => blocks.len(),
_ => 1,
},
"agent_mode_instructions_bytes": mode_bytes,
"agent_mode_instructions_tokens_est": mode_tokens_est,
})
);
}

#[test]
fn deferred_tool_hydration_activates_without_guard_result_for_same_turn_retry() {
let mut edit = api_tool("edit_file");
Expand Down Expand Up @@ -5471,7 +5498,7 @@ fn deferred_edit_file_first_use_hydrates_schema_without_execution() {
fn model_tool_catalog_defers_non_core_native_tools_in_yolo_mode() {
let always_load = HashSet::new();
let catalog = build_model_tool_catalog(
vec![api_tool("read_file"), api_tool("project_map")],
vec![api_tool("File"), api_tool("project_map")],
vec![api_tool("mcp_server_write")],
AppMode::Yolo,
&always_load,
Expand All @@ -5484,7 +5511,7 @@ fn model_tool_catalog_defers_non_core_native_tools_in_yolo_mode() {
.and_then(|tool| tool.defer_loading)
};

assert_eq!(defer_loading("read_file"), Some(false));
assert_eq!(defer_loading("File"), Some(false));
assert_eq!(defer_loading("project_map"), Some(true));
assert_eq!(defer_loading("mcp_server_write"), Some(false));
}
Expand Down Expand Up @@ -5629,7 +5656,7 @@ fn active_tool_list_pushes_deferred_activations_to_the_tail() {
}

#[test]
fn deferred_tool_preflight_loads_edit_schema_without_executing_bad_aliases() {
fn hidden_edit_alias_bypasses_deferred_model_catalog_preflight() {
let (engine, _handle) = Engine::new(EngineConfig::default(), &Config::default());
let registry = engine
.build_turn_tool_registry_builder(
Expand All @@ -5647,11 +5674,11 @@ fn deferred_tool_preflight_loads_edit_schema_without_executing_bad_aliases() {
);
catalog
.iter_mut()
.find(|tool| tool.name == "edit_file")
.expect("edit_file registered")
.find(|tool| tool.name == "File")
.expect("File registered")
.defer_loading = Some(true);
let mut active = initial_active_tools(&catalog);
assert!(!active.contains("edit_file"));
assert!(!active.contains("File"));

let result = preflight_requested_deferred_tool(
"edit_file",
Expand All @@ -5662,22 +5689,10 @@ fn deferred_tool_preflight_loads_edit_schema_without_executing_bad_aliases() {
}),
&catalog,
&mut active,
)
.expect("deferred edit_file should preflight");

assert!(active.contains("edit_file"));
assert!(result.success);
assert!(result.content.contains("Tool `edit_file` was deferred"));
assert!(result.content.contains("The tool was not executed"));
assert!(result.content.contains("path: string required"));
assert!(result.content.contains("search: string required"));
assert!(result.content.contains("replace: string required"));
assert!(result.content.contains("old_string -> search"));
assert!(result.content.contains("new_string -> replace"));
assert_eq!(
result.metadata.as_ref().unwrap()["deferred_tool_loaded"],
json!(true)
);

assert!(result.is_none());
assert!(!active.contains("File"));
}

#[test]
Expand Down Expand Up @@ -7442,6 +7457,7 @@ fn turn_tool_registry_builder_keeps_plan_mode_read_only_for_files() {

assert!(registry.contains("read_file"));
assert!(registry.contains("list_dir"));
assert!(registry.contains("File"));
assert!(!registry.contains("write_file"));
assert!(!registry.contains("edit_file"));
assert!(!registry.contains("exec_shell"));
Expand Down Expand Up @@ -9808,11 +9824,7 @@ fn turn_metadata_includes_plan_mode_policy() {
);
assert!(text.contains("##### Mode: Plan"), "got: {text}");
assert!(
text.contains("All writes and patches are blocked"),
"got: {text}"
);
assert!(
text.contains("Shell and code execution are unavailable"),
text.contains("All writes, patches, shell commands, and code execution"),
"got: {text}"
);
}
Expand Down
Loading
Loading