use agentignore::fs::{CascadingAllowList, Policy};
use std::ffi::OsStr;
use std::fs;
use std::path::Path;
mod common;
fn proc_comm() -> String {
std::fs::read_to_string("/proc/self/comm")
.unwrap()
.trim()
.to_string()
}
fn current_pid() -> u32 {
std::process::id()
}
fn make_agentallow_in_dir(dir: &Path, content: &str) {
fs::write(dir.join(".agentallow"), content).unwrap();
}
#[test]
fn cascading_agentallow_empty_root_but_subdir_has_rules() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::mkdirp(&root.join("subdir"));
make_agentallow_in_dir(&root.join("subdir"), &format!("{comm}\n"));
common::touch(&root.join("root_file.txt"));
common::touch(&root.join("subdir").join("sub_file.txt"));
let allow_list = CascadingAllowList::load(&root);
assert!(!allow_list.is_allowed(&root.join("root_file.txt"), pid));
assert!(allow_list.is_allowed(&root.join("subdir").join("sub_file.txt"), pid));
assert!(allow_list.has_any_entries());
let empty_root = CascadingAllowList::load(&root);
assert!(empty_root.has_any_entries());
}
#[test]
fn cascading_agentallow_gid_across_levels() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::mkdirp(&root.join("services").join("db"));
common::make_agentallow(&root, "npm\n");
make_agentallow_in_dir(&root.join("services"), &format!("{comm}\n"));
make_agentallow_in_dir(&root.join("services").join("db"), "postgres!\n");
common::touch(&root.join("services").join("db").join("data.sql"));
let allow_list = CascadingAllowList::load(&root);
assert!(allow_list.is_allowed(&root.join("services").join("db").join("data.sql"), pid,));
assert!(allow_list.has_any_entries());
assert!(!allow_list.is_allowed(&root.join("services").join("db").join("data.sql"), 99999));
}
#[test]
fn cascading_agentallow_hot_reload_detects_new_subdir_allow() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::make_agentallow(&root, &format!("{comm}\n"));
common::mkdirp(&root.join("dynamic"));
let mut policy = Policy::load(&root);
common::touch(&root.join("dynamic").join("file.txt"));
assert!(policy.is_allowed_raw(&root.join("dynamic").join("file.txt"), pid));
std::thread::sleep(std::time::Duration::from_millis(15));
make_agentallow_in_dir(&root.join("dynamic"), "specific_tool\n");
assert!(policy.check_and_reload());
assert!(policy.is_allowed_raw(&root.join("dynamic").join("file.txt"), pid));
let allow_list = CascadingAllowList::load(&root);
assert!(allow_list.has_any_entries());
assert!(allow_list.is_allowed(&root.join("dynamic").join("file.txt"), pid));
}
#[test]
fn cascading_agentallow_hot_reload_detects_removed_subdir_allow() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::mkdirp(&root.join("removable"));
make_agentallow_in_dir(&root.join("removable"), &format!("{comm}\n"));
let mut policy = Policy::load(&root);
common::touch(&root.join("removable").join("file.txt"));
assert!(policy.is_allowed_raw(&root.join("removable").join("file.txt"), pid));
std::thread::sleep(std::time::Duration::from_millis(15));
fs::remove_file(root.join("removable").join(".agentallow")).unwrap();
assert!(policy.check_and_reload());
assert!(!policy.is_allowed_raw(&root.join("removable").join("file.txt"), pid));
}
#[test]
fn cascading_agentallow_multiple_rules_in_chain() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::mkdirp(&root.join("a").join("b").join("c"));
common::make_agentallow(&root, &format!("{comm}\n")); make_agentallow_in_dir(&root.join("a"), &format!("{comm}\nnpm\n")); make_agentallow_in_dir(&root.join("a").join("b"), "docker\n"); make_agentallow_in_dir(&root.join("a").join("b").join("c"), "git\n");
common::touch(&root.join("a").join("b").join("c").join("file.txt"));
let allow_list = CascadingAllowList::load(&root);
assert!(allow_list.is_allowed(&root.join("a").join("b").join("c").join("file.txt"), pid,));
assert!(!allow_list.is_allowed(&root.join("a").join("b").join("c").join("file.txt"), 99999,));
}
#[test]
fn cascading_agentallow_root_and_subdir_combined_with_agentignore_cascade() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::make_agentignore(&root, "*.log\n");
common::make_agentallow(&root, &format!("{comm}\n"));
common::mkdirp(&root.join("subdir"));
fs::write(root.join("subdir").join(".agentignore"), "*.tmp\n").unwrap();
make_agentallow_in_dir(&root.join("subdir"), "another_daemon\n");
common::touch(&root.join("app.log"));
common::touch(&root.join("subdir").join("server.log"));
common::touch(&root.join("subdir").join("cache.tmp"));
let policy = Policy::load(&root);
assert!(policy.is_hidden(&root.join("app.log")));
assert!(policy.is_allowed_raw(&root.join("app.log"), pid));
assert!(policy.is_hidden(&root.join("subdir").join("server.log")));
assert!(policy.is_hidden(&root.join("subdir").join("cache.tmp")));
assert!(policy.is_allowed_raw(&root.join("subdir").join("server.log"), pid));
assert!(policy.is_allowed_raw(&root.join("subdir").join("cache.tmp"), pid));
assert!(!policy.is_allowed_raw(&root.join("app.log"), 99999));
assert!(!policy.is_allowed_raw(&root.join("subdir").join("server.log"), 99999));
}
#[test]
fn cascading_agentallow_sibling_directories_have_different_rules() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::mkdirp(&root.join("frontend"));
common::mkdirp(&root.join("backend"));
make_agentallow_in_dir(&root.join("frontend"), &format!("{comm}\n"));
make_agentallow_in_dir(&root.join("backend"), "backend_daemon\n");
common::touch(&root.join("frontend").join("app.js"));
common::touch(&root.join("backend").join("server.py"));
let allow_list = CascadingAllowList::load(&root);
assert!(allow_list.is_allowed(&root.join("frontend").join("app.js"), pid));
assert!(!allow_list.is_allowed(&root.join("backend").join("server.py"), pid));
assert!(!allow_list.is_allowed(&root.join("frontend").join("app.js"), 99999));
assert!(!allow_list.is_allowed(&root.join("backend").join("server.py"), 99999));
}
#[test]
fn cascading_agentallow_subdir_adds_specific_rules() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::make_agentallow(&root, &format!("{comm}\n"));
common::mkdirp(&root.join("subdir"));
make_agentallow_in_dir(&root.join("subdir"), "subdir_tool\n");
common::touch(&root.join("subdir").join("file.txt"));
let allow_list = CascadingAllowList::load(&root);
assert!(allow_list.is_allowed(&root.join("subdir").join("file.txt"), pid));
assert!(allow_list.has_any_entries());
}
#[test]
fn cascading_agentallow_subdir_inherits_root_allows() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::make_agentallow(&root, &format!("{comm}\n"));
common::mkdirp(&root.join("subdir"));
common::touch(&root.join("subdir").join("file.txt"));
let allow_list = CascadingAllowList::load(&root);
assert!(allow_list.is_allowed(&root.join("file.txt"), pid));
assert!(allow_list.is_allowed(&root.join("subdir").join("file.txt"), pid));
assert!(!allow_list.is_allowed(&root.join("subdir").join("file.txt"), 99999));
}
#[test]
fn cascading_agentallow_with_agentfs_integration_lookup() {
let (_dir, root) = common::test_dir();
let pid = current_pid();
let comm = proc_comm();
common::make_agentignore(&root, "*.log\n");
common::make_agentallow(&root, &format!("{comm}\n"));
common::mkdirp(&root.join("project"));
fs::write(root.join("project").join(".agentignore"), "*.secret\n").unwrap();
make_agentallow_in_dir(&root.join("project"), "project_specific\n");
common::touch(&root.join("app.log"));
common::touch(&root.join("project").join("server.log"));
common::touch(&root.join("project").join("key.secret"));
common::touch(&root.join("project").join("readme.md"));
let fs = agentignore::fs::AgentFS::new(root.clone());
assert!(
fs.lookup_child(fuser::INodeNo::ROOT, OsStr::new("app.log"), None)
.is_none()
);
assert!(
fs.lookup_child(fuser::INodeNo::ROOT, OsStr::new("project"), None)
.is_some()
);
let (_project_path, project_ino) = fs
.lookup_child(fuser::INodeNo::ROOT, OsStr::new("project"), None)
.unwrap();
assert!(
fs.lookup_child(fuser::INodeNo(project_ino), OsStr::new("server.log"), None)
.is_none()
);
assert!(
fs.lookup_child(fuser::INodeNo(project_ino), OsStr::new("key.secret"), None)
.is_none()
);
assert!(
fs.lookup_child(fuser::INodeNo(project_ino), OsStr::new("readme.md"), None)
.is_some()
);
let policy = fs.policy_read();
assert!(policy.is_allowed_raw(&root.join("app.log"), pid));
assert!(policy.is_allowed_raw(&root.join("project").join("server.log"), pid));
assert!(policy.is_allowed_raw(&root.join("project").join("key.secret"), pid));
assert!(!policy.is_allowed_raw(&root.join("project").join("readme.md"), 99999));
}