- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
use std::sync::Arc; - 4
- 5
use serde_json::json; - 6
use tempfile::tempdir; - 7
use tokio_util::sync::CancellationToken; - 8
- 9
use vak_hooks::{HookDef, HookEvent, HookOutcome, run_hooks}; - 10
- 11
fn hook(command: &str) -> Arc<Vec<HookDef>> { - 12
Arc::new(vec![HookDef { - 13
event: HookEvent::PreToolUse, - 14
matcher: None, - 15
command: command.to_string(), - 16
timeout_ms: 5000, - 17
failure_mode: vak_hooks::HookFailureMode::Open, - 18
refusal: None, - 19
}]) - 20
} - 21
- 22
#[tokio::test] - 23
async fn silent_exit_zero_is_no_opinion() { - 24
let dir = tempdir().unwrap(); - 25
let out = run_hooks( - 26
hook("exit 0"), - 27
HookEvent::PreToolUse, - 28
"s", - 29
dir.path(), - 30
Some(("bash", &json!({"command": "ls"}))), - 31
None, - 32
&CancellationToken::new(), - 33
) - 34
.await; - 35
assert_eq!(out, HookOutcome::default()); - 36
} - 37
- 38
#[tokio::test] - 39
async fn json_block_decision_blocks_with_reason() { - 40
let dir = tempdir().unwrap(); - 41
let out = run_hooks( - 42
hook(r#"echo '{"decision":"block","reason":"no rm allowed"}'"#), - 43
HookEvent::PreToolUse, - 44
"s", - 45
dir.path(), - 46
Some(("bash", &json!({"command": "rm -rf /"}))), - 47
None, - 48
&CancellationToken::new(), - 49
) - 50
.await; - 51
assert!(out.blocked); - 52
assert_eq!(out.reason.as_deref(), Some("no rm allowed")); - 53
} - 54
- 55
#[tokio::test] - 56
async fn exit_two_blocks_with_stderr_reason() { - 57
let dir = tempdir().unwrap(); - 58
let out = run_hooks( - 59
hook("echo policy-violation >&2; exit 2"), - 60
HookEvent::PreToolUse, - 61
"s", - 62
dir.path(), - 63
Some(("bash", &json!({"command": "x"}))), - 64
None, - 65
&CancellationToken::new(), - 66
) - 67
.await; - 68
assert!(out.blocked); - 69
assert_eq!(out.reason.as_deref(), Some("policy-violation")); - 70
} - 71
- 72
#[tokio::test] - 73
async fn handler_receives_event_json_on_stdin() { - 74
let dir = tempdir().unwrap(); - 75
let capture = dir.path().join("captured.json"); - 76
let cmd = format!("cat > {}", capture.display()); - 77
let out = run_hooks( - 78
hook(&cmd), - 79
HookEvent::PreToolUse, - 80
"sess-1", - 81
dir.path(), - 82
Some(("edit", &json!({"path": "a.txt"}))), - 83
None, - 84
&CancellationToken::new(), - 85
) - 86
.await; - 87
assert!(!out.blocked); - 88
let captured: serde_json::Value = - 89
serde_json::from_str(&std::fs::read_to_string(&capture).unwrap()).unwrap(); - 90
assert_eq!(captured["event"], "pre_tool_use"); - 91
assert_eq!(captured["session_id"], "sess-1"); - 92
assert_eq!(captured["tool"]["name"], "edit"); - 93
assert_eq!(captured["tool"]["input"]["path"], "a.txt"); - 94
} - 95
- 96
#[tokio::test] - 97
async fn matcher_filters_by_tool_and_args() { - 98
let dir = tempdir().unwrap(); - 99
let hooks = Arc::new(vec![HookDef { - 100
event: HookEvent::PreToolUse, - 101
matcher: Some(vak_permission::Rule::parse("Bash(git push *)").unwrap()), - 102
command: r#"echo '{"decision":"block","reason":"push blocked"}'"#.to_string(), - 103
timeout_ms: 5000, - 104
failure_mode: vak_hooks::HookFailureMode::Open, - 105
refusal: None, - 106
}]); - 107
- 108
let hit = run_hooks( - 109
hooks.clone(), - 110
HookEvent::PreToolUse, - 111
"s", - 112
dir.path(), - 113
Some(("bash", &json!({"command": "git push origin main"}))), - 114
None, - 115
&CancellationToken::new(), - 116
) - 117
.await; - 118
assert!(hit.blocked); - 119
- 120
let miss = run_hooks( - 121
hooks, - 122
HookEvent::PreToolUse, - 123
"s", - 124
dir.path(), - 125
Some(("bash", &json!({"command": "git status"}))), - 126
None, - 127
&CancellationToken::new(), - 128
) - 129
.await; - 130
assert!(!miss.blocked); - 131
} - 132
- 133
#[tokio::test] - 134
async fn timeout_kills_hook_and_reports() { - 135
let dir = tempdir().unwrap(); - 136
let hooks = Arc::new(vec![HookDef { - 137
event: HookEvent::PreToolUse, - 138
matcher: None, - 139
command: "sleep 30".to_string(), - 140
timeout_ms: 800, - 141
failure_mode: vak_hooks::HookFailureMode::Open, - 142
refusal: None, - 143
}]); - 144
let start = std::time::Instant::now(); - 145
let out = run_hooks( - 146
hooks, - 147
HookEvent::PreToolUse, - 148
"s", - 149
dir.path(), - 150
Some(("bash", &json!({}))), - 151
None, - 152
&CancellationToken::new(), - 153
) - 154
.await; - 155
assert!(!out.blocked); - 156
assert!(start.elapsed() < std::time::Duration::from_secs(10)); - 157
} - 158
- 159
#[tokio::test] - 160
async fn closed_failure_mode_blocks_on_timeout_and_nonzero_exit() { - 161
let dir = tempdir().unwrap(); - 162
let timeout = Arc::new(vec![HookDef { - 163
event: HookEvent::PreToolUse, - 164
matcher: None, - 165
command: "sleep 30".to_string(), - 166
timeout_ms: 50, - 167
failure_mode: vak_hooks::HookFailureMode::Closed, - 168
refusal: None, - 169
}]); - 170
let timed_out = run_hooks( - 171
timeout, - 172
HookEvent::PreToolUse, - 173
"s", - 174
dir.path(), - 175
Some(("bash", &json!({}))), - 176
None, - 177
&CancellationToken::new(), - 178
) - 179
.await; - 180
assert!(timed_out.blocked); - 181
- 182
let nonzero = Arc::new(vec![HookDef { - 183
event: HookEvent::PreToolUse, - 184
matcher: None, - 185
command: "exit 1".to_string(), - 186
timeout_ms: 5000, - 187
failure_mode: vak_hooks::HookFailureMode::Closed, - 188
refusal: None, - 189
}]); - 190
let failed = run_hooks( - 191
nonzero, - 192
HookEvent::PreToolUse, - 193
"s", - 194
dir.path(), - 195
Some(("bash", &json!({}))), - 196
None, - 197
&CancellationToken::new(), - 198
) - 199
.await; - 200
assert!(failed.blocked); - 201
} - 202
- 203
#[tokio::test] - 204
async fn non_matching_event_skips_handler() { - 205
let dir = tempdir().unwrap(); - 206
let out = run_hooks( - 207
hook(r#"echo '{"decision":"block"}'"#), - 208
HookEvent::Stop, - 209
"s", - 210
dir.path(), - 211
None, - 212
None, - 213
&CancellationToken::new(), - 214
) - 215
.await; - 216
assert!( - 217
!out.blocked, - 218
"hook defined for PreToolUse must not fire on Stop" - 219
); - 220
} - 221
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.