test: add regression coverage for edit_file fuzz omission (#2138)
- Test that edit_file accepts calls with fuzz omitted, fuzz=false, and fuzz=true - Verify fuzz is excluded from schema required fields but present as optional boolean - Add agent-mode catalog test confirming edit_file is loaded and fuzz-less calls execute - Update existing required-fields assertions to check for exactly path/search/replace
This commit is contained in:
@@ -472,6 +472,70 @@ fn model_tool_catalog_applies_native_and_mcp_deferral() {
|
||||
assert_eq!(defer_loading("mcp_server_write"), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_catalog_keeps_edit_file_loaded_when_fuzz_is_omitted() {
|
||||
let (engine, _handle) = Engine::new(EngineConfig::default(), &Config::default());
|
||||
let registry = engine
|
||||
.build_turn_tool_registry_builder(
|
||||
AppMode::Agent,
|
||||
engine.config.todos.clone(),
|
||||
engine.config.plan_state.clone(),
|
||||
)
|
||||
.build(engine.build_tool_context(AppMode::Agent, false));
|
||||
let always_load = HashSet::new();
|
||||
let catalog = build_model_tool_catalog(
|
||||
registry.to_api_tools_with_cache(true),
|
||||
vec![],
|
||||
AppMode::Agent,
|
||||
&always_load,
|
||||
);
|
||||
let edit = catalog
|
||||
.iter()
|
||||
.find(|tool| tool.name == "edit_file")
|
||||
.expect("edit_file registered");
|
||||
|
||||
assert_eq!(edit.defer_loading, Some(false));
|
||||
let required = edit.input_schema["required"]
|
||||
.as_array()
|
||||
.expect("edit_file schema should include required fields");
|
||||
assert!(required.iter().any(|field| field.as_str() == Some("path")));
|
||||
assert!(
|
||||
required
|
||||
.iter()
|
||||
.any(|field| field.as_str() == Some("search"))
|
||||
);
|
||||
assert!(
|
||||
required
|
||||
.iter()
|
||||
.any(|field| field.as_str() == Some("replace"))
|
||||
);
|
||||
assert!(!required.iter().any(|field| field.as_str() == Some("fuzz")));
|
||||
assert_eq!(
|
||||
edit.input_schema["properties"]["fuzz"]["type"].as_str(),
|
||||
Some("boolean")
|
||||
);
|
||||
|
||||
let active_at_batch_start = initial_active_tools(&catalog);
|
||||
assert!(active_at_batch_start.contains("edit_file"));
|
||||
let mut hydrated_this_batch = HashSet::new();
|
||||
assert!(
|
||||
maybe_hydrate_requested_deferred_tool(
|
||||
"edit_file",
|
||||
&json!({
|
||||
"path": "src/foo.rs",
|
||||
"search": "before",
|
||||
"replace": "after"
|
||||
}),
|
||||
&catalog,
|
||||
&active_at_batch_start,
|
||||
&mut hydrated_this_batch,
|
||||
)
|
||||
.is_none(),
|
||||
"loaded edit_file calls without fuzz should execute instead of hydrating the schema"
|
||||
);
|
||||
assert!(hydrated_this_batch.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tools_always_load_overrides_default_native_deferral() {
|
||||
let always_load = HashSet::from(["git_show".to_string()]);
|
||||
|
||||
@@ -1475,6 +1475,41 @@ mod tests {
|
||||
assert_eq!(edited, "hi world hi");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_edit_file_accepts_omitted_and_explicit_fuzz() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let ctx = ToolContext::new(tmp.path().to_path_buf());
|
||||
let tool = EditFileTool;
|
||||
|
||||
for (file_name, fuzz) in [
|
||||
("fuzz_omitted.txt", None),
|
||||
("fuzz_false.txt", Some(false)),
|
||||
("fuzz_true.txt", Some(true)),
|
||||
] {
|
||||
let test_file = tmp.path().join(file_name);
|
||||
fs::write(&test_file, "hello world").expect("write");
|
||||
|
||||
let mut input = serde_json::Map::from_iter([
|
||||
("path".to_string(), json!(file_name)),
|
||||
("search".to_string(), json!("hello")),
|
||||
("replace".to_string(), json!("hi")),
|
||||
]);
|
||||
if let Some(fuzz) = fuzz {
|
||||
input.insert("fuzz".to_string(), json!(fuzz));
|
||||
}
|
||||
|
||||
let result = tool
|
||||
.execute(Value::Object(input), &ctx)
|
||||
.await
|
||||
.expect("execute");
|
||||
|
||||
assert!(result.success, "{file_name}: {}", result.content);
|
||||
assert!(result.content.contains("Replaced 1 occurrence"));
|
||||
let edited = fs::read_to_string(&test_file).expect("read");
|
||||
assert_eq!(edited, "hi world");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_edit_file_single_match_has_no_multi_match_warning() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
@@ -1827,7 +1862,13 @@ mod tests {
|
||||
.get("required")
|
||||
.and_then(|value| value.as_array())
|
||||
.expect("edit schema should include required array");
|
||||
assert_eq!(required.len(), 3);
|
||||
let required_fields: Vec<_> = required.iter().filter_map(|value| value.as_str()).collect();
|
||||
assert_eq!(required_fields, vec!["path", "search", "replace"]);
|
||||
assert!(!required_fields.contains(&"fuzz"));
|
||||
assert_eq!(
|
||||
edit_schema["properties"]["fuzz"]["type"].as_str(),
|
||||
Some("boolean")
|
||||
);
|
||||
let search_desc = edit_schema["properties"]["search"]["description"]
|
||||
.as_str()
|
||||
.expect("search description");
|
||||
|
||||
Reference in New Issue
Block a user