fix: serialize same-file mutations
This commit is contained in:
@@ -33,7 +33,7 @@ impl Tool for WriteTool {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_ctx: llm_worker::tool::ToolExecutionContext,
|
||||
ctx: llm_worker::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: WriteParams = serde_json::from_str(input_json)
|
||||
.map_err(|e| ToolError::InvalidArgument(format!("invalid Write input: {e}")))?;
|
||||
@@ -44,6 +44,8 @@ impl Tool for WriteTool {
|
||||
"Write"
|
||||
);
|
||||
|
||||
let _mutation_permit = self.tracker.acquire_mutation(¶ms.file_path, &ctx).await;
|
||||
|
||||
// Policy check: if the target already exists, it must have been
|
||||
// observed by the Read tool (via the tracker) and its current
|
||||
// contents must match the recorded hash.
|
||||
@@ -231,4 +233,90 @@ mod tests {
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ToolError::InvalidArgument(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_then_edit_same_file_same_batch_uses_call_order() {
|
||||
use crate::edit::edit_tool;
|
||||
use llm_worker::tool::ToolExecutionContext;
|
||||
|
||||
let (dir, fs, tracker) = setup();
|
||||
let file = dir.path().join("ordered.txt");
|
||||
|
||||
let write_def = write_tool(fs.clone(), tracker.clone());
|
||||
let (_, writer) = write_def();
|
||||
let edit_def = edit_tool(fs, tracker);
|
||||
let (_, editor) = edit_def();
|
||||
|
||||
let write_in = serde_json::json!({
|
||||
"file_path": file.to_str().unwrap(),
|
||||
"content": "hello",
|
||||
});
|
||||
let edit_in = serde_json::json!({
|
||||
"file_path": file.to_str().unwrap(),
|
||||
"old_string": "hello",
|
||||
"new_string": "goodbye",
|
||||
});
|
||||
|
||||
let write_json = write_in.to_string();
|
||||
let edit_json = edit_in.to_string();
|
||||
let (write_out, edit_out) = tokio::join!(
|
||||
writer.execute(&write_json, ToolExecutionContext::new("write", "batch", 0),),
|
||||
editor.execute(&edit_json, ToolExecutionContext::new("edit", "batch", 1)),
|
||||
);
|
||||
|
||||
write_out.unwrap();
|
||||
edit_out.unwrap();
|
||||
assert_eq!(std::fs::read_to_string(&file).unwrap(), "goodbye");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_same_file_mutation_releases_guard_for_followup() {
|
||||
use crate::edit::edit_tool;
|
||||
use llm_worker::tool::ToolExecutionContext;
|
||||
|
||||
let (dir, fs, tracker) = setup();
|
||||
let file = dir.path().join("release.txt");
|
||||
std::fs::write(&file, "alpha").unwrap();
|
||||
|
||||
let read_def = read_tool(fs.clone(), tracker.clone());
|
||||
let (_, reader) = read_def();
|
||||
reader
|
||||
.execute(
|
||||
&serde_json::json!({ "file_path": file.to_str().unwrap() }).to_string(),
|
||||
ToolExecutionContext::new("read", "pre", 0),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let edit_def = edit_tool(fs, tracker);
|
||||
let (_, editor) = edit_def();
|
||||
let bad_edit = serde_json::json!({
|
||||
"file_path": file.to_str().unwrap(),
|
||||
"old_string": "missing",
|
||||
"new_string": "beta",
|
||||
});
|
||||
let good_edit = serde_json::json!({
|
||||
"file_path": file.to_str().unwrap(),
|
||||
"old_string": "alpha",
|
||||
"new_string": "beta",
|
||||
});
|
||||
|
||||
assert!(
|
||||
editor
|
||||
.execute(
|
||||
&bad_edit.to_string(),
|
||||
ToolExecutionContext::new("bad", "batch", 0),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
editor
|
||||
.execute(
|
||||
&good_edit.to_string(),
|
||||
ToolExecutionContext::new("good", "batch", 1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(std::fs::read_to_string(&file).unwrap(), "beta");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user