#![allow(clippy::doc_markdown)]
use std::collections::HashMap;
use std::io::Write;
use std::process::{Command, Stdio};
fn dcg_binary() -> std::path::PathBuf {
let mut path = std::env::current_exe().unwrap();
path.pop(); path.pop(); path.push("dcg");
path
}
fn run_hook_mode_with_env(command: &str, env_vars: &[(&str, &str)]) -> (String, String, i32) {
let input = format!(
r#"{{"tool_name":"Bash","tool_input":{{"command":"{}"}}}}"#,
command.replace('\\', "\\\\").replace('"', "\\\"")
);
let mut cmd = Command::new(dcg_binary());
cmd.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (key, value) in env_vars {
cmd.env(key, value);
}
let mut child = cmd.spawn().expect("failed to spawn dcg process");
{
let stdin = child.stdin.as_mut().expect("failed to get stdin");
stdin
.write_all(input.as_bytes())
.expect("failed to write to stdin");
}
let output = child.wait_with_output().expect("failed to wait for dcg");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
(stdout, stderr, exit_code)
}
fn run_robot_mode_with_env(args: &[&str], env_vars: &[(&str, &str)]) -> (String, String, i32) {
let mut cmd = Command::new(dcg_binary());
cmd.args(["--robot"])
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (key, value) in env_vars {
cmd.env(key, value);
}
let output = cmd.output().expect("failed to run dcg");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
(stdout, stderr, exit_code)
}
#[allow(dead_code)]
fn run_test_command_as_agent(command: &str, agent: &str) -> (String, String, i32) {
let agent_env_var = match agent {
"claude-code" | "claude_code" => "CLAUDE_CODE",
"aider" => "AIDER_SESSION",
"continue" => "CONTINUE_SESSION_ID",
"codex" | "codex-cli" => "CODEX_CLI",
"gemini" | "gemini-cli" => "GEMINI_CLI",
"copilot" | "copilot-cli" => "COPILOT_CLI",
_ => "DCG_AGENT_TYPE",
};
run_robot_mode_with_env(&["test", command], &[(agent_env_var, "1")])
}
mod agent_detection_tests {
use super::*;
#[test]
fn test_detects_claude_code_via_env() {
let (_stdout, stderr, exit_code) =
run_robot_mode_with_env(&["--version"], &[("CLAUDE_CODE", "1")]);
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_detects_aider_via_env() {
let (_stdout, stderr, exit_code) =
run_robot_mode_with_env(&["--version"], &[("AIDER_SESSION", "1")]);
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_detects_continue_via_env() {
let (_stdout, stderr, exit_code) = run_robot_mode_with_env(
&["--version"],
&[("CONTINUE_SESSION_ID", "test-session-123")],
);
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_detects_codex_via_env() {
let (_stdout, stderr, exit_code) =
run_robot_mode_with_env(&["--version"], &[("CODEX_CLI", "1")]);
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_detects_gemini_via_env() {
let (_stdout, stderr, exit_code) =
run_robot_mode_with_env(&["--version"], &[("GEMINI_CLI", "1")]);
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_detects_copilot_cli_via_env() {
let (_stdout, stderr, exit_code) =
run_robot_mode_with_env(&["--version"], &[("COPILOT_CLI", "1")]);
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_detects_copilot_cli_via_start_time_env() {
let (_stdout, stderr, exit_code) = run_robot_mode_with_env(
&["--version"],
&[("COPILOT_AGENT_START_TIME_SEC", "1709573241")],
);
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_unknown_agent_when_no_env_set() {
let (_stdout, stderr, exit_code) = run_robot_mode_with_env(&["--version"], &[]);
assert_eq!(
exit_code, 0,
"version command should succeed without agent env"
);
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
#[test]
fn test_explicit_agent_flag_override() {
let (_stdout, stderr, exit_code) = run_robot_mode_with_env(
&["--agent", "custom-agent", "--version"],
&[("CLAUDE_CODE", "1")], );
assert_eq!(exit_code, 0, "version command should succeed");
assert!(
stderr.contains("dcg") || !stderr.is_empty(),
"should produce output on stderr"
);
}
}
mod profile_loading_tests {
use super::*;
#[test]
fn test_loads_agent_profile_from_config() {
let (stdout, stderr, exit_code) =
run_robot_mode_with_env(&["config"], &[("CLAUDE_CODE", "1")]);
assert_eq!(
exit_code, 0,
"config command should succeed. stderr: {stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains('{') || combined.contains('[') || combined.contains("Config"),
"config output should contain structured data. stdout: {stdout}, stderr: {stderr}"
);
}
#[test]
fn test_default_profile_when_no_agent_config() {
let (stdout, stderr, exit_code) = run_robot_mode_with_env(&["config"], &[]);
assert_eq!(
exit_code, 0,
"config command should succeed without agent. stderr: {stderr}"
);
assert!(
!stdout.is_empty(),
"should produce config output. stdout: {stdout}"
);
}
}
mod trust_level_tests {
use super::*;
#[test]
fn test_destructive_command_blocked_regardless_of_agent() {
let destructive_commands = [
"git reset --hard HEAD~5",
"rm -rf /",
"git clean -fd",
"git push --force origin main",
];
let agents = [
("CLAUDE_CODE", "1"),
("AIDER_SESSION", "1"),
("CODEX_CLI", "1"),
("GEMINI_CLI", "1"),
("COPILOT_CLI", "1"),
];
for cmd in destructive_commands {
for (agent_var, agent_val) in &agents {
let (stdout, _stderr, exit_code) =
run_hook_mode_with_env(cmd, &[(agent_var, agent_val)]);
assert_eq!(
exit_code, 0,
"hook mode should exit 0 for cmd: {cmd} with agent: {agent_var}"
);
if !stdout.is_empty() {
let json: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|_| panic!("Invalid JSON for cmd '{cmd}': {stdout}"));
if let Some(hook_output) = json.get("hookSpecificOutput") {
let decision = hook_output
.get("permissionDecision")
.and_then(|v| v.as_str());
assert_eq!(
decision,
Some("deny"),
"Critical command '{cmd}' should be denied for agent {agent_var}"
);
}
}
}
}
}
#[test]
fn test_safe_command_allowed_for_all_agents() {
let safe_commands = [
"git status",
"git log --oneline",
"ls -la",
"git diff HEAD",
"git branch -a",
];
let agents = [
("CLAUDE_CODE", "1"),
("AIDER_SESSION", "1"),
("CODEX_CLI", "1"),
("GEMINI_CLI", "1"),
("COPILOT_CLI", "1"),
];
for cmd in safe_commands {
for (agent_var, agent_val) in &agents {
let (stdout, _stderr, exit_code) =
run_hook_mode_with_env(cmd, &[(agent_var, agent_val)]);
assert_eq!(exit_code, 0, "hook mode should exit 0 for safe cmd: {cmd}");
assert!(
stdout.trim().is_empty(),
"Safe command '{cmd}' should be allowed (empty output) for agent {agent_var}, got: {stdout}"
);
}
}
}
#[test]
fn test_trust_level_ordering() {
let (stdout, stderr, exit_code) = run_robot_mode_with_env(&["config"], &[]);
assert_eq!(
exit_code, 0,
"config command should succeed. stderr: {stderr}"
);
assert!(
!stdout.is_empty(),
"config should produce output. stdout: {stdout}"
);
}
}
mod agent_allowlist_tests {
use super::*;
#[test]
fn test_command_evaluation_varies_by_agent() {
let test_command = "npm run build";
let agents = [
("CLAUDE_CODE", "1"),
("AIDER_SESSION", "1"),
("CODEX_CLI", "1"),
];
let mut results: HashMap<&str, bool> = HashMap::new();
for (agent_var, agent_val) in &agents {
let (stdout, _stderr, exit_code) =
run_hook_mode_with_env(test_command, &[(agent_var, agent_val)]);
assert_eq!(exit_code, 0, "hook mode should exit 0");
let is_allowed = stdout.trim().is_empty();
results.insert(agent_var, is_allowed);
}
let all_same = results.values().all(|&v| v == results["CLAUDE_CODE"]);
assert!(
all_same,
"Without agent-specific config, results should be consistent"
);
}
}
mod unknown_agent_tests {
use super::*;
#[test]
fn test_unknown_agent_uses_safe_defaults() {
let destructive_cmd = "git reset --hard";
let (stdout, _stderr, exit_code) = run_hook_mode_with_env(destructive_cmd, &[]);
assert_eq!(exit_code, 0, "hook mode should exit 0");
if !stdout.is_empty() {
let json: serde_json::Value =
serde_json::from_str(&stdout).expect("should be valid JSON");
if let Some(hook_output) = json.get("hookSpecificOutput") {
let decision = hook_output
.get("permissionDecision")
.and_then(|v| v.as_str());
assert_eq!(
decision,
Some("deny"),
"Unknown agent should still have destructive commands blocked"
);
}
}
}
#[test]
fn test_custom_agent_name_handled() {
let (stdout, _stderr, exit_code) = run_robot_mode_with_env(
&["test", "git status"],
&[("DCG_AGENT_TYPE", "my-custom-agent")],
);
assert!(
exit_code == 0 || exit_code == 1,
"should handle custom agent name gracefully, got exit code: {exit_code}"
);
if !stdout.trim().is_empty() {
let _: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|_| panic!("Invalid JSON for custom agent: {stdout}"));
}
}
}
mod integration_tests {
use super::*;
#[test]
fn test_agent_type_in_verbose_output() {
let (stdout, stderr, exit_code) =
run_robot_mode_with_env(&["-v", "test", "git status"], &[("CLAUDE_CODE", "1")]);
assert!(
exit_code == 0 || exit_code == 1,
"test command should complete. stderr: {stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
!combined.is_empty() || exit_code == 0,
"should produce some output or succeed"
);
}
#[test]
fn test_explain_shows_evaluation_context() {
let (stdout, stderr, exit_code) =
run_robot_mode_with_env(&["explain", "git reset --hard"], &[("CLAUDE_CODE", "1")]);
assert!(
exit_code == 0 || exit_code == 1,
"explain command should complete. stderr: {stderr}"
);
assert!(
!stdout.is_empty() || !stderr.is_empty(),
"explain should produce output"
);
}
#[test]
fn test_consistent_results_across_multiple_calls() {
let test_cmd = "git status";
let agent_env = &[("CLAUDE_CODE", "1")];
let mut results: Vec<bool> = Vec::new();
for _ in 0..3 {
let (stdout, _stderr, exit_code) = run_hook_mode_with_env(test_cmd, agent_env);
assert_eq!(exit_code, 0);
results.push(stdout.trim().is_empty()); }
assert!(
results.iter().all(|&r| r == results[0]),
"Results should be consistent across multiple calls"
);
}
}
mod edge_cases {
use super::*;
#[test]
fn test_multiple_agent_env_vars_set() {
let (stdout, _stderr, exit_code) = run_hook_mode_with_env(
"git status",
&[("CLAUDE_CODE", "1"), ("AIDER_SESSION", "1")],
);
assert_eq!(exit_code, 0, "should handle multiple agent env vars");
assert!(stdout.trim().is_empty(), "safe command should be allowed");
}
#[test]
fn test_empty_agent_env_var_value() {
let (stdout, _stderr, exit_code) =
run_hook_mode_with_env("git status", &[("CLAUDE_CODE", "")]);
assert_eq!(exit_code, 0, "should handle empty agent env var");
assert!(stdout.trim().is_empty(), "safe command should be allowed");
}
#[test]
fn test_agent_env_var_with_special_characters() {
let (stdout, _stderr, exit_code) = run_hook_mode_with_env(
"git status",
&[("CLAUDE_SESSION_ID", "session-123-test_value.abc")],
);
assert_eq!(exit_code, 0, "should handle special chars in env var");
assert!(stdout.trim().is_empty(), "safe command should be allowed");
}
}
mod performance_tests {
use super::*;
use std::time::Instant;
#[test]
fn test_agent_detection_does_not_add_significant_latency() {
let iterations = 5;
let mut total_time = std::time::Duration::ZERO;
for _ in 0..iterations {
let start = Instant::now();
let (stdout, _stderr, exit_code) =
run_hook_mode_with_env("git status", &[("CLAUDE_CODE", "1")]);
let duration = start.elapsed();
assert_eq!(exit_code, 0);
assert!(stdout.trim().is_empty());
total_time += duration;
}
let avg_time = total_time / iterations as u32;
assert!(
avg_time.as_millis() < 100,
"Average hook evaluation time should be < 100ms, got: {:?}",
avg_time
);
}
}