#![allow(clippy::unwrap_used)]
mod common;
use common::run_rippy;
#[test]
fn inspect_list_with_config() {
let dir = tempfile::TempDir::new().unwrap();
let config = dir.path().join("test.toml");
std::fs::write(
&config,
"[[rules]]\naction = \"deny\"\npattern = \"rm -rf *\"\nmessage = \"use trash\"\n",
)
.unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["inspect", "--config"])
.arg(&config)
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("deny"));
assert!(stdout.contains("rm -rf *"));
assert!(stdout.contains("Handlers:"));
assert!(stdout.contains("Simple safe:"));
}
#[test]
fn inspect_trace_safe_command() {
let output = std::process::Command::new(common::rippy_binary())
.args(["inspect", "cat /tmp/file"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("ALLOW"));
assert!(stdout.contains("Allowlist"));
}
#[test]
fn inspect_trace_with_config_rule() {
let dir = tempfile::TempDir::new().unwrap();
let config = dir.path().join("test.toml");
std::fs::write(
&config,
"[[rules]]\naction = \"deny\"\npattern = \"echo evil\"\nmessage = \"no evil allowed\"\n",
)
.unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["inspect", "--config"])
.arg(&config)
.arg("echo evil")
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("DENY"));
assert!(stdout.contains("no evil allowed"));
}
#[test]
fn inspect_json_output() {
let output = std::process::Command::new(common::rippy_binary())
.args(["inspect", "--json", "cat /tmp/file"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(parsed["decision"], "allow");
assert!(parsed["steps"].is_array());
}
#[test]
fn inspect_list_json_output() {
let dir = tempfile::TempDir::new().unwrap();
let config = dir.path().join("test.toml");
std::fs::write(&config, "[[rules]]\naction = \"allow\"\npattern = \"ls\"\n").unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["inspect", "--json", "--config"])
.arg(&config)
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed["config_sources"].is_array());
assert!(parsed["handler_count"].is_number());
assert!(parsed["simple_safe_count"].is_number());
}
#[test]
fn stats_json_from_populated_db() {
let dir = tempfile::TempDir::new().unwrap();
let db_path = dir.path().join("test.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"PRAGMA journal_mode=WAL;
CREATE TABLE decisions (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
session_id TEXT, mode TEXT, tool_name TEXT NOT NULL,
command TEXT, decision TEXT NOT NULL, reason TEXT, payload_json TEXT
);
INSERT INTO decisions (tool_name, command, decision, reason) VALUES ('Bash', 'git status', 'allow', 'safe');
INSERT INTO decisions (tool_name, command, decision, reason) VALUES ('Bash', 'git push', 'ask', 'review');
INSERT INTO decisions (tool_name, command, decision, reason) VALUES ('Bash', 'rm -rf /', 'deny', 'dangerous');",
)
.unwrap();
drop(conn);
let output = std::process::Command::new(common::rippy_binary())
.args(["stats", "--json", "--db"])
.arg(&db_path)
.output()
.unwrap();
assert!(output.status.success(), "stats failed: {:?}", output.status);
let stdout = String::from_utf8(output.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(parsed["counts"]["total"], 3);
assert_eq!(parsed["counts"]["allow"], 1);
assert_eq!(parsed["counts"]["ask"], 1);
assert_eq!(parsed["counts"]["deny"], 1);
}
#[test]
fn stdlib_cargo_test_allowed() {
let json = r#"{"tool_name":"Bash","tool_input":{"command":"cargo test --release"}}"#;
let (_, code) = run_rippy(json, "claude", &[]);
assert_eq!(code, 0);
}
#[test]
fn stdlib_cargo_run_asks() {
let dir = tempfile::TempDir::new().unwrap();
let json = r#"{"tool_name":"Bash","tool_input":{"command":"cargo run"}}"#;
let (_, code) = common::run_rippy_in_dir(json, "claude", dir.path());
assert_eq!(code, 2);
}
#[test]
fn stdlib_rm_asks() {
let json = r#"{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/test"}}"#;
let (_, code) = run_rippy(json, "claude", &[]);
assert_eq!(code, 2);
}
#[test]
fn stdlib_sudo_asks() {
let json = r#"{"tool_name":"Bash","tool_input":{"command":"sudo apt install foo"}}"#;
let (_, code) = run_rippy(json, "claude", &[]);
assert_eq!(code, 2);
}
#[test]
fn init_stdout_prints_stdlib() {
let output = std::process::Command::new(common::rippy_binary())
.args(["init", "--stdout"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("[[rules]]"));
assert!(stdout.contains("cargo"));
}
#[test]
fn init_creates_config_file() {
let dir = tempfile::TempDir::new().unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["init"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let content = std::fs::read_to_string(dir.path().join(".rippy.toml")).unwrap();
assert!(content.contains("[settings]"));
assert!(content.contains("package = \"develop\""));
}
#[test]
fn init_with_package_flag() {
let dir = tempfile::TempDir::new().unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["init", "--package", "review"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let content = std::fs::read_to_string(dir.path().join(".rippy.toml")).unwrap();
assert!(content.contains("package = \"review\""));
}
#[test]
fn init_stdout_still_works() {
let output = std::process::Command::new(common::rippy_binary())
.args(["init", "--stdout"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("[[rules]]"));
assert!(stdout.contains("cargo"));
}
#[test]
fn init_invalid_package_fails() {
let dir = tempfile::TempDir::new().unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["init", "--package", "bogus"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(!output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("unknown package"),
"expected 'unknown package' in output, got: {stdout}"
);
}
#[test]
fn init_refuses_existing() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join(".rippy.toml"), "existing").unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["init"])
.current_dir(dir.path())
.output()
.unwrap();
assert!(!output.status.success());
}
#[test]
fn discover_finds_curl_flags() {
let home = tempfile::TempDir::new().unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["discover", "curl", "--json"])
.env("HOME", home.path())
.output()
.unwrap();
if output.status.success() {
let stdout = String::from_utf8(output.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = parsed.as_array().unwrap();
assert!(!arr.is_empty());
}
}
#[test]
fn discover_without_args_errors() {
let output = std::process::Command::new(common::rippy_binary())
.args(["discover"])
.output()
.unwrap();
assert!(!output.status.success());
}
#[test]
fn suggest_from_session_file() {
let dir = tempfile::TempDir::new().unwrap();
let session_file = dir.path().join("test-session.jsonl");
let jsonl = [
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"git status"}}]}}"#,
r#"{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t1","content":"ok"}]}}"#,
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t2","name":"Bash","input":{"command":"git status"}}]}}"#,
r#"{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t2","content":"ok"}]}}"#,
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t3","name":"Bash","input":{"command":"git status"}}]}}"#,
r#"{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t3","content":"ok"}]}}"#,
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t4","name":"Bash","input":{"command":"rm -rf /"}}]}}"#,
r#"{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t4","is_error":true,"content":"denied"}]}}"#,
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t5","name":"Bash","input":{"command":"rm -rf /"}}]}}"#,
r#"{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t5","is_error":true,"content":"denied"}]}}"#,
r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t6","name":"Bash","input":{"command":"rm -rf /"}}]}}"#,
r#"{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t6","is_error":true,"content":"denied"}]}}"#,
];
std::fs::write(&session_file, jsonl.join("\n")).unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args([
"suggest",
"--session-file",
session_file.to_str().unwrap(),
"--json",
"--min-count",
"2",
])
.current_dir(dir.path())
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let suggestions: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let arr = suggestions.as_array().unwrap();
assert!(arr.len() >= 2);
let actions: Vec<&str> = arr.iter().filter_map(|s| s["action"].as_str()).collect();
assert!(actions.contains(&"allow"));
assert!(actions.contains(&"deny"));
}
#[test]
fn debug_shows_allow_verdict() {
let output = std::process::Command::new(common::rippy_binary())
.args(["debug", "git status"])
.output()
.unwrap();
assert_eq!(
output.status.code().unwrap_or(-1),
0,
"debug always exits 0"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("ALLOW"), "should show ALLOW, got: {stdout}");
assert!(stdout.contains("Config sources:"), "should show sources");
assert!(stdout.contains("Decision trace:"), "should show trace");
}
#[test]
fn debug_shows_deny_with_reason() {
let dir = tempfile::TempDir::new().unwrap();
let config_path = dir.path().join("test.toml");
std::fs::write(
&config_path,
"[[rules]]\naction = \"deny\"\npattern = \"rm -rf *\"\nmessage = \"use trash\"\n",
)
.unwrap();
let config_str = config_path.to_str().unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["debug", "rm -rf /tmp", "--config", config_str])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("DENY"), "should show DENY, got: {stdout}");
assert!(
stdout.contains("use trash"),
"should show reason, got: {stdout}"
);
}
#[test]
fn debug_json_output_valid() {
let output = std::process::Command::new(common::rippy_binary())
.args(["debug", "ls", "--json"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["decision"], "allow");
assert!(v["sources"].is_array());
assert!(v["steps"].is_array());
}
#[test]
fn debug_shows_config_source_override() {
let dir = tempfile::TempDir::new().unwrap();
let config_path = dir.path().join("custom.toml");
std::fs::write(&config_path, "").unwrap();
let config_str = config_path.to_str().unwrap();
let output = std::process::Command::new(common::rippy_binary())
.args(["debug", "echo hello", "--config", config_str])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("override"),
"should show override, got: {stdout}"
);
}
#[test]
fn debug_unknown_command_shows_ask() {
let output = std::process::Command::new(common::rippy_binary())
.args(["debug", "totally_unknown_command_xyz"])
.output()
.unwrap();
assert_eq!(
output.status.code().unwrap_or(-1),
0,
"debug always exits 0"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("ASK"),
"unknown cmd should show ASK, got: {stdout}"
);
}
#[test]
fn debug_shows_resolved_command_for_arithmetic() {
let output = std::process::Command::new(common::rippy_binary())
.args(["debug", "echo $((2+2))"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Resolved: echo 4"),
"should show resolved arithmetic, got: {stdout}"
);
assert!(
stdout.contains("ALLOW"),
"resolved arithmetic should allow, got: {stdout}"
);
}
#[test]
fn debug_json_includes_resolved_field() {
let output = std::process::Command::new(common::rippy_binary())
.args(["debug", "echo $'\\x41'", "--json"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(v["resolved"], "echo A");
assert_eq!(v["decision"], "allow");
}