use aa_core::identity::{AgentId, SessionId};
use aa_core::{AuditEntry, AuditEventType};
use aa_gateway::audit::{AuditWriter, VerifyResult};
use tokio::sync::mpsc;
const AGENT: AgentId = AgentId::from_bytes([1u8; 16]);
const SESSION: SessionId = SessionId::from_bytes([2u8; 16]);
fn make_entry(seq: u64, previous_hash: [u8; 32]) -> AuditEntry {
AuditEntry::new(
seq,
1_700_000_000_000_000_000 + seq,
AuditEventType::ToolCallIntercepted,
AGENT,
SESSION,
format!("{{\"seq\":{seq}}}"),
previous_hash,
)
}
fn make_chain(count: u64) -> Vec<AuditEntry> {
let mut entries = Vec::new();
let mut prev_hash = [0u8; 32];
for seq in 0..count {
let entry = make_entry(seq, prev_hash);
prev_hash = *entry.entry_hash();
entries.push(entry);
}
entries
}
#[tokio::test]
async fn append_writes_valid_jsonl() {
let dir = tempfile::tempdir().unwrap();
let (tx, rx) = mpsc::channel(64);
let writer = AuditWriter::new(dir.path().to_path_buf(), "agent-1", "sess-1", rx)
.await
.unwrap();
tokio::spawn(writer.run());
let entries = make_chain(3);
for entry in &entries {
tx.send(entry.clone()).await.unwrap();
}
drop(tx);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let path = dir.path().join("agent-1-sess-1.jsonl");
assert!(path.exists(), "JSONL file should be created");
let content = tokio::fs::read_to_string(&path).await.unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3, "should have 3 JSON lines");
for (i, line) in lines.iter().enumerate() {
let entry: AuditEntry = serde_json::from_str(line).unwrap_or_else(|e| panic!("line {i} failed to parse: {e}"));
assert_eq!(entry.seq(), i as u64);
}
}
#[tokio::test]
async fn verify_chain_valid() {
let dir = tempfile::tempdir().unwrap();
let (tx, rx) = mpsc::channel(64);
let writer = AuditWriter::new(dir.path().to_path_buf(), "agent-v", "sess-v", rx)
.await
.unwrap();
tokio::spawn(writer.run());
let entries = make_chain(5);
for entry in &entries {
tx.send(entry.clone()).await.unwrap();
}
drop(tx);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let path = dir.path().join("agent-v-sess-v.jsonl");
let result = AuditWriter::verify_chain(&path).await.unwrap();
assert_eq!(
result,
VerifyResult {
is_valid: true,
entries_checked: 5,
first_invalid: None,
}
);
}
#[tokio::test]
async fn verify_chain_detects_tampering() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("tampered.jsonl");
let entries = make_chain(3);
let mut lines: Vec<String> = entries.iter().map(|e| serde_json::to_string(e).unwrap()).collect();
let _original: AuditEntry = serde_json::from_str(&lines[1]).unwrap();
let bad_entry = AuditEntry::new(
1,
entries[1].timestamp_ns(),
entries[1].event_type(),
entries[1].agent_id(),
entries[1].session_id(),
"TAMPERED".to_string(),
*entries[1].previous_hash(),
);
lines[1] = serde_json::to_string(&bad_entry).unwrap();
let content = lines.join("\n") + "\n";
tokio::fs::write(&path, content).await.unwrap();
let result = AuditWriter::verify_chain(&path).await.unwrap();
assert!(!result.is_valid);
assert_eq!(result.first_invalid, Some(2));
}
#[tokio::test]
async fn read_last_hash_returns_correct_hash() {
let dir = tempfile::tempdir().unwrap();
let (tx, rx) = mpsc::channel(64);
let writer = AuditWriter::new(dir.path().to_path_buf(), "agent-h", "sess-h", rx)
.await
.unwrap();
tokio::spawn(writer.run());
let entries = make_chain(3);
let expected_hash = *entries.last().unwrap().entry_hash();
for entry in &entries {
tx.send(entry.clone()).await.unwrap();
}
drop(tx);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let path = dir.path().join("agent-h-sess-h.jsonl");
let hash = AuditWriter::read_last_hash(&path).await.unwrap();
assert_eq!(hash, Some(expected_hash));
}
#[tokio::test]
async fn read_last_hash_returns_none_for_missing_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nonexistent.jsonl");
let hash = AuditWriter::read_last_hash(&path).await.unwrap();
assert_eq!(hash, None);
}