use super::{empty, Verdict};
use serde_json::{json, Value};
pub fn translate(event: &str, verdict: &Verdict<'_>) -> Value {
match event {
"pre_tool_use" => pre_tool_use(verdict),
"user_prompt_submit" => user_prompt_submit(verdict),
_ => empty(),
}
}
fn pre_tool_use(verdict: &Verdict<'_>) -> Value {
let decision = match verdict.decision {
"deny" => "deny",
"ask" => "ask",
_ => return empty(),
};
let mut specific = json!({
"hookEventName": "PreToolUse",
"permissionDecision": decision,
});
if let Some(reason) = verdict.reason {
specific["permissionDecisionReason"] = Value::String(reason.to_string());
}
json!({ "hookSpecificOutput": specific })
}
fn user_prompt_submit(verdict: &Verdict<'_>) -> Value {
if verdict.decision != "deny" {
return empty();
}
let reason = verdict.reason.unwrap_or("Blocked by OpenLatch");
json!({ "decision": "block", "reason": reason })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pre_tool_use_allow_is_empty() {
let out = translate("pre_tool_use", &Verdict::allow());
assert_eq!(out, empty());
}
#[test]
fn pre_tool_use_deny_uses_hook_specific_output() {
let v = Verdict {
decision: "deny",
reason: Some("credentials detected"),
};
let out = translate("pre_tool_use", &v);
assert_eq!(
out,
json!({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "credentials detected",
}
})
);
}
#[test]
fn pre_tool_use_ask_surfaces_without_reason() {
let v = Verdict {
decision: "ask",
reason: None,
};
let out = translate("pre_tool_use", &v);
assert_eq!(
out,
json!({
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
}
})
);
}
#[test]
fn user_prompt_submit_deny_blocks() {
let v = Verdict {
decision: "deny",
reason: Some("prompt injection"),
};
let out = translate("user_prompt_submit", &v);
assert_eq!(
out,
json!({ "decision": "block", "reason": "prompt injection" })
);
}
#[test]
fn user_prompt_submit_allow_is_empty() {
let out = translate("user_prompt_submit", &Verdict::allow());
assert_eq!(out, empty());
}
#[test]
fn stop_any_verdict_is_empty() {
for decision in ["allow", "approve", "deny"] {
let v = Verdict {
decision,
reason: Some("irrelevant"),
};
assert_eq!(translate("stop", &v), empty(), "decision={decision}");
assert_eq!(
translate("subagent_stop", &v),
empty(),
"decision={decision}"
);
}
}
#[test]
fn post_tool_use_any_verdict_is_empty() {
for decision in ["allow", "approve", "deny"] {
let v = Verdict {
decision,
reason: None,
};
assert_eq!(
translate("post_tool_use", &v),
empty(),
"decision={decision}"
);
}
}
#[test]
fn unknown_event_is_empty() {
let out = translate("some_future_event", &Verdict::allow());
assert_eq!(out, empty());
}
#[test]
fn notification_and_session_events_are_empty() {
for ev in [
"notification",
"pre_compact",
"session_start",
"session_end",
] {
assert_eq!(translate(ev, &Verdict::allow()), empty(), "event={ev}");
}
}
}