Skip to content

Commit c7134b7

Browse files
committed
wip
1 parent e410db9 commit c7134b7

File tree

8 files changed

+159
-35
lines changed

8 files changed

+159
-35
lines changed

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

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,17 @@ use std::str::FromStr;
55

66
use crate::agent::tools::BuiltInToolName;
77
use crate::agent::util::path::canonicalize_path_sys;
8-
use crate::agent::util::providers::{
9-
RealProvider,
10-
SystemProvider,
11-
};
8+
use crate::agent::util::providers::SystemProvider;
129

1310
/// Represents a value from the `resources` array in the agent config.
1411
#[derive(Debug, Clone, PartialEq, Eq)]
1512
pub enum ResourceKind<'a> {
16-
File { original: &'a str, file_path: &'a str },
13+
File { original: &'a str, file_path: String },
1714
FileGlob { original: &'a str, pattern: glob::Pattern },
1815
}
1916

2017
impl<'a> ResourceKind<'a> {
21-
pub fn parse(value: &'a str) -> Result<Self, String> {
22-
let sys = RealProvider;
23-
Self::parse_impl(value, &sys)
24-
}
25-
26-
fn parse_impl(value: &'a str, sys: &impl SystemProvider) -> Result<Self, String> {
18+
pub fn parse(value: &'a str, sys: &impl SystemProvider) -> Result<Self, String> {
2719
if !value.starts_with("file://") {
2820
return Err("Only file schemes are currently supported".to_string());
2921
}
@@ -41,7 +33,8 @@ impl<'a> ResourceKind<'a> {
4133
} else {
4234
Ok(Self::File {
4335
original: value,
44-
file_path,
36+
file_path: canonicalize_path_sys(file_path, sys)
37+
.map_err(|err| format!("Failed to canonicalize path for {}: {}", file_path, err))?,
4538
})
4639
}
4740
}
@@ -216,7 +209,7 @@ mod tests {
216209
#[test]
217210
fn test_resource_kind_parse_nonfile() {
218211
assert!(
219-
ResourceKind::parse("https://google.com").is_err(),
212+
ResourceKind::parse("https://google.com", &TestProvider::new()).is_err(),
220213
"non-file scheme should be an error"
221214
);
222215
}
@@ -226,18 +219,15 @@ mod tests {
226219
let sys = TestProvider::new();
227220

228221
let resource = "file://project/README.md";
229-
assert_eq!(ResourceKind::parse_impl(resource, &sys).unwrap(), ResourceKind::File {
222+
assert_eq!(ResourceKind::parse(resource, &sys).unwrap(), ResourceKind::File {
230223
original: resource,
231-
file_path: "project/README.md"
224+
file_path: "project/README.md".to_string()
232225
});
233226

234227
let resource = "file://~/project/**/*.rs";
235-
assert_eq!(
236-
ResourceKind::parse_impl(resource, &sys).unwrap(),
237-
ResourceKind::FileGlob {
238-
original: resource,
239-
pattern: glob::Pattern::new("/home/testuser/project/**/*.rs").unwrap()
240-
}
241-
);
228+
assert_eq!(ResourceKind::parse(resource, &sys).unwrap(), ResourceKind::FileGlob {
229+
original: resource,
230+
pattern: glob::Pattern::new("/home/testuser/project/**/*.rs").unwrap()
231+
});
242232
}
243233
}

crates/agent/src/agent/mod.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,18 +1645,20 @@ impl Agent {
16451645
match result {
16461646
ToolExecutorResult::Completed { id, result } => match result {
16471647
Ok(res) => {
1648+
let mut content_items = Vec::new();
16481649
for item in &res.items {
16491650
let content_item = match item {
16501651
ToolExecutionOutputItem::Text(s) => ToolResultContentBlock::Text(s.clone()),
16511652
ToolExecutionOutputItem::Json(v) => ToolResultContentBlock::Json(v.clone()),
16521653
ToolExecutionOutputItem::Image(i) => ToolResultContentBlock::Image(i.clone()),
16531654
};
1654-
content.push(ContentBlock::ToolResult(ToolResultBlock {
1655-
tool_use_id: id.tool_use_id().to_string(),
1656-
content: vec![content_item],
1657-
status: ToolResultStatus::Success,
1658-
}));
1655+
content_items.push(content_item);
16591656
}
1657+
content.push(ContentBlock::ToolResult(ToolResultBlock {
1658+
tool_use_id: id.tool_use_id().to_string(),
1659+
content: content_items,
1660+
status: ToolResultStatus::Success,
1661+
}));
16601662
},
16611663
Err(err) => content.push(ContentBlock::ToolResult(ToolResultBlock {
16621664
tool_use_id: id.tool_use_id().to_string(),
@@ -1916,7 +1918,7 @@ where
19161918

19171919
let mut return_val = Vec::new();
19181920
for resource in resources {
1919-
let Ok(kind) = ResourceKind::parse(resource.as_ref()) else {
1921+
let Ok(kind) = ResourceKind::parse(resource.as_ref(), provider) else {
19201922
continue;
19211923
};
19221924
match kind {
@@ -2165,3 +2167,34 @@ pub enum HookStage {
21652167
/// Hooks after executing tool uses
21662168
PostToolUse { tool_results: Vec<ToolExecutorResult> },
21672169
}
2170+
2171+
#[cfg(test)]
2172+
mod tests {
2173+
use super::*;
2174+
use crate::util::test::{
2175+
TestDir,
2176+
TestProvider,
2177+
};
2178+
2179+
#[tokio::test]
2180+
async fn test_collect_resources() {
2181+
let mut test_dir = TestDir::new();
2182+
let test_provider = TestProvider::new_with_base(test_dir.path());
2183+
2184+
let files = [
2185+
(".amazonq/rules/first.md", "first"),
2186+
(".amazonq/rules/dir/subdir.md", "subdir"),
2187+
("~/home.txt", "home"),
2188+
];
2189+
2190+
for file in files {
2191+
test_dir = test_dir.with_file_sys(file, &test_provider).await;
2192+
}
2193+
2194+
let resources = collect_resources(["file://.amazonq/rules/**/*.md", "file://~/home.txt"], &test_provider).await;
2195+
2196+
for file in files {
2197+
assert!(resources.iter().any(|r| r.content == file.1));
2198+
}
2199+
}
2200+
}

crates/agent/src/agent/task_executor/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ mod tests {
706706
})
707707
.await;
708708

709-
run_with_timeout(Duration::from_millis(100), async move {
709+
run_with_timeout(Duration::from_millis(1000), async move {
710710
let mut event_buf = Vec::new();
711711
loop {
712712
executor.recv_next(&mut event_buf).await;

crates/agent/src/agent/tools/fs_write.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ const FS_WRITE_SCHEMA: &str = r#"
6565
"description": "Required parameter of `strReplace` command containing the string in `path` to replace.",
6666
"type": "string"
6767
},
68+
"replaceAll": {
69+
"description": "Optional parameter of `strReplace` command. Default is false. When true, all instances of `oldStr` will be replaced with `newStr`.",
70+
"type": "boolean"
71+
},
6872
"path": {
6973
"description": "Path to the file",
7074
"type": "string"
@@ -204,6 +208,7 @@ pub struct StrReplace {
204208
path: String,
205209
old_str: String,
206210
new_str: String,
211+
#[serde(default)]
207212
replace_all: bool,
208213
}
209214

crates/agent/src/agent/util/test.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::path::{
66
PathBuf,
77
};
88

9+
use super::path::canonicalize_path_sys;
910
use super::providers::{
1011
CwdProvider,
1112
EnvProvider,
@@ -37,10 +38,11 @@ impl TestDir {
3738
/// Writes the given file under the test directory. Creates parent directories if needed.
3839
///
3940
/// The path given by `file` is *not* canonicalized.
41+
#[deprecated]
4042
pub async fn with_file(self, file: impl TestFile) -> Self {
4143
let file_path = file.path();
42-
if file_path.is_absolute() {
43-
panic!("absolute paths are currently not supported");
44+
if file_path.is_absolute() && !file_path.starts_with(self.temp_dir.path()) {
45+
panic!("path falls outside of the temp dir");
4446
}
4547

4648
let path = self.temp_dir.path().join(file_path);
@@ -52,6 +54,28 @@ impl TestDir {
5254
tokio::fs::write(path, file.content()).await.unwrap();
5355
self
5456
}
57+
58+
/// Writes the given file under the test directory. Creates parent directories if needed.
59+
///
60+
/// This function panics if the file path is outside of the test directory.
61+
pub async fn with_file_sys<P: SystemProvider>(self, file: impl TestFile, provider: &P) -> Self {
62+
let file_path = canonicalize_path_sys(file.path().to_string_lossy(), provider).unwrap();
63+
64+
// Check to ensure that the file path resolves under the test directory.
65+
if !file_path.starts_with(&self.temp_dir.path().to_string_lossy().to_string()) {
66+
panic!("outside of temp dir");
67+
}
68+
69+
let file_path = PathBuf::from(file_path);
70+
if let Some(parent) = file_path.parent() {
71+
if !parent.exists() {
72+
tokio::fs::create_dir_all(parent).await.unwrap();
73+
}
74+
}
75+
76+
tokio::fs::write(file_path, file.content()).await.unwrap();
77+
self
78+
}
5579
}
5680

5781
impl Default for TestDir {
@@ -79,6 +103,16 @@ where
79103
}
80104
}
81105

106+
impl TestFile for Box<dyn TestFile> {
107+
fn path(&self) -> PathBuf {
108+
(**self).path()
109+
}
110+
111+
fn content(&self) -> Vec<u8> {
112+
(**self).content()
113+
}
114+
}
115+
82116
/// Test helper that implements [EnvProvider], [HomeProvider], and [CwdProvider].
83117
#[derive(Debug, Clone)]
84118
pub struct TestProvider {
@@ -161,3 +195,34 @@ impl CwdProvider for TestProvider {
161195
}
162196

163197
impl SystemProvider for TestProvider {}
198+
199+
#[cfg(test)]
200+
mod tests {
201+
use tokio::fs;
202+
203+
use super::*;
204+
205+
#[tokio::test]
206+
async fn test_tempdir_files() {
207+
let mut test_dir = TestDir::new();
208+
let test_provider = TestProvider::new_with_base(test_dir.path());
209+
210+
let files = [("base", "base"), ("~/tilde", "tilde"), ("$HOME/home", "home")];
211+
for file in files {
212+
test_dir = test_dir.with_file_sys(file, &test_provider).await;
213+
}
214+
215+
assert_eq!(fs::read_to_string(test_dir.join("base")).await.unwrap(), "base");
216+
assert_eq!(fs::read_to_string(test_dir.join("tilde")).await.unwrap(), "tilde");
217+
assert_eq!(fs::read_to_string(test_dir.join("home")).await.unwrap(), "home");
218+
}
219+
220+
#[tokio::test]
221+
#[should_panic]
222+
async fn test_tempdir_write_file_outside() {
223+
let test_dir = TestDir::new();
224+
let test_provider = TestProvider::new_with_base(test_dir.path());
225+
226+
let _ = test_dir.with_file_sys(("..", "hello"), &test_provider).await;
227+
}
228+
}

crates/agent/src/cli/run.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ impl RunArgs {
165165

166166
if self.output_format == Some(OutputFormat::Json) {
167167
let md = user_turn_metadata.expect("user turn metadata should exist");
168-
println!("user turn metadata: {:?}", md);
169168
let is_error = md.end_reason != LoopEndReason::UserTurnEnd || md.result.as_ref().is_none_or(|v| v.is_err());
170169
let result = md.result.and_then(|r| r.ok().map(|m| m.text()));
171170

crates/agent/tests/common/mod.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use agent::agent_loop::protocol::{
1919
};
2020
use agent::agent_loop::types::{
2121
ContentBlock,
22+
Message,
2223
Role,
2324
ToolSpec,
2425
};
@@ -45,6 +46,8 @@ use rand::Rng as _;
4546
use rand::distr::Alphanumeric;
4647
use serde::Serialize;
4748

49+
type MockResponseStreams = Vec<Vec<StreamResult>>;
50+
4851
#[derive(Default)]
4952
pub struct TestCaseBuilder {
5053
test_name: Option<String>,
@@ -99,7 +102,13 @@ impl TestCaseBuilder {
99102
}
100103

101104
let mut agent = Agent::new(snapshot, Arc::new(model), McpManager::new().spawn()).await?;
102-
let temp_dir = TestDir::new();
105+
106+
let mut temp_dir = TestDir::new();
107+
let test_provider = TestProvider::new_with_base(temp_dir.path());
108+
for file in self.files {
109+
temp_dir = temp_dir.with_file_sys(file, &test_provider).await;
110+
}
111+
103112
agent.set_sys_provider(TestProvider::new_with_base(temp_dir.path()));
104113

105114
let test_name = self.test_name.unwrap_or(format!(
@@ -234,6 +243,10 @@ pub struct SentRequest {
234243
}
235244

236245
impl SentRequest {
246+
pub fn messages(&self) -> &[Message] {
247+
&self.original.messages
248+
}
249+
237250
pub fn prompt_contains_text(&self, text: impl AsRef<str>) -> bool {
238251
let text = text.as_ref();
239252
let prompt = self.original.messages.last().unwrap();
@@ -278,5 +291,3 @@ pub async fn parse_response_streams(content: impl AsRef<str>) -> Result<MockResp
278291
}
279292
Ok(stream)
280293
}
281-
282-
type MockResponseStreams = Vec<Vec<StreamResult>>;

crates/agent/tests/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ async fn test_agent_defaults() {
1616
const AMAZON_Q_MD_CONTENT: &str = "AmazonQ.md-FILE-CONTENT";
1717
const AGENTS_MD_CONTENT: &str = "AGENTS.md-FILE-CONTENT";
1818
const README_MD_CONTENT: &str = "README.md-FILE-CONTENT";
19+
const LOCAL_RULE_MD_CONTENT: &str = "local_rule.md-FILE-CONTENT";
20+
const SUB_LOCAL_RULE_MD_CONTENT: &str = "sub_local_rule.md-FILE-CONTENT";
1921

2022
let mut test = TestCase::builder()
2123
.test_name("agent default config behavior")
2224
.with_agent_config(AgentConfig::default())
2325
.with_file(("AmazonQ.md", AMAZON_Q_MD_CONTENT))
2426
.with_file(("AGENTS.md", AGENTS_MD_CONTENT))
2527
.with_file(("README.md", README_MD_CONTENT))
28+
.with_file((".amazonq/rules/local_rule.md", LOCAL_RULE_MD_CONTENT))
29+
.with_file((".amazonq/rules/subfolder/sub_local_rule.md", SUB_LOCAL_RULE_MD_CONTENT))
2630
.with_responses(
2731
parse_response_streams(include_str!("./mock_responses/builtin_tools.jsonl"))
2832
.await
@@ -49,4 +53,21 @@ async fn test_agent_defaults() {
4953
test.send_prompt("start turn".to_string()).await;
5054

5155
test.wait_until_agent_stop(Duration::from_secs(2)).await;
56+
57+
for req in test.requests() {
58+
let first_msg = req.messages().first().expect("first message should exist").text();
59+
let assert_contains = |expected: &str| {
60+
assert!(
61+
first_msg.contains(expected),
62+
"expected to find '{}' inside content: '{}'",
63+
expected,
64+
first_msg
65+
);
66+
};
67+
assert_contains(AMAZON_Q_MD_CONTENT);
68+
assert_contains(AGENTS_MD_CONTENT);
69+
assert_contains(README_MD_CONTENT);
70+
assert_contains(LOCAL_RULE_MD_CONTENT);
71+
assert_contains(SUB_LOCAL_RULE_MD_CONTENT);
72+
}
5273
}

0 commit comments

Comments
 (0)