Skip to content

Commit ae5dace

Browse files
committed
more tests and cleanup
1 parent 6df2581 commit ae5dace

File tree

10 files changed

+485
-30
lines changed

10 files changed

+485
-30
lines changed

crates/agent/src/agent/agent_loop/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use futures::{
1111
Stream,
1212
StreamExt,
1313
};
14-
use model::AgentLoopModel;
14+
use model::Model;
1515
use protocol::{
1616
AgentLoopEventKind,
1717
AgentLoopRequest,
@@ -642,7 +642,7 @@ impl AgentLoopHandle {
642642
self.loop_event_rx.recv().await
643643
}
644644

645-
pub async fn send_request<M: AgentLoopModel>(
645+
pub async fn send_request<M: Model>(
646646
&mut self,
647647
model: M,
648648
args: SendRequestArgs,

crates/agent/src/agent/agent_loop/model.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ use crate::agent::rts::RtsModel;
1818
/// Represents a backend implementation for a converse stream compatible API.
1919
///
2020
/// **Important** - implementations should be cancel safe
21-
pub trait Model {
21+
pub trait Model: std::fmt::Debug + Send + Sync + 'static {
22+
/// Sends a conversation to a model, returning a stream of events as the response.
2223
fn stream(
2324
&self,
2425
messages: Vec<Message>,
@@ -28,12 +29,6 @@ pub trait Model {
2829
) -> Pin<Box<dyn Stream<Item = Result<StreamEvent, StreamError>> + Send + 'static>>;
2930
}
3031

31-
/// Required for defining [Model] with a [Box<dyn Model>] for [super::AgentLoopRequest].
32-
pub trait AgentLoopModel: Model + std::fmt::Debug + Send + Sync + 'static {}
33-
34-
// Helper blanket impl
35-
impl<T> AgentLoopModel for T where T: Model + std::fmt::Debug + Send + Sync + 'static {}
36-
3732
/// The supported backends
3833
#[derive(Debug, Clone)]
3934
pub enum Models {

crates/agent/src/agent/agent_loop/protocol.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use serde::{
1010
};
1111
use tokio::sync::mpsc;
1212

13-
use super::model::AgentLoopModel;
13+
use super::model::Model;
1414
use super::types::{
1515
Message,
1616
MetadataEvent,
@@ -29,7 +29,7 @@ use super::{
2929
pub enum AgentLoopRequest {
3030
GetExecutionState,
3131
SendRequest {
32-
model: Box<dyn AgentLoopModel>,
32+
model: Box<dyn Model>,
3333
args: SendRequestArgs,
3434
},
3535
/// Ends the agent loop
@@ -135,8 +135,14 @@ pub enum AgentLoopEventKind {
135135
/// The agent loop has changed states
136136
LoopStateChange { from: LoopState, to: LoopState },
137137
/// Low level event. Generally only useful for [AgentLoop].
138+
///
139+
/// This reflects the exact event the agent loop parses from a [Model::stream] response as part
140+
/// of executing a user turn.
138141
StreamEvent(StreamEvent),
139142
/// Low level event. Generally only useful for [AgentLoop].
143+
///
144+
/// This reflects the exact event the agent loop parses from a [Model::stream] response as part
145+
/// of executing a user turn.
140146
StreamError(StreamError),
141147
}
142148

crates/agent/src/agent/mod.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2104,11 +2104,3 @@ pub enum HookStage {
21042104
/// Hooks after executing tool uses
21052105
PostToolUse { tool_results: Vec<ToolExecutorResult> },
21062106
}
2107-
2108-
#[cfg(test)]
2109-
mod tests {
2110-
use super::*;
2111-
2112-
#[tokio::test]
2113-
async fn test_collect_resources() {}
2114-
}

crates/agent/src/agent/protocol.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@ use super::types::AgentSnapshot;
2929
pub enum AgentEvent {
3030
/// Agent has finished initialization, and is ready to receive requests
3131
Initialized,
32-
/// Events associated with the agent loop
32+
/// Events associated with the agent loop.
33+
///
34+
/// These events contain information about the model's response, including:
35+
/// - Text content
36+
/// - Tool uses
37+
/// - Metadata about a response stream, and about a complete user turn
3338
AgentLoop(AgentLoopEvent),
3439
/// The exact request sent to the backend
3540
RequestSent(SendRequestArgs),
3641
/// An unknown error occurred with the model backend that could not be handled by the agent.
3742
RequestError(LoopError),
38-
/// An agent has changed state.
43+
/// The agent has changed state.
3944
StateChange { from: ExecutionState, to: ExecutionState },
4045
/// A tool use was requested by the model, and the permission was evaluated
4146
ToolPermissionEvalResult {

crates/agent/src/agent/tools/file_read.rs

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ impl FsReadOp {
148148
async fn execute(&self) -> Result<ToolExecutionOutputItem, ToolExecutionError> {
149149
let path = PathBuf::from(canonicalize_path(&self.path).map_err(|e| ToolExecutionError::Custom(e.to_string()))?);
150150

151-
// add line numbers
151+
// TODO: add line numbers
152152
let file_lines = LinesStream::new(
153153
BufReader::new(
154154
fs::File::open(&path)
@@ -157,7 +157,10 @@ impl FsReadOp {
157157
)
158158
.lines(),
159159
);
160-
let mut file_lines = file_lines.enumerate().skip(self.offset.unwrap_or_default() as usize);
160+
let mut file_lines = file_lines
161+
.enumerate()
162+
.skip(self.offset.unwrap_or_default() as usize)
163+
.take(self.limit.unwrap_or(u32::MAX) as usize);
161164

162165
let mut is_truncated = false;
163166
let mut content = Vec::new();
@@ -190,10 +193,100 @@ pub struct FileReadContext {}
190193
#[cfg(test)]
191194
mod tests {
192195
use super::*;
196+
use crate::agent::util::test::TestDir;
193197

194-
#[test]
195-
fn test_file_read_tool_schema() {
196-
let schema = FsRead::tool_schema();
197-
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
198+
#[tokio::test]
199+
async fn test_fs_read_single_file() {
200+
let test_dir = TestDir::new().with_file(("test.txt", "line1\nline2\nline3")).await;
201+
202+
let tool = FsRead {
203+
ops: vec![FsReadOp {
204+
path: test_dir.path("test.txt").to_string_lossy().to_string(),
205+
limit: None,
206+
offset: None,
207+
}],
208+
};
209+
210+
assert!(tool.validate().await.is_ok());
211+
let result = tool.execute().await.unwrap();
212+
assert_eq!(result.items.len(), 1);
213+
if let ToolExecutionOutputItem::Text(content) = &result.items[0] {
214+
assert_eq!(content, "line1\nline2\nline3");
215+
}
216+
}
217+
218+
#[tokio::test]
219+
async fn test_fs_read_with_offset_and_limit() {
220+
let test_dir = TestDir::new()
221+
.with_file(("test.txt", "line1\nline2\nline3\nline4\nline5"))
222+
.await;
223+
224+
let tool = FsRead {
225+
ops: vec![FsReadOp {
226+
path: test_dir.path("test.txt").to_string_lossy().to_string(),
227+
limit: Some(2),
228+
offset: Some(1),
229+
}],
230+
};
231+
232+
let result = tool.execute().await.unwrap();
233+
if let ToolExecutionOutputItem::Text(content) = &result.items[0] {
234+
assert_eq!(content, "line2\nline3");
235+
}
236+
}
237+
238+
#[tokio::test]
239+
async fn test_fs_read_multiple_files() {
240+
let test_dir = TestDir::new()
241+
.with_file(("file1.txt", "content1"))
242+
.await
243+
.with_file(("file2.txt", "content2"))
244+
.await;
245+
246+
let tool = FsRead {
247+
ops: vec![
248+
FsReadOp {
249+
path: test_dir.path("file1.txt").to_string_lossy().to_string(),
250+
limit: None,
251+
offset: None,
252+
},
253+
FsReadOp {
254+
path: test_dir.path("file2.txt").to_string_lossy().to_string(),
255+
limit: None,
256+
offset: None,
257+
},
258+
],
259+
};
260+
261+
let result = tool.execute().await.unwrap();
262+
assert_eq!(result.items.len(), 2);
263+
}
264+
265+
#[tokio::test]
266+
async fn test_fs_read_validate_nonexistent_file() {
267+
let tool = FsRead {
268+
ops: vec![FsReadOp {
269+
path: "/nonexistent/file.txt".to_string(),
270+
limit: None,
271+
offset: None,
272+
}],
273+
};
274+
275+
assert!(tool.validate().await.is_err());
276+
}
277+
278+
#[tokio::test]
279+
async fn test_fs_read_validate_directory_path() {
280+
let test_dir = TestDir::new();
281+
282+
let tool = FsRead {
283+
ops: vec![FsReadOp {
284+
path: test_dir.path("").to_string_lossy().to_string(),
285+
limit: None,
286+
offset: None,
287+
}],
288+
};
289+
290+
assert!(tool.validate().await.is_err());
198291
}
199292
}

crates/agent/src/agent/tools/file_write.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,141 @@ impl FileLineTracker {
342342
(self.lines_added_by_agent + self.lines_removed_by_agent) as isize
343343
}
344344
}
345+
#[cfg(test)]
346+
mod tests {
347+
use super::*;
348+
use crate::agent::util::test::TestDir;
349+
350+
#[tokio::test]
351+
async fn test_create_file() {
352+
let test_dir = TestDir::new();
353+
let tool = FsWrite::Create(FileCreate {
354+
path: test_dir.path("new.txt").to_string_lossy().to_string(),
355+
content: "hello world".to_string(),
356+
});
357+
358+
assert!(tool.validate().await.is_ok());
359+
assert!(tool.execute(None).await.is_ok());
360+
361+
let content = tokio::fs::read_to_string(test_dir.path("new.txt")).await.unwrap();
362+
assert_eq!(content, "hello world");
363+
}
364+
365+
#[tokio::test]
366+
async fn test_create_file_with_parent_dirs() {
367+
let test_dir = TestDir::new();
368+
let tool = FsWrite::Create(FileCreate {
369+
path: test_dir.path("nested/dir/file.txt").to_string_lossy().to_string(),
370+
content: "nested content".to_string(),
371+
});
372+
373+
assert!(tool.execute(None).await.is_ok());
374+
375+
let content = tokio::fs::read_to_string(test_dir.path("nested/dir/file.txt"))
376+
.await
377+
.unwrap();
378+
assert_eq!(content, "nested content");
379+
}
380+
381+
#[tokio::test]
382+
async fn test_str_replace_single_occurrence() {
383+
let test_dir = TestDir::new().with_file(("test.txt", "hello world")).await;
384+
385+
let tool = FsWrite::StrReplace(StrReplace {
386+
path: test_dir.path("test.txt").to_string_lossy().to_string(),
387+
old_str: "world".to_string(),
388+
new_str: "rust".to_string(),
389+
replace_all: false,
390+
});
391+
392+
assert!(tool.execute(None).await.is_ok());
393+
394+
let content = tokio::fs::read_to_string(test_dir.path("test.txt")).await.unwrap();
395+
assert_eq!(content, "hello rust");
396+
}
397+
398+
#[tokio::test]
399+
async fn test_str_replace_multiple_occurrences() {
400+
let test_dir = TestDir::new().with_file(("test.txt", "foo bar foo")).await;
401+
402+
let tool = FsWrite::StrReplace(StrReplace {
403+
path: test_dir.path("test.txt").to_string_lossy().to_string(),
404+
old_str: "foo".to_string(),
405+
new_str: "baz".to_string(),
406+
replace_all: true,
407+
});
408+
409+
assert!(tool.execute(None).await.is_ok());
410+
411+
let content = tokio::fs::read_to_string(test_dir.path("test.txt")).await.unwrap();
412+
assert_eq!(content, "baz bar baz");
413+
}
414+
415+
#[tokio::test]
416+
async fn test_str_replace_no_match() {
417+
let test_dir = TestDir::new().with_file(("test.txt", "hello world")).await;
418+
419+
let tool = FsWrite::StrReplace(StrReplace {
420+
path: test_dir.path("test.txt").to_string_lossy().to_string(),
421+
old_str: "missing".to_string(),
422+
new_str: "replacement".to_string(),
423+
replace_all: false,
424+
});
425+
426+
assert!(tool.execute(None).await.is_err());
427+
}
428+
429+
#[tokio::test]
430+
async fn test_insert_at_line() {
431+
let test_dir = TestDir::new().with_file(("test.txt", "line1\nline2\nline3")).await;
432+
433+
let tool = FsWrite::Insert(Insert {
434+
path: test_dir.path("test.txt").to_string_lossy().to_string(),
435+
content: "inserted".to_string(),
436+
insert_line: Some(1),
437+
});
438+
439+
assert!(tool.execute(None).await.is_ok());
440+
441+
let content = tokio::fs::read_to_string(test_dir.path("test.txt")).await.unwrap();
442+
assert_eq!(content, "line1\ninserted\nline2\nline3");
443+
}
444+
445+
#[tokio::test]
446+
async fn test_insert_append() {
447+
let test_dir = TestDir::new().with_file(("test.txt", "existing")).await;
448+
449+
let tool = FsWrite::Insert(Insert {
450+
path: test_dir.path("test.txt").to_string_lossy().to_string(),
451+
content: "appended".to_string(),
452+
insert_line: None,
453+
});
454+
455+
assert!(tool.execute(None).await.is_ok());
456+
457+
let content = tokio::fs::read_to_string(test_dir.path("test.txt")).await.unwrap();
458+
assert_eq!(content, "existing\nappended");
459+
}
460+
461+
#[tokio::test]
462+
async fn test_fs_write_validate_empty_path() {
463+
let tool = FsWrite::Create(FileCreate {
464+
path: "".to_string(),
465+
content: "content".to_string(),
466+
});
467+
468+
assert!(tool.validate().await.is_err());
469+
}
470+
471+
#[tokio::test]
472+
async fn test_fs_write_validate_nonexistent_file_for_replace() {
473+
let tool = FsWrite::StrReplace(StrReplace {
474+
path: "/nonexistent/file.txt".to_string(),
475+
old_str: "old".to_string(),
476+
new_str: "new".to_string(),
477+
replace_all: false,
478+
});
479+
480+
assert!(tool.validate().await.is_err());
481+
}
482+
}

0 commit comments

Comments
 (0)