fix: serialize same-file mutations

This commit is contained in:
2026-06-10 18:24:53 +09:00
parent 536ff4dd57
commit 401301438d
4 changed files with 216 additions and 4 deletions
+89 -1
View File
@@ -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(&params.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");
}
}