#![expect(clippy::unwrap_used, reason = "test scaffolding")]
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
fn current_user() -> String {
std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "runner".to_string())
}
fn setup_config(content: &str) -> (TempDir, std::path::PathBuf) {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.yaml");
fs::write(&config_path, content).unwrap();
(dir, config_path)
}
fn config_no_backend() -> String {
format!(
r#"
scope:
network: []
host: []
user:
- id: test-user
match:
user: {user}
tags: [test]
tag:
test: ""
bundle:
- name: test-bundle
when: [test]
cache:
sync_interval_minutes: 60
adapter:
engine: claude-code
"#,
user = current_user(),
)
}
fn config_with_memory_addr(addr: &str, port: u16) -> String {
format!(
r#"
scope:
network: []
host: []
user:
- id: test-user
match:
user: {user}
tags: [test]
tag:
test: ""
host:
memhost:
addr: "{addr}"
features:
memory:
- server_host: memhost
port: {port}
when: [test]
cache:
sync_interval_minutes: 60
adapter:
engine: claude-code
"#,
user = current_user(),
addr = addr,
port = port,
)
}
fn hook_cmd(config_dir: &std::path::Path, config_path: &std::path::Path, event: &str) -> Command {
let mut cmd = Command::cargo_bin("llmenv").unwrap();
cmd.env("LLMENV_CONFIG", config_path)
.env("LLMENV_CONFIG_DIR", config_dir)
.env("LLMENV_STATE_DIR", config_dir)
.arg("hook-run")
.arg(event);
cmd
}
fn assert_fail_soft(mut cmd: Command, stderr_needle: &str) {
cmd.assert()
.success()
.stdout(predicate::str::is_empty())
.stderr(predicate::str::contains(stderr_needle));
}
#[test]
fn unknown_event_exits_zero_with_warning() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.yaml");
fs::write(&config_path, "adapter:\n engine: claude-code\n").unwrap();
assert_fail_soft(
hook_cmd(dir.path(), &config_path, "not_a_real_event"),
"unknown hook event",
);
}
#[test]
fn no_memory_backend_active_exits_zero_with_warning() {
let (dir, config_path) = setup_config(&config_no_backend());
assert_fail_soft(
hook_cmd(dir.path(), &config_path, "session_start"),
"no memory backend active",
);
}
#[test]
fn malformed_backend_url_exits_zero_with_warning() {
let (dir, config_path) = setup_config(&config_with_memory_addr("no-such-host.invalid", 9));
assert_fail_soft(
hook_cmd(dir.path(), &config_path, "session_start"),
"invalid memory backend URL",
);
}
#[test]
fn ssrf_rejected_loopback_url_exits_zero_with_warning() {
let (dir, config_path) = setup_config(&config_with_memory_addr("127.0.0.1", 9));
assert_fail_soft(
hook_cmd(dir.path(), &config_path, "session_start"),
"invalid memory backend URL",
);
}
#[test]
fn ssrf_rejected_private_url_exits_zero_with_warning() {
let (dir, config_path) = setup_config(&config_with_memory_addr("10.0.0.1", 8080));
assert_fail_soft(
hook_cmd(dir.path(), &config_path, "session_start"),
"invalid memory backend URL",
);
}
#[test]
fn unreachable_public_backend_exits_zero_with_warning() {
let (dir, config_path) = setup_config(&config_with_memory_addr("192.0.2.1", 9));
assert_fail_soft(
hook_cmd(dir.path(), &config_path, "session_start"),
"session_start skipped",
);
}
#[test]
fn all_events_fail_soft_without_backend() {
let (dir, config_path) = setup_config(&config_no_backend());
for event in ["session_start", "turn_start", "session_end"] {
hook_cmd(dir.path(), &config_path, event)
.assert()
.success()
.stdout(predicate::str::is_empty());
}
}