use std::io::BufRead;
use std::path::Path;
use serde::Deserialize;
use super::error::SimulationError;
#[derive(Debug, Clone, Deserialize)]
pub struct SimulationEvent {
pub event_type: String,
pub agent_id: String,
pub payload: String,
}
#[derive(Debug)]
pub struct HistoricalReplay {
events: Vec<SimulationEvent>,
}
impl HistoricalReplay {
pub fn from_file(path: &Path) -> Result<Self, SimulationError> {
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
let mut events = Vec::new();
for (line_num, line_result) in reader.lines().enumerate() {
let line = line_result?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let event: SimulationEvent = serde_json::from_str(trimmed)
.map_err(|e| SimulationError::AuditParse(format!("line {}: {e}", line_num + 1)))?;
events.push(event);
}
Ok(Self { events })
}
pub fn events(&self) -> &[SimulationEvent] {
&self.events
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn parse_valid_jsonl() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(
tmp,
r#"{{"event_type":"ToolCallIntercepted","agent_id":"agent-1","payload":"{{\"name\":\"read_file\"}}" }}"#
)
.unwrap();
writeln!(
tmp,
r#"{{"event_type":"PolicyViolation","agent_id":"agent-2","payload":"{{\"name\":\"delete_db\"}}" }}"#
)
.unwrap();
let replay = HistoricalReplay::from_file(tmp.path()).unwrap();
assert_eq!(replay.events().len(), 2);
assert_eq!(replay.events()[0].event_type, "ToolCallIntercepted");
assert_eq!(replay.events()[0].agent_id, "agent-1");
assert_eq!(replay.events()[1].event_type, "PolicyViolation");
}
#[test]
fn parse_empty_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let replay = HistoricalReplay::from_file(tmp.path()).unwrap();
assert!(replay.events().is_empty());
}
#[test]
fn skip_blank_lines() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp).unwrap();
writeln!(
tmp,
r#"{{"event_type":"ToolCallIntercepted","agent_id":"a1","payload":"{{}}"}}"#
)
.unwrap();
writeln!(tmp).unwrap();
let replay = HistoricalReplay::from_file(tmp.path()).unwrap();
assert_eq!(replay.events().len(), 1);
}
#[test]
fn malformed_line_returns_error() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "not valid json").unwrap();
let result = HistoricalReplay::from_file(tmp.path());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("line 1"));
}
#[test]
fn missing_file_returns_error() {
let result = HistoricalReplay::from_file(Path::new("/tmp/nonexistent-audit-log.jsonl"));
assert!(result.is_err());
}
}