mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Add e2e tests for ReadBeforeWriteSandbox through execute_and_emit_one_tool
Tests the full production path: execute_and_emit_one_tool → real tool executor → ReadBeforeWriteSandbox guard, covering write-blocked, read-then-write, grep-then-write, edit-blocked, and new-file-allowed scenarios. Also updates MutableMockSandbox::grep to do substring matching against in-memory files so the grep integration test works end-to-end. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d95a9acf77
commit
28bd9616a8
2 changed files with 213 additions and 2 deletions
|
|
@ -308,11 +308,20 @@ impl Sandbox for MutableMockSandbox {
|
|||
|
||||
async fn grep(
|
||||
&self,
|
||||
_pattern: &str,
|
||||
pattern: &str,
|
||||
_path: &str,
|
||||
_options: &GrepOptions,
|
||||
) -> Result<Vec<String>, String> {
|
||||
Ok(vec![])
|
||||
let files = self.files.lock().expect("files lock poisoned");
|
||||
let mut results = Vec::new();
|
||||
for (path, content) in files.iter() {
|
||||
for (i, line) in content.lines().enumerate() {
|
||||
if line.contains(pattern) {
|
||||
results.push(format!("{}:{}:{}", path, i + 1, line));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn glob(&self, _pattern: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
|
||||
|
|
|
|||
|
|
@ -602,4 +602,206 @@ mod tests {
|
|||
let content = result.content.to_string();
|
||||
assert!(content.contains("echo: hello"));
|
||||
}
|
||||
|
||||
// --- ReadBeforeWriteSandbox e2e tests ---
|
||||
|
||||
fn make_guarded_sandbox(files: HashMap<String, String>) -> Arc<dyn Sandbox> {
|
||||
Arc::new(
|
||||
crate::read_before_write_sandbox::ReadBeforeWriteSandbox::new(Arc::new(
|
||||
crate::test_support::MutableMockSandbox::new(files),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_to_unread_file_blocked() {
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry.register(crate::tools::make_write_file_tool());
|
||||
|
||||
let sandbox = make_guarded_sandbox(HashMap::from([("a.ts".into(), "content".into())]));
|
||||
let tc = make_tool_call(
|
||||
"write_file",
|
||||
"call_1",
|
||||
serde_json::json!({"file_path": "a.ts", "content": "new"}),
|
||||
);
|
||||
let emitter = EventEmitter::new();
|
||||
let config = SessionConfig::default();
|
||||
|
||||
let result = execute_and_emit_one_tool(
|
||||
&tc,
|
||||
®istry,
|
||||
sandbox,
|
||||
None,
|
||||
CancellationToken::new(),
|
||||
&config,
|
||||
&emitter,
|
||||
"test-session",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_error);
|
||||
assert!(result.content.to_string().contains("has not been read"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_then_write_succeeds() {
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry.register(crate::tools::make_read_file_tool());
|
||||
registry.register(crate::tools::make_write_file_tool());
|
||||
|
||||
let sandbox = make_guarded_sandbox(HashMap::from([("a.ts".into(), "content".into())]));
|
||||
let emitter = EventEmitter::new();
|
||||
let config = SessionConfig::default();
|
||||
|
||||
// First read the file
|
||||
let read_tc = make_tool_call(
|
||||
"read_file",
|
||||
"call_1",
|
||||
serde_json::json!({"file_path": "a.ts"}),
|
||||
);
|
||||
let read_result = execute_and_emit_one_tool(
|
||||
&read_tc,
|
||||
®istry,
|
||||
sandbox.clone(),
|
||||
None,
|
||||
CancellationToken::new(),
|
||||
&config,
|
||||
&emitter,
|
||||
"test-session",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(!read_result.is_error);
|
||||
|
||||
// Then write should succeed
|
||||
let write_tc = make_tool_call(
|
||||
"write_file",
|
||||
"call_2",
|
||||
serde_json::json!({"file_path": "a.ts", "content": "new"}),
|
||||
);
|
||||
let write_result = execute_and_emit_one_tool(
|
||||
&write_tc,
|
||||
®istry,
|
||||
sandbox,
|
||||
None,
|
||||
CancellationToken::new(),
|
||||
&config,
|
||||
&emitter,
|
||||
"test-session",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!write_result.is_error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn grep_then_write_succeeds() {
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry.register(crate::tools::make_grep_tool());
|
||||
registry.register(crate::tools::make_write_file_tool());
|
||||
|
||||
let sandbox = make_guarded_sandbox(HashMap::from([("a.ts".into(), "content".into())]));
|
||||
let emitter = EventEmitter::new();
|
||||
let config = SessionConfig::default();
|
||||
|
||||
// Grep matching a.ts
|
||||
let grep_tc = make_tool_call("grep", "call_1", serde_json::json!({"pattern": "content"}));
|
||||
let grep_result = execute_and_emit_one_tool(
|
||||
&grep_tc,
|
||||
®istry,
|
||||
sandbox.clone(),
|
||||
None,
|
||||
CancellationToken::new(),
|
||||
&config,
|
||||
&emitter,
|
||||
"test-session",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert!(!grep_result.is_error);
|
||||
|
||||
// Then write should succeed
|
||||
let write_tc = make_tool_call(
|
||||
"write_file",
|
||||
"call_2",
|
||||
serde_json::json!({"file_path": "a.ts", "content": "new"}),
|
||||
);
|
||||
let write_result = execute_and_emit_one_tool(
|
||||
&write_tc,
|
||||
®istry,
|
||||
sandbox,
|
||||
None,
|
||||
CancellationToken::new(),
|
||||
&config,
|
||||
&emitter,
|
||||
"test-session",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!write_result.is_error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edit_unread_file_blocked() {
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry.register(crate::tools::make_edit_file_tool());
|
||||
|
||||
let sandbox = make_guarded_sandbox(HashMap::from([("a.ts".into(), "content".into())]));
|
||||
let tc = make_tool_call(
|
||||
"edit_file",
|
||||
"call_1",
|
||||
serde_json::json!({"file_path": "a.ts", "old_string": "content", "new_string": "updated"}),
|
||||
);
|
||||
let emitter = EventEmitter::new();
|
||||
let config = SessionConfig::default();
|
||||
|
||||
let result = execute_and_emit_one_tool(
|
||||
&tc,
|
||||
®istry,
|
||||
sandbox,
|
||||
None,
|
||||
CancellationToken::new(),
|
||||
&config,
|
||||
&emitter,
|
||||
"test-session",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_error);
|
||||
assert!(result.content.to_string().contains("has not been read"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn write_new_file_succeeds() {
|
||||
let mut registry = ToolRegistry::new();
|
||||
registry.register(crate::tools::make_write_file_tool());
|
||||
|
||||
let sandbox = make_guarded_sandbox(HashMap::new());
|
||||
let tc = make_tool_call(
|
||||
"write_file",
|
||||
"call_1",
|
||||
serde_json::json!({"file_path": "new.ts", "content": "hello"}),
|
||||
);
|
||||
let emitter = EventEmitter::new();
|
||||
let config = SessionConfig::default();
|
||||
|
||||
let result = execute_and_emit_one_tool(
|
||||
&tc,
|
||||
®istry,
|
||||
sandbox,
|
||||
None,
|
||||
CancellationToken::new(),
|
||||
&config,
|
||||
&emitter,
|
||||
"test-session",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(!result.is_error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue