use runtime::Hook;
use sdk::tools::os::OsHook;
use std::path::PathBuf;
use wcore::ToolDispatch;
fn hook(cwd: PathBuf) -> OsHook {
OsHook::new(cwd)
}
fn dispatch(args: &str) -> ToolDispatch {
ToolDispatch {
args: args.to_owned(),
agent: "agent".into(),
sender: String::new(),
conversation_id: None,
call_id: String::new(),
}
}
async fn call(hook: &OsHook, tool: &str, args: &str) -> Result<String, String> {
hook.dispatch(tool, dispatch(args))
.expect("hook should handle this tool")
.await
}
#[tokio::test]
async fn read_basic() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hello.txt");
std::fs::write(&file, "line one\nline two\nline three\n").unwrap();
let h = hook(dir.path().to_path_buf());
let args = format!(r#"{{"path":"{}"}}"#, file.display());
let result = call(&h, "read", &args).await.unwrap();
assert!(result.contains("1\tline one"));
assert!(result.contains("2\tline two"));
assert!(result.contains("3\tline three"));
assert!(result.contains("--- 3 total lines ---"));
}
#[tokio::test]
async fn read_offset_limit() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("lines.txt");
let content: String = (1..=100).map(|i| format!("line {i}\n")).collect();
std::fs::write(&file, &content).unwrap();
let h = hook(dir.path().to_path_buf());
let args = format!(r#"{{"path":"{}","offset":10,"limit":5}}"#, file.display());
let result = call(&h, "read", &args).await.unwrap();
assert!(result.contains("10\tline 10"));
assert!(result.contains("14\tline 14"));
assert!(!result.contains("15\tline 15"));
assert!(result.contains("showing lines 10-14"));
assert!(result.contains("100 total lines"));
}
#[tokio::test]
async fn read_missing() {
let h = hook(PathBuf::from("/tmp"));
let err = call(&h, "read", r#"{"path":"/nonexistent/file.txt"}"#)
.await
.unwrap_err();
assert!(err.contains("error reading"));
}
#[tokio::test]
async fn read_offset_past_end() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("short.txt");
std::fs::write(&file, "one\ntwo\n").unwrap();
let h = hook(dir.path().to_path_buf());
let args = format!(r#"{{"path":"{}","offset":999}}"#, file.display());
let result = call(&h, "read", &args).await.unwrap();
assert!(result.contains("past end of file"));
}
#[tokio::test]
async fn read_relative_path() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("rel.txt"), "content\n").unwrap();
let h = hook(dir.path().to_path_buf());
let result = call(&h, "read", r#"{"path":"rel.txt"}"#).await.unwrap();
assert!(result.contains("1\tcontent"));
}
#[tokio::test]
async fn read_large_file_rejected() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("big.bin");
let f = std::fs::File::create(&file).unwrap();
f.set_len(51 * 1024 * 1024).unwrap();
let h = hook(dir.path().to_path_buf());
let args = format!(r#"{{"path":"{}"}}"#, file.display());
let err = call(&h, "read", &args).await.unwrap_err();
assert!(err.contains("file is too large"));
}
#[tokio::test]
async fn edit_basic() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("edit.txt");
std::fs::write(&file, "hello world\n").unwrap();
let h = hook(dir.path().to_path_buf());
let read_args = format!(r#"{{"path":"{}"}}"#, file.display());
call(&h, "read", &read_args).await.unwrap();
let args = format!(
r#"{{"path":"{}","old_string":"hello","new_string":"goodbye"}}"#,
file.display()
);
let result = call(&h, "edit", &args).await.unwrap();
assert_eq!(result, "ok");
assert_eq!(std::fs::read_to_string(&file).unwrap(), "goodbye world\n");
}
#[tokio::test]
async fn edit_not_found() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("edit.txt");
std::fs::write(&file, "hello world\n").unwrap();
let h = hook(dir.path().to_path_buf());
let read_args = format!(r#"{{"path":"{}"}}"#, file.display());
call(&h, "read", &read_args).await.unwrap();
let args = format!(
r#"{{"path":"{}","old_string":"missing","new_string":"x"}}"#,
file.display()
);
let err = call(&h, "edit", &args).await.unwrap_err();
assert_eq!(err, "old_string not found");
}
#[tokio::test]
async fn edit_not_unique() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("dup.txt");
std::fs::write(&file, "aaa bbb aaa\n").unwrap();
let h = hook(dir.path().to_path_buf());
let read_args = format!(r#"{{"path":"{}"}}"#, file.display());
call(&h, "read", &read_args).await.unwrap();
let args = format!(
r#"{{"path":"{}","old_string":"aaa","new_string":"ccc"}}"#,
file.display()
);
let err = call(&h, "edit", &args).await.unwrap_err();
assert!(err.contains("not unique"));
assert!(err.contains("2 occurrences"));
}
#[tokio::test]
async fn edit_identical_strings() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("same.txt");
std::fs::write(&file, "hello\n").unwrap();
let h = hook(dir.path().to_path_buf());
let read_args = format!(r#"{{"path":"{}"}}"#, file.display());
call(&h, "read", &read_args).await.unwrap();
let args = format!(
r#"{{"path":"{}","old_string":"hello","new_string":"hello"}}"#,
file.display()
);
let err = call(&h, "edit", &args).await.unwrap_err();
assert!(err.contains("identical"));
}
#[tokio::test]
async fn edit_empty_old_string() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("empty.txt");
std::fs::write(&file, "hello\n").unwrap();
let h = hook(dir.path().to_path_buf());
let read_args = format!(r#"{{"path":"{}"}}"#, file.display());
call(&h, "read", &read_args).await.unwrap();
let args = format!(
r#"{{"path":"{}","old_string":"","new_string":"x"}}"#,
file.display()
);
let err = call(&h, "edit", &args).await.unwrap_err();
assert!(err.contains("must not be empty"));
}
#[tokio::test]
async fn edit_missing_file() {
let h = hook(PathBuf::from("/tmp"));
let args = r#"{"path":"/nonexistent/file.txt","old_string":"a","new_string":"b"}"#;
let err = call(&h, "edit", args).await.unwrap_err();
assert!(err.contains("you must read"));
}
#[tokio::test]
async fn edit_without_read_rejected() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("no_read.txt");
std::fs::write(&file, "hello\n").unwrap();
let h = hook(dir.path().to_path_buf());
let args = format!(
r#"{{"path":"{}","old_string":"hello","new_string":"bye"}}"#,
file.display()
);
let err = call(&h, "edit", &args).await.unwrap_err();
assert!(err.contains("you must read"));
}
#[tokio::test]
async fn read_allowed_for_gateway_sender() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("gateway.txt");
std::fs::write(&file, "test content\n").unwrap();
let h = hook(dir.path().to_path_buf());
let call = ToolDispatch {
args: format!(r#"{{"path":"{}"}}"#, file.display()),
agent: "agent".into(),
sender: "gateway:telegram".into(),
conversation_id: None,
call_id: String::new(),
};
let result = h
.dispatch("read", call)
.expect("hook should handle read")
.await
.unwrap();
assert!(result.contains("1\ttest content"));
}