#![cfg(unix)]
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::thread;
use kintsugi_core::{Class, Decision, EventLog, ProposedCommand};
use kintsugi_daemon::{Client, Daemon, Server};
fn serial_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}
struct Harness {
_guard: MutexGuard<'static, ()>,
_tmp: tempfile::TempDir,
db: std::path::PathBuf,
server: Option<thread::JoinHandle<()>>,
}
fn start(requests: usize) -> Harness {
let guard = serial_lock();
let tmp = tempfile::tempdir().unwrap();
let sock = tmp.path().join("kintsugi.sock");
let db = tmp.path().join("events.db");
std::env::set_var("KINTSUGI_SOCKET", &sock);
std::env::set_var("KINTSUGI_DB", &db);
let db_for_thread = db.clone();
let server = Server::bind().unwrap();
let handle = thread::spawn(move || {
let daemon = Daemon::open(&db_for_thread).unwrap();
server
.serve_n(requests, |req| daemon.handle_request(req))
.unwrap();
});
Harness {
_guard: guard,
_tmp: tmp,
db,
server: Some(handle),
}
}
impl Harness {
fn join(&mut self) {
if let Some(h) = self.server.take() {
h.join().unwrap();
}
}
}
#[test]
fn recorded_safe_command_is_logged_as_allow() {
let mut h = start(1);
let cmd = ProposedCommand::new(
"shell",
"/tmp/db",
vec!["ls".into(), "-la".into()],
"ls -la",
);
Client::record(&cmd).unwrap();
h.join();
let log = EventLog::open(&h.db).unwrap();
let tail = log.tail(10).unwrap();
assert_eq!(tail.len(), 1);
assert_eq!(tail[0].agent, "shell");
assert_eq!(tail[0].decision, Decision::Allow);
assert_eq!(tail[0].class, Class::Safe);
assert!(log.verify_chain().unwrap().is_intact());
}
#[test]
fn recorded_destructive_command_is_allowed_but_flagged_catastrophic() {
let mut h = start(1);
let cmd = ProposedCommand::new(
"shell",
"/srv",
vec!["rm".into(), "-rf".into(), "/srv/data".into()],
"rm -rf /srv/data",
);
Client::record(&cmd).unwrap();
h.join();
let log = EventLog::open(&h.db).unwrap();
let tail = log.tail(1).unwrap();
assert_eq!(tail[0].decision, Decision::Allow, "it already ran");
assert_eq!(tail[0].class, Class::Catastrophic, "but flagged for audit");
assert!(tail[0].reason.starts_with("recorded:"));
}
#[test]
fn recorded_secret_command_is_redacted_in_the_log() {
let mut h = start(1);
let cmd = ProposedCommand::new(
"shell",
"/srv",
vec!["mysql".into(), "-ps3cr3tPa55".into()],
"mysql -ps3cr3tPa55 -u root",
);
Client::record(&cmd).unwrap();
h.join();
let log = EventLog::open(&h.db).unwrap();
let tail = log.tail(1).unwrap();
assert!(
!tail[0].command.contains("s3cr3tPa55"),
"secret must be redacted, got: {}",
tail[0].command
);
assert!(tail[0].command.contains("[redacted]"));
assert!(log.verify_chain().unwrap().is_intact());
}
#[test]
fn record_cannot_forge_an_agent_label() {
let mut h = start(1);
let forged = ProposedCommand::new("claude-code", "/tmp", vec!["ls".into()], "ls");
Client::record(&forged).unwrap();
h.join();
let log = EventLog::open(&h.db).unwrap();
assert_eq!(log.tail(1).unwrap()[0].agent, "shell");
}
#[test]
fn recoverer_snapshots_a_destructive_human_command_for_undo() {
let mut h = start(1);
let victim = h.db.with_file_name("data.txt");
std::fs::write(&victim, b"original").unwrap();
let raw = format!("rm -rf {}", victim.display());
let cmd = ProposedCommand::new(
"shell",
h.db.parent().unwrap(),
vec!["rm".into(), "-rf".into(), victim.display().to_string()],
raw,
);
Client::record(&cmd).unwrap();
h.join();
let log = EventLog::open(&h.db).unwrap();
let tail = log.tail(1).unwrap();
assert_eq!(tail[0].class, Class::Catastrophic);
assert!(
tail[0].snapshot_id.is_some(),
"a destructive human command must be snapshotted for undo"
);
std::fs::remove_file(&victim).unwrap();
assert!(!victim.exists());
let snapshot_dir = h.db.with_file_name("snapshots");
let manifest = log
.latest_unreverted_snapshot()
.unwrap()
.expect("a snapshot to undo");
kintsugi_core::restore_snapshot(&snapshot_dir, &manifest).unwrap();
assert_eq!(
std::fs::read(&victim).unwrap(),
b"original",
"undo recovered the human's deleted file"
);
}
#[test]
fn recorded_and_proposed_events_share_one_intact_chain() {
let mut h = start(2);
let proposed = ProposedCommand::new("claude-code", "/tmp/p", vec!["ls".into()], "ls");
assert_eq!(Client::send(&proposed).unwrap().decision, Decision::Allow);
let recorded = ProposedCommand::new("shell", "/tmp/p", vec!["whoami".into()], "whoami");
Client::record(&recorded).unwrap();
h.join();
let log = EventLog::open(&h.db).unwrap();
assert_eq!(log.count().unwrap(), 2);
assert!(log.verify_chain().unwrap().is_intact());
}