- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
use std::path::Path; - 4
- 5
use tempfile::tempdir; - 6
- 7
use vak_tools::{ - 8
Tool, ToolContext, ToolOutput, bash::BashTool, edit::EditTool, glob::GlobTool, grep::GrepTool, - 9
read::ReadTool, write::WriteTool, - 10
}; - 11
- 12
async fn run(tool: &dyn Tool, dir: &Path, args: serde_json::Value) -> ToolOutput { - 13
let ctx = ToolContext { - 14
cwd: dir.to_path_buf(), - 15
cancel: tokio_util::sync::CancellationToken::new(), - 16
sandbox: None, - 17
sandbox_sink: None, - 18
agent_id: None, - 19
new_documents: Vec::new(), - 20
}; - 21
tool.execute(&args, &ctx).await - 22
} - 23
- 24
#[tokio::test] - 25
async fn write_then_read_roundtrip() { - 26
let dir = tempdir().unwrap(); - 27
run( - 28
&WriteTool, - 29
dir.path(), - 30
serde_json::json!({ - 31
"path": "nested/dir/file.txt", - 32
"content": "alpha\nbeta\n" - 33
}), - 34
) - 35
.await; - 36
- 37
let out = run( - 38
&ReadTool, - 39
dir.path(), - 40
serde_json::json!({"path": "nested/dir/file.txt"}), - 41
) - 42
.await; - 43
assert!(!out.is_error); - 44
assert!(out.content.contains("alpha")); - 45
assert!(out.content.contains('1')); - 46
} - 47
- 48
#[tokio::test] - 49
async fn read_reports_missing_file_as_error_value() { - 50
let dir = tempdir().unwrap(); - 51
let out = run( - 52
&ReadTool, - 53
dir.path(), - 54
serde_json::json!({"path": "nope.txt"}), - 55
) - 56
.await; - 57
assert!(out.is_error); - 58
assert!(out.content.contains("cannot read")); - 59
} - 60
- 61
#[tokio::test] - 62
async fn read_paging_hint() { - 63
let dir = tempdir().unwrap(); - 64
let content: String = (0..50).map(|i| format!("line{i}\n")).collect(); - 65
run( - 66
&WriteTool, - 67
dir.path(), - 68
serde_json::json!({"path": "big.txt", "content": content}), - 69
) - 70
.await; - 71
let out = run( - 72
&ReadTool, - 73
dir.path(), - 74
serde_json::json!({"path": "big.txt", "limit": 10}), - 75
) - 76
.await; - 77
assert!(!out.is_error); - 78
assert!(out.content.contains("Use offset=11 to continue.")); - 79
} - 80
- 81
#[tokio::test] - 82
async fn edit_is_atomic_on_duplicate_match() { - 83
let dir = tempdir().unwrap(); - 84
run( - 85
&WriteTool, - 86
dir.path(), - 87
serde_json::json!({ - 88
"path": "f.txt", - 89
"content": "x = 1\ny = 1\nz = 1\n" - 90
}), - 91
) - 92
.await; - 93
- 94
let out = run(&EditTool, dir.path(), serde_json::json!({ - 95
"path": "f.txt", - 96
"edits": [{"old_text": "y = 1", "new_text": "y = 2"}, {"old_text": "= 1", "new_text": "= 9"}] - 97
})).await; - 98
- 99
assert!( - 100
out.is_error, - 101
"second edit matches twice; whole op must fail" - 102
); - 103
assert!(out.content.contains("no changes applied")); - 104
let after = tokio::fs::read_to_string(dir.path().join("f.txt")) - 105
.await - 106
.unwrap(); - 107
assert_eq!(after, "x = 1\ny = 1\nz = 1\n", "file must be untouched"); - 108
} - 109
- 110
#[tokio::test] - 111
async fn edit_applies_sequential_edits_with_diff() { - 112
let dir = tempdir().unwrap(); - 113
run( - 114
&WriteTool, - 115
dir.path(), - 116
serde_json::json!({ - 117
"path": "f.txt", - 118
"content": "fn main() {\n println!(\"hello\");\n}\n" - 119
}), - 120
) - 121
.await; - 122
- 123
let out = run( - 124
&EditTool, - 125
dir.path(), - 126
serde_json::json!({ - 127
"path": "f.txt", - 128
"edits": [ - 129
{"old_text": "\"hello\"", "new_text": "\"goodbye\""}, - 130
{"old_text": "fn main()", "new_text": "fn main() -> ()"} - 131
] - 132
}), - 133
) - 134
.await; - 135
- 136
assert!(!out.is_error); - 137
let after = tokio::fs::read_to_string(dir.path().join("f.txt")) - 138
.await - 139
.unwrap(); - 140
assert_eq!(after, "fn main() -> () {\n println!(\"goodbye\");\n}\n"); - 141
assert!( - 142
out.content.contains("-fn main() {"), - 143
"diff should show removed line" - 144
); - 145
} - 146
- 147
#[tokio::test] - 148
async fn edit_preserves_crlf_line_endings() { - 149
let dir = tempdir().unwrap(); - 150
tokio::fs::write(dir.path().join("crlf.txt"), "a = 1\r\nb = 2\r\n") - 151
.await - 152
.unwrap(); - 153
- 154
let out = run( - 155
&EditTool, - 156
dir.path(), - 157
serde_json::json!({ - 158
"path": "crlf.txt", - 159
"edits": [{"old_text": "a = 1", "new_text": "a = 10"}] - 160
}), - 161
) - 162
.await; - 163
- 164
assert!(!out.is_error); - 165
let after = tokio::fs::read_to_string(dir.path().join("crlf.txt")) - 166
.await - 167
.unwrap(); - 168
assert_eq!(after, "a = 10\r\nb = 2\r\n"); - 169
} - 170
- 171
#[tokio::test] - 172
async fn bash_runs_and_captures() { - 173
let dir = tempdir().unwrap(); - 174
let out = run( - 175
&BashTool, - 176
dir.path(), - 177
serde_json::json!({"command": "echo hi && echo err >&2"}), - 178
) - 179
.await; - 180
assert!(!out.is_error); - 181
assert!(out.content.contains("[stdout]\nhi")); - 182
assert!(out.content.contains("[stderr]\nerr")); - 183
} - 184
- 185
#[tokio::test] - 186
async fn bash_nonzero_exit_is_error_value() { - 187
let dir = tempdir().unwrap(); - 188
let out = run( - 189
&BashTool, - 190
dir.path(), - 191
serde_json::json!({"command": "exit 3"}), - 192
) - 193
.await; - 194
assert!(out.is_error); - 195
assert!(out.content.contains("exit code: 3")); - 196
} - 197
- 198
#[tokio::test] - 199
async fn bash_timeout_kills_process() { - 200
let dir = tempdir().unwrap(); - 201
let start = std::time::Instant::now(); - 202
let out = run( - 203
&BashTool, - 204
dir.path(), - 205
serde_json::json!({ - 206
"command": "sleep 30", - 207
"timeout_ms": 1500 - 208
}), - 209
) - 210
.await; - 211
assert!(out.is_error); - 212
assert!(start.elapsed() < std::time::Duration::from_secs(5)); - 213
assert!(out.content.contains("timed out")); - 214
} - 215
- 216
#[tokio::test] - 217
async fn glob_finds_nested_files() { - 218
let dir = tempdir().unwrap(); - 219
for p in ["src/a.rs", "src/deep/b.rs", "docs/c.md"] { - 220
run( - 221
&WriteTool, - 222
dir.path(), - 223
serde_json::json!({"path": p, "content": "x"}), - 224
) - 225
.await; - 226
} - 227
let out = run( - 228
&GlobTool, - 229
dir.path(), - 230
serde_json::json!({"pattern": "src/**/*.rs"}), - 231
) - 232
.await; - 233
assert!(!out.is_error); - 234
assert!(out.content.contains("src/a.rs")); - 235
assert!(out.content.contains("src/deep/b.rs")); - 236
assert!(!out.content.contains("docs/c.md")); - 237
} - 238
- 239
/// `.vak/agents/<id>/workspace` is another Agent's isolated project - 240
/// workspace (see `vak_config::paths::agent_workspace`), nested inside the - 241
/// shared base workspace on disk. Glob/grep must never wander into it — the - 242
/// whole point of an isolated workspace is defeated if a different Agent's - 243
/// tool calls can read its files. - 244
#[tokio::test] - 245
async fn glob_does_not_walk_into_another_agents_isolated_workspace() { - 246
let dir = tempdir().unwrap(); - 247
for p in ["notes.md", ".vak/agents/other-agent/workspace/secret.md"] { - 248
run( - 249
&WriteTool, - 250
dir.path(), - 251
serde_json::json!({"path": p, "content": "x"}), - 252
) - 253
.await; - 254
} - 255
let out = run( - 256
&GlobTool, - 257
dir.path(), - 258
serde_json::json!({"pattern": "**/*.md"}), - 259
) - 260
.await; - 261
assert!(!out.is_error); - 262
assert!(out.content.contains("notes.md")); - 263
assert!(!out.content.contains("secret.md")); - 264
} - 265
- 266
#[tokio::test] - 267
async fn grep_does_not_walk_into_another_agents_isolated_workspace() { - 268
let dir = tempdir().unwrap(); - 269
run( - 270
&WriteTool, - 271
dir.path(), - 272
serde_json::json!({"path": "notes.md", "content": "the answer is 42\n"}), - 273
) - 274
.await; - 275
run( - 276
&WriteTool, - 277
dir.path(), - 278
serde_json::json!({ - 279
"path": ".vak/agents/other-agent/workspace/secret.md", - 280
"content": "the answer is 42\n" - 281
}), - 282
) - 283
.await; - 284
let out = run(&GrepTool, dir.path(), serde_json::json!({"pattern": "42"})).await; - 285
assert!(!out.is_error); - 286
assert!(out.content.contains("notes.md")); - 287
assert!(!out.content.contains("secret.md")); - 288
} - 289
- 290
#[tokio::test] - 291
async fn grep_matches_with_include_filter() { - 292
let dir = tempdir().unwrap(); - 293
run( - 294
&WriteTool, - 295
dir.path(), - 296
serde_json::json!({"path": "code.rs", "content": "let answer = 42;\n"}), - 297
) - 298
.await; - 299
run( - 300
&WriteTool, - 301
dir.path(), - 302
serde_json::json!({"path": "note.md", "content": "the answer is 42\n"}), - 303
) - 304
.await; - 305
- 306
let out = run( - 307
&GrepTool, - 308
dir.path(), - 309
serde_json::json!({ - 310
"pattern": "42", - 311
"include": "*.rs" - 312
}), - 313
) - 314
.await; - 315
assert!(!out.is_error); - 316
assert!(out.content.contains("code.rs:1")); - 317
assert!(!out.content.contains("note.md")); - 318
} - 319
- 320
#[tokio::test] - 321
async fn a_tool_returns_its_whole_output() { - 322
let dir = tempdir().unwrap(); - 323
let long: String = (0..40_000).map(|i| format!("L{i:05}\n")).collect(); - 324
std::fs::write(dir.path().join("long.txt"), &long).unwrap(); - 325
let out = run( - 326
&BashTool, - 327
dir.path(), - 328
serde_json::json!({"command": "cat long.txt"}), - 329
) - 330
.await; - 331
assert!(!out.is_error, "{}", out.content); - 332
assert!(out.content.contains("L00000") && out.content.contains("L39999")); - 333
assert!(out.content.chars().count() > long.chars().count()); - 334
} - 335
- 336
#[tokio::test] - 337
async fn edit_rejects_non_utf8_instead_of_corrupting() { - 338
let dir = tempdir().unwrap(); - 339
let bytes: &[u8] = &[0x68, 0x69, 0xFF, 0xFE, 0x00, 0x01]; - 340
std::fs::write(dir.path().join("blob.bin"), bytes).unwrap(); - 341
let out = run( - 342
&EditTool, - 343
dir.path(), - 344
serde_json::json!({"path": "blob.bin", "edits": [{"old_text": "hi", "new_text": "ho"}]}), - 345
) - 346
.await; - 347
assert!(out.is_error, "non-UTF-8 edit must be refused"); - 348
assert_eq!( - 349
std::fs::read(dir.path().join("blob.bin")).unwrap(), - 350
bytes, - 351
"file must be untouched" - 352
); - 353
} - 354
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.