use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use serde_json::{Value, json};
struct Env {
_dir: tempfile::TempDir,
project: std::path::PathBuf,
db: std::path::PathBuf,
run: std::path::PathBuf,
}
fn scryer(env: &Env) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_scryer"));
cmd.current_dir(&env.project)
.env("SCRYER_DB_URL", &env.db)
.env("SCRYER_NO_DAEMON", "1")
.env("XDG_RUNTIME_DIR", &env.run)
.env_remove("SCRYER_HOOK");
cmd
}
fn setup() -> Env {
let dir = tempfile::tempdir().unwrap();
let project = dunce::canonicalize(dir.path()).unwrap().join("proj");
std::fs::create_dir_all(project.join("src")).unwrap();
std::fs::write(
project.join("Cargo.toml"),
"[package]\nname = \"proj\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(
project.join("src/lib.rs"),
"pub fn make_widget() -> u32 {\n 1\n}\n\npub fn use_it() -> u32 {\n make_widget()\n}\n",
)
.unwrap();
let env = Env {
db: dir.path().join("db/scryer.db"),
run: dir.path().join("run"),
project,
_dir: dir,
};
let out = scryer(&env).arg("index").output().unwrap();
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
env
}
fn hook(env: &Env, mode: Option<&str>, event: &Value) -> String {
let mut cmd = scryer(env);
cmd.args(["hook", "pre-grep"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(mode) = mode {
cmd.env("SCRYER_HOOK", mode);
}
let mut child = cmd.spawn().unwrap();
child
.stdin
.take()
.unwrap()
.write_all(event.to_string().as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"the hook must never fail: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
fn grep(project: &Path, session: &str, pattern: &str) -> Value {
json!({
"session_id": session,
"cwd": project,
"tool_name": "Grep",
"tool_input": { "pattern": pattern },
})
}
#[test]
fn it_is_off_unless_asked() {
let env = setup();
let event = grep(&env.project, "off-by-default", "make_widget");
assert_eq!(hook(&env, None, &event), "");
assert_eq!(hook(&env, Some("off"), &event), "");
assert_eq!(hook(&env, Some("garbage"), &event), "");
}
#[test]
fn a_bare_identifier_is_answered_once_then_the_repeat_runs() {
let env = setup();
let event = grep(&env.project, "once", "make_widget");
let first = hook(&env, Some("on"), &event);
let out: Value = serde_json::from_str(&first).unwrap_or_else(|e| panic!("{e}: {first}"));
let specific = &out["hookSpecificOutput"];
assert_eq!(specific["permissionDecision"], "deny");
let reason = specific["permissionDecisionReason"].as_str().unwrap();
assert!(
reason.contains("use_it"),
"the caller is in the answer: {reason}"
);
assert!(reason.contains("src/lib.rs:6"), "{reason}");
assert!(reason.contains("repeat the same search"), "{reason}");
assert_eq!(hook(&env, Some("on"), &event), "");
let other = grep(&env.project, "another", "make_widget");
assert!(hook(&env, Some("on"), &other).contains("deny"));
}
#[test]
fn the_same_search_as_a_bash_command_is_answered_too() {
let env = setup();
let event = json!({
"session_id": "bash",
"cwd": env.project,
"tool_name": "Bash",
"tool_input": { "command": "grep -rn make_widget ." },
});
assert!(hook(&env, Some("on"), &event).contains("use_it"));
}
#[test]
fn what_is_not_a_single_known_identifier_runs_as_usual() {
let env = setup();
for (session, pattern) in [
("regex", "make_.*widget"),
("short", "use"),
("unknown", "does_not_exist_anywhere"),
("literal", "\"make_widget\""),
] {
let event = grep(&env.project, session, pattern);
assert_eq!(hook(&env, Some("on"), &event), "", "{pattern}");
}
}
#[test]
fn context_mode_allows_the_grep_and_adds_the_answer() {
let env = setup();
let out = hook(
&env,
Some("context"),
&grep(&env.project, "ctx", "make_widget"),
);
let out: Value = serde_json::from_str(&out).unwrap();
let specific = &out["hookSpecificOutput"];
assert!(specific.get("permissionDecision").is_none());
assert!(
specific["additionalContext"]
.as_str()
.unwrap()
.contains("use_it")
);
}
#[test]
fn garbage_on_stdin_never_fails_the_agents_call() {
let env = setup();
let mut cmd = scryer(&env);
cmd.args(["hook", "pre-grep"])
.env("SCRYER_HOOK", "on")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().unwrap();
child
.stdin
.take()
.unwrap()
.write_all(b"not json at all")
.unwrap();
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
assert!(out.stdout.is_empty());
}