//! Tests for audit and session log records against JSONL fixtures. Do not edit these or the fixtures. use proto::{ AuditRecord, CallId, DataClass, DecisionRecord, DenyReason, Epoch, Hash32, LogRecord, SessionId, Timestamp, ToolCall, }; use serde::{Serialize, de::DeserializeOwned}; use std::fmt::Debug; const SEQ_HEX: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; const REV_HEX: &str = "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100"; fn ts(s: &str) -> Timestamp { Timestamp::parse(s).unwrap() } /// Line `i` of the fixture must decode to `want[i]`, and `want[i]` must encode to exactly that line. fn check(name: &str, want: &[T]) { let path = format!( "{}/tests/fixtures/records/{name}", env!("CARGO_MANIFEST_DIR") ); let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}")); let lines: Vec<&str> = text.lines().collect(); assert_eq!(lines.len(), want.len(), "{name}: number of lines"); for (i, (line, want)) in lines.iter().zip(want).enumerate() { let got: T = serde_json::from_str(line).unwrap_or_else(|e| panic!("{name}:{}: {e}", i + 1)); assert_eq!(&got, want, "{name}:{}: decoded value", i + 1); assert_eq!( &serde_json::to_string(want).unwrap(), line, "{name}:{}: encoded bytes", i + 1 ); } } /// A record for session `session`; each test case overrides the fields it cares about. fn audit(seq: u64, time: &str, prev: Hash32, session: &str) -> AuditRecord { AuditRecord { seq, time: ts(time), prev, session: SessionId::new(session).unwrap(), call: CallId(1), tool: String::new(), arguments: "{}".to_string(), session_taint: DataClass::Private, decision: DecisionRecord::Denied { reason: DenyReason::NoGrant, grant: None, }, } } #[test] fn audit_records() { let seq_hash = Hash32::from_hex(SEQ_HEX).unwrap(); let rev_hash = Hash32::from_hex(REV_HEX).unwrap(); let want = [ AuditRecord { tool: "read_file".to_string(), arguments: r#"{"path":"/etc/hosts"}"#.to_string(), decision: DecisionRecord::Allowed { grant: "read-etc".to_string(), }, ..audit(0, "2026-09-17T08:05:00.000Z", Hash32::ZERO, "mm-thread-42") }, AuditRecord { call: CallId(2), tool: "shell".to_string(), arguments: r#"{"command":"df -h"}"#.to_string(), decision: DecisionRecord::Approved { grant: "shell-ask".to_string(), approver: "u8f3k2".to_string(), post: Some("p9x7".to_string()), }, ..audit(1, "2026-09-17T08:05:01.250Z", seq_hash, "mm-thread-42") }, AuditRecord { tool: "consult".to_string(), arguments: r#"{"question":"hi"}"#.to_string(), session_taint: DataClass::Secret, decision: DecisionRecord::Denied { reason: DenyReason::TaintTooHigh, grant: Some("consult-private".to_string()), }, ..audit(2, "2026-09-17T08:05:02.000Z", rev_hash, "cron-morning") }, AuditRecord { call: CallId(2), tool: "rm_rf".to_string(), session_taint: DataClass::Secret, ..audit(3, "2026-09-17T08:05:03.000Z", rev_hash, "cron-morning") }, ]; check("audit.jsonl", &want); } #[test] fn session_log_records() { let want = [ LogRecord::SessionStart { time: ts("2026-09-17T08:05:00.000Z"), session: SessionId::new("mm-thread-42").unwrap(), epoch: Epoch(0), slot: 0, baseline: Hash32::from_hex(SEQ_HEX).unwrap(), }, LogRecord::User { time: ts("2026-09-17T08:05:01.000Z"), content: "What is in /etc/hosts?".to_string(), }, LogRecord::Assistant { time: ts("2026-09-17T08:05:03.000Z"), content: None, reasoning_content: Some("The user wants a file.\nI will read it.".to_string()), tool_calls: vec![ToolCall { id: "call_a1".to_string(), name: "read_file".to_string(), arguments: r#"{"path":"/etc/hosts"}"#.to_string(), }], }, LogRecord::ToolResult { time: ts("2026-09-17T08:05:04.000Z"), call: CallId(1), tool_call_id: "call_a1".to_string(), content: "127.0.0.1 localhost\n".to_string(), class: DataClass::Private, untrusted: true, truncated: false, }, LogRecord::Assistant { time: ts("2026-09-17T08:05:06.000Z"), content: Some("It maps localhost to 127.0.0.1.".to_string()), reasoning_content: None, tool_calls: vec![], }, LogRecord::CacheLoss { time: ts("2026-09-17T09:00:00.000Z"), expected: 30695, got: 0, }, LogRecord::EpochEnd { time: ts("2026-09-17T12:00:00.000Z"), next: Epoch(1), summary: "Looked at /etc/hosts.".to_string(), }, ]; check("session.jsonl", &want); } #[test] fn unknown_fields_and_types_are_rejected() { let user = r#"{"type":"user","time":"2026-09-17T08:05:01.000Z","content":"hi"}"#; assert!(serde_json::from_str::(user).is_ok()); let extra = user.replacen("\"content\"", "\"role\":\"user\",\"content\"", 1); assert!(serde_json::from_str::(&extra).is_err()); let unknown_type = user.replacen("\"user\"", "\"system\"", 1); assert!(serde_json::from_str::(&unknown_type).is_err()); let decision = r#"{"outcome":"allowed","grant":"g"}"#; assert!(serde_json::from_str::(decision).is_ok()); let extra = decision.replacen("\"grant\"", "\"why\":\"\",\"grant\"", 1); assert!(serde_json::from_str::(&extra).is_err()); }