Skip to content

Commit 6df2581

Browse files
committed
Add tests
1 parent 562e1aa commit 6df2581

File tree

9 files changed

+119
-99
lines changed

9 files changed

+119
-99
lines changed

Cargo.lock

Lines changed: 1 addition & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agent/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ shellexpand.workspace = true
6464
strum.workspace = true
6565
syntect = "5.2.0"
6666
sysinfo.workspace = true
67-
textwrap = "0.16.2"
67+
tempfile.workspace = true
6868
thiserror.workspace = true
6969
time.workspace = true
7070
tokio.workspace = true

crates/agent/src/agent/agent_config/parse.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,33 @@ use std::str::FromStr;
66
use crate::agent::agent_loop::types::ToolUseBlock;
77
use crate::agent::protocol::AgentError;
88
use crate::agent::tools::BuiltInToolName;
9-
use crate::agent::util::path::canonicalize_path;
9+
use crate::agent::util::path::canonicalize_path_impl;
10+
use crate::agent::util::providers::{
11+
RealProvider,
12+
SystemProvider,
13+
};
1014

1115
/// Represents a value from the `resources` array in the agent config.
16+
#[derive(Debug, Clone, PartialEq, Eq)]
1217
pub enum ResourceKind<'a> {
1318
File { original: &'a str, file_path: &'a str },
1419
FileGlob { original: &'a str, pattern: glob::Pattern },
1520
}
1621

1722
impl<'a> ResourceKind<'a> {
1823
pub fn parse(value: &'a str) -> Result<Self, String> {
24+
let sys = RealProvider;
25+
Self::parse_impl(value, &sys)
26+
}
27+
28+
fn parse_impl(value: &'a str, sys: &impl SystemProvider) -> Result<Self, String> {
1929
if !value.starts_with("file://") {
2030
return Err("Only file schemes are currently supported".to_string());
2131
}
2232

2333
let file_path = value.trim_start_matches("file://");
2434
if file_path.contains('*') || file_path.contains('?') {
25-
let canon = canonicalize_path(file_path)
35+
let canon = canonicalize_path_impl(file_path, sys, sys, sys)
2636
.map_err(|err| format!("Failed to canonicalize path for {}: {}", file_path, err))?;
2737
let pattern = glob::Pattern::new(canon.as_str())
2838
.map_err(|err| format!("Failed to create glob for {}: {}", canon, err))?;
@@ -244,3 +254,37 @@ impl FromStr for CanonicalToolName {
244254
}
245255
}
246256
}
257+
258+
#[cfg(test)]
259+
mod tests {
260+
use super::*;
261+
use crate::agent::util::test::TestSystem;
262+
263+
#[test]
264+
fn test_resource_kind_parse_nonfile() {
265+
assert!(
266+
ResourceKind::parse("https://google.com").is_err(),
267+
"non-file scheme should be an error"
268+
);
269+
}
270+
271+
#[test]
272+
fn test_resource_kind_parse_file_scheme() {
273+
let sys = TestSystem::new();
274+
275+
let resource = "file://project/README.md";
276+
assert_eq!(ResourceKind::parse_impl(resource, &sys).unwrap(), ResourceKind::File {
277+
original: resource,
278+
file_path: "project/README.md"
279+
});
280+
281+
let resource = "file://~/project/**/*.rs";
282+
assert_eq!(
283+
ResourceKind::parse_impl(resource, &sys).unwrap(),
284+
ResourceKind::FileGlob {
285+
original: resource,
286+
pattern: glob::Pattern::new("/home/testuser/project/**/*.rs").unwrap()
287+
}
288+
);
289+
}
290+
}

crates/agent/src/agent/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2104,3 +2104,11 @@ 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/tools/file_read.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ FEATURES:
4949
LIMITATIONS:
5050
- Maximum file size is 250KB
5151
- Cannot display binary files or images
52+
53+
TIPS:
54+
- Read multiple files in one go if you know you want to read more than one file
55+
- Dont use limit and offset for small files
5256
"#;
5357

5458
// TODO - migrate from JsonSchema, it's not very configurable and prone to breaking changes in the

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,25 @@ const FS_WRITE_SCHEMA: &str = r#"
4444
"type": "string",
4545
"enum": [
4646
"create",
47-
"str_replace",
47+
"strReplace",
4848
"insert"
4949
],
50-
"description": "The commands to run. Allowed options are: `create`, `str_replace`, `insert`"
50+
"description": "The commands to run. Allowed options are: `create`, `strReplace`, `insert`"
5151
},
5252
"content": {
5353
"description": "Required parameter of `create` and `insert` commands.",
5454
"type": "string"
5555
},
56-
"insert_line": {
56+
"insertLine": {
5757
"description": "Optional parameter of `insert` command. Line is 0-indexed. `content` will be inserted at the provided line. If not provided, content will be inserted at the end of the file on a new line, inserting a newline at the end of the file if it is missing.",
5858
"type": "integer"
5959
},
60-
"new_str": {
61-
"description": "Required parameter of `str_replace` command containing the new string.",
60+
"newStr": {
61+
"description": "Required parameter of `strReplace` command containing the new string.",
6262
"type": "string"
6363
},
64-
"old_str": {
65-
"description": "Required parameter of `str_replace` command containing the string in `path` to replace.",
64+
"oldStr": {
65+
"description": "Required parameter of `strReplace` command containing the string in `path` to replace.",
6666
"type": "string"
6767
},
6868
"path": {

crates/agent/src/agent/util/mod.rs

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ pub mod glob;
55
pub mod path;
66
pub mod providers;
77
pub mod request_channel;
8+
#[cfg(test)]
9+
pub mod test;
810

911
use std::collections::HashMap;
1012
use std::env::VarError;
@@ -100,6 +102,15 @@ pub async fn read_file_with_max_limit(
100102
.await
101103
.with_context(|| format!("Failed to query file metadata at '{}'", path.to_string_lossy()))?;
102104

105+
// Read only the max supported length.
106+
let mut reader = BufReader::new(file).take(max_file_length);
107+
let mut content = Vec::new();
108+
reader
109+
.read_to_end(&mut content)
110+
.await
111+
.with_context(|| format!("Failed to read from file at '{}'", path.to_string_lossy()))?;
112+
let mut content = content.to_str_lossy().to_string();
113+
103114
let truncated_amount = if md.size() > max_file_length {
104115
// Edge case check to ensure the suffix is less than max file length.
105116
if suffix.len() as u64 > max_file_length {
@@ -110,18 +121,11 @@ pub async fn read_file_with_max_limit(
110121
0
111122
};
112123

113-
// Read only the max supported length.
114-
let mut reader = BufReader::new(file).take(max_file_length);
115-
let mut content = Vec::new();
116-
reader
117-
.read_to_end(&mut content)
118-
.await
119-
.with_context(|| format!("Failed to read from file at '{}'", path.to_string_lossy()))?;
120-
121-
// Truncate content safely.
122-
let mut content = content.to_str_lossy().to_string();
123-
truncate_safe_in_place(&mut content, max_file_length as usize, suffix);
124+
if truncated_amount == 0 {
125+
return Ok((content, 0));
126+
}
124127

128+
content.replace_range((content.len().saturating_sub(suffix.len())).., suffix);
125129
Ok((content, truncated_amount))
126130
}
127131

@@ -132,6 +136,7 @@ pub fn is_integ_test() -> bool {
132136
#[cfg(test)]
133137
mod tests {
134138
use super::*;
139+
use crate::agent::util::test::TestDir;
135140

136141
#[test]
137142
fn test_truncate_safe() {
@@ -185,4 +190,26 @@ mod tests {
185190
assert_eq!(env_vars.get("KEY1").unwrap(), "Value is test_value");
186191
assert_eq!(env_vars.get("KEY2").unwrap(), "No substitution");
187192
}
193+
194+
#[tokio::test]
195+
async fn test_read_file_with_max_limit() {
196+
// Test file with 30 bytes in length
197+
let test_file = "123456789\n".repeat(3);
198+
let d = TestDir::new().with_file(("test.txt", &test_file)).await;
199+
200+
// Test not truncated
201+
let (content, bytes_truncated) = read_file_with_max_limit(d.path("test.txt"), 100, "...").await.unwrap();
202+
assert_eq!(content, test_file);
203+
assert_eq!(bytes_truncated, 0);
204+
205+
// Test truncated
206+
let (content, bytes_truncated) = read_file_with_max_limit(d.path("test.txt"), 10, "...").await.unwrap();
207+
assert_eq!(content, "1234567...");
208+
assert_eq!(bytes_truncated, 23);
209+
210+
// Test suffix greater than max length
211+
let (content, bytes_truncated) = read_file_with_max_limit(d.path("test.txt"), 1, "...").await.unwrap();
212+
assert_eq!(content, "");
213+
assert_eq!(bytes_truncated, 30);
214+
}
188215
}

crates/agent/src/agent/util/path.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ use super::providers::{
1212
CwdProvider,
1313
EnvProvider,
1414
HomeProvider,
15-
SystemProvider,
15+
RealProvider,
1616
};
1717

1818
/// Performs tilde and environment variable expansion on the provided input.
1919
pub fn expand_path(input: &str) -> Result<Cow<'_, str>, UtilError> {
20-
let sys = SystemProvider;
20+
let sys = RealProvider;
2121
Ok(shellexpand::full_with_context(
2222
input,
2323
sys.shellexpand_home(),
@@ -32,7 +32,7 @@ pub fn expand_path(input: &str) -> Result<Cow<'_, str>, UtilError> {
3232
/// - Performs env var expansion
3333
/// - Resolves `.` and `..` path components
3434
pub fn canonicalize_path(path: impl AsRef<str>) -> Result<String, UtilError> {
35-
let sys = SystemProvider;
35+
let sys = RealProvider;
3636
canonicalize_path_impl(path, &sys, &sys, &sys)
3737
}
3838

@@ -97,7 +97,7 @@ fn normalize_path(path: &Path) -> PathBuf {
9797
#[cfg(test)]
9898
mod tests {
9999
use super::*;
100-
use crate::agent::util::providers::TestSystem;
100+
use crate::agent::util::test::TestSystem;
101101

102102
#[test]
103103
fn test_canonicalize_path() {

crates/agent/src/agent/util/providers.rs

Lines changed: 10 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ use std::path::PathBuf;
33

44
use super::directories;
55

6+
/// A trait for accessing system and process context (env vars, home dir, current working dir,
7+
/// etc.).
8+
pub trait SystemProvider: EnvProvider + HomeProvider + CwdProvider {}
9+
10+
impl<T> SystemProvider for T where T: EnvProvider + HomeProvider + CwdProvider {}
11+
612
/// A trait for accessing environment variables.
713
///
814
/// This provides unit tests the capability to fake system context.
@@ -36,84 +42,22 @@ pub trait CwdProvider {
3642

3743
/// Provides real implementations for [EnvProvider], [HomeProvider], and [CwdProvider].
3844
#[derive(Clone, Copy)]
39-
pub struct SystemProvider;
45+
pub struct RealProvider;
4046

41-
impl EnvProvider for SystemProvider {
47+
impl EnvProvider for RealProvider {
4248
fn var(&self, input: &str) -> Result<String, VarError> {
4349
std::env::var(input)
4450
}
4551
}
4652

47-
impl HomeProvider for SystemProvider {
53+
impl HomeProvider for RealProvider {
4854
fn home(&self) -> Option<PathBuf> {
4955
directories::home_dir().ok()
5056
}
5157
}
5258

53-
impl CwdProvider for SystemProvider {
59+
impl CwdProvider for RealProvider {
5460
fn cwd(&self) -> Result<PathBuf, std::io::Error> {
5561
std::env::current_dir()
5662
}
5763
}
58-
59-
#[cfg(test)]
60-
#[derive(Debug, Clone)]
61-
pub struct TestSystem {
62-
env: std::collections::HashMap<String, String>,
63-
home: Option<PathBuf>,
64-
cwd: Option<PathBuf>,
65-
}
66-
67-
#[cfg(test)]
68-
impl TestSystem {
69-
pub fn new() -> Self {
70-
let mut env = std::collections::HashMap::new();
71-
env.insert("HOME".to_string(), "/home/testuser".to_string());
72-
Self {
73-
env,
74-
home: Some(PathBuf::from("/home/testuser")),
75-
cwd: Some(PathBuf::from("/home/testuser")),
76-
}
77-
}
78-
79-
pub fn with_var(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
80-
self.env.insert(key.as_ref().to_string(), value.as_ref().to_string());
81-
self
82-
}
83-
84-
pub fn with_cwd(mut self, cwd: impl AsRef<std::path::Path>) -> Self {
85-
self.cwd = Some(PathBuf::from(cwd.as_ref()));
86-
self
87-
}
88-
}
89-
90-
#[cfg(test)]
91-
impl Default for TestSystem {
92-
fn default() -> Self {
93-
Self::new()
94-
}
95-
}
96-
97-
#[cfg(test)]
98-
impl EnvProvider for TestSystem {
99-
fn var(&self, input: &str) -> Result<String, VarError> {
100-
self.env.get(input).cloned().ok_or(VarError::NotPresent)
101-
}
102-
}
103-
104-
#[cfg(test)]
105-
impl HomeProvider for TestSystem {
106-
fn home(&self) -> Option<PathBuf> {
107-
self.home.as_ref().cloned()
108-
}
109-
}
110-
111-
#[cfg(test)]
112-
impl CwdProvider for TestSystem {
113-
fn cwd(&self) -> Result<PathBuf, std::io::Error> {
114-
self.cwd.as_ref().cloned().ok_or(std::io::Error::new(
115-
std::io::ErrorKind::NotFound,
116-
eyre::eyre!("not found"),
117-
))
118-
}
119-
}

0 commit comments

Comments
 (0)