Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions crates/chat-cli/src/cli/chat/tools/fs_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,68 @@ mod tests {
assert_eq!(nested_content, "content in nested path\n");
}

// Issue 1932: printing the diff opened the literal "~/..." path and returned
// "No such file or directory", while fs_read already resolved that home path.
#[tokio::test]
async fn test_fs_write_tilde_path_queue_description() {
let os = Os::new().await.unwrap();
let mut output = Vec::new();
let mut line_tracker = HashMap::new();

let home = os.fs.chroot_path(os.env.home().unwrap_or_default());
os.fs.create_dir_all(&home).await.unwrap();

serde_json::from_value::<FsWrite>(serde_json::json!({
"path": "~/karakuri-checkstyle.xml",
"command": "create",
"file_text": "<?xml version=\"1.0\"?>\n<module>\n</module>\n"
}))
.unwrap()
.invoke(&os, &mut output, &mut line_tracker)
.await
.unwrap();

let replace = serde_json::from_value::<FsWrite>(serde_json::json!({
"path": "~/karakuri-checkstyle.xml",
"command": "str_replace",
"old_str": "<module>",
"new_str": "<module name=\"karakuri\">"
}))
.unwrap();

replace
.queue_description(&os, &mut output)
.expect("fs_write should print a diff for paths starting with ~/");

replace
.invoke(&os, &mut output, &mut line_tracker)
.await
.expect("fs_write should update a file addressed with ~/");

assert_eq!(
os.fs
.read_to_string(home.join("karakuri-checkstyle.xml"))
.await
.unwrap(),
"<?xml version=\"1.0\"?>\n<module name=\"karakuri\">\n</module>\n"
);

serde_json::from_value::<FsWrite>(serde_json::json!({
"path": "$HOME/from-home-env.txt",
"command": "create",
"file_text": "expanded"
}))
.unwrap()
.invoke(&os, &mut output, &mut line_tracker)
.await
.expect("fs_write should create a file addressed with $HOME");

assert_eq!(
os.fs.read_to_string(home.join("from-home-env.txt")).await.unwrap(),
"expanded\n"
);
}

#[tokio::test]
async fn test_eval_perm() {
const DENIED_PATH_ONE: &str = "/some/denied/path";
Expand Down
30 changes: 25 additions & 5 deletions crates/chat-cli/src/cli/chat/tools/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,17 +375,18 @@ impl Default for OutputKind {
}
}

/// Performs tilde expansion and other required sanitization modifications for handling tool use
/// path arguments.
/// Performs tilde / `$HOME` expansion and other required sanitization modifications for handling
/// tool use path arguments.
///
/// Required since path arguments are defined by the model.
/// Required since path arguments are defined by the model. Only a leading `~` or `$HOME`
/// component is expanded.
#[allow(dead_code)]
pub fn sanitize_path_tool_arg(os: &Os, path: impl AsRef<Path>) -> PathBuf {
let mut res = PathBuf::new();
// Expand `~` only if it is the first part.
// Expand `~` and `$HOME` only if either is the first part.
let mut path = path.as_ref().components();
match path.next() {
Some(p) if p.as_os_str() == "~" => {
Some(p) if is_home_path_alias(p.as_os_str()) => {
res.push(os.env.home().unwrap_or_default());
},
Some(p) => res.push(p),
Expand All @@ -399,6 +400,11 @@ pub fn sanitize_path_tool_arg(os: &Os, path: impl AsRef<Path>) -> PathBuf {
os.fs.chroot_path(res)
}

/// `~` and `$HOME` are aliases for the user home directory when they are the first path component.
fn is_home_path_alias(component: &std::ffi::OsStr) -> bool {
component == "~" || component == "$HOME"
}

/// Converts `path` to a relative path according to the current working directory `cwd`.
fn absolute_to_relative(cwd: impl AsRef<Path>, path: impl AsRef<Path>) -> Result<PathBuf> {
let cwd = cwd.as_ref().canonicalize()?;
Expand Down Expand Up @@ -562,12 +568,26 @@ mod tests {
os.fs.chroot_path(expected_home.join("hello")),
"tilde should expand"
);
let actual = sanitize_path_tool_arg(&os, "$HOME/hello");
assert_eq!(
actual,
os.fs.chroot_path(expected_home.join("hello")),
"$HOME should expand"
);
let actual = sanitize_path_tool_arg(&os, "$HOME");
assert_eq!(actual, os.fs.chroot_path(&expected_home), "$HOME should expand");
let actual = sanitize_path_tool_arg(&os, "/~");
assert_eq!(
actual,
os.fs.chroot_path("/~"),
"tilde should not expand when not the first component"
);
let actual = sanitize_path_tool_arg(&os, "dir/$HOME");
assert_eq!(
actual,
os.fs.chroot_path("dir/$HOME"),
"$HOME should not expand when not the first component"
);
}

#[tokio::test]
Expand Down