use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(target_family = "wasm"))]
use fs2::FileExt;
use crate::session::event::SessionEvent;
#[derive(Debug)]
pub enum EventLogError {
Io(std::io::Error),
Json(serde_json::Error),
}
impl std::fmt::Display for EventLogError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "event log io: {e}"),
Self::Json(e) => write!(f, "event log json: {e}"),
}
}
}
impl std::error::Error for EventLogError {}
impl From<std::io::Error> for EventLogError {
fn from(e: std::io::Error) -> Self { Self::Io(e) }
}
impl From<serde_json::Error> for EventLogError {
fn from(e: serde_json::Error) -> Self { Self::Json(e) }
}
pub struct EventLog {
path: PathBuf,
sequence: AtomicU64,
}
impl EventLog {
pub fn open(session_dir: &Path) -> Result<Self, EventLogError> {
std::fs::create_dir_all(session_dir)?;
let path = session_dir.join("events.jsonl");
let count = read_counter_or_recount(&path)?;
Ok(Self { path, sequence: AtomicU64::new(count) })
}
pub fn append(&self, event: &mut SessionEvent) -> Result<(), EventLogError> {
self.append_locked(event)
}
#[cfg(not(target_family = "wasm"))]
fn append_locked(&self, event: &mut SessionEvent) -> Result<(), EventLogError> {
let lock_path = self.path.with_extension("jsonl.lock");
let lock_file = open_lock_file(&lock_path)?;
FileExt::lock_exclusive(&lock_file)?;
let result = (|| -> Result<(), EventLogError> {
let count = read_counter_or_recount(&self.path)?;
event.sequence_no = count;
let mut line = serde_json::to_vec(event)?;
line.push(b'\n');
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
file.write_all(&line)?;
file.flush()?;
let new_size = file.metadata().map(|m| m.len()).unwrap_or(0);
let _ = write_counter(&self.path, count + 1, new_size);
self.sequence.store(count + 1, Ordering::SeqCst);
Ok(())
})();
let _ = FileExt::unlock(&lock_file);
result
}
#[cfg(target_family = "wasm")]
fn append_locked(&self, event: &mut SessionEvent) -> Result<(), EventLogError> {
event.sequence_no = self.sequence.fetch_add(1, Ordering::SeqCst);
let mut line = serde_json::to_vec(event)?;
line.push(b'\n');
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
file.write_all(&line)?;
file.flush()?;
Ok(())
}
pub fn read_all(&self) -> Result<Vec<SessionEvent>, EventLogError> {
self.read_all_with_stats().map(|(events, _skipped)| events)
}
pub fn read_all_with_stats(&self) -> Result<(Vec<SessionEvent>, usize), EventLogError> {
if !self.path.exists() {
return Ok((Vec::new(), 0));
}
let file = std::fs::File::open(&self.path)?;
let reader = std::io::BufReader::new(file);
let mut events = Vec::new();
let mut skipped = 0usize;
for (idx, line) in reader.lines().enumerate() {
let line = line?;
if line.trim().is_empty() {
continue;
}
match serde_json::from_str::<SessionEvent>(&line) {
Ok(event) => events.push(event),
Err(e) => {
skipped += 1;
eprintln!(
"[treeship] event_log: skipping malformed line {} in {}: {}",
idx + 1,
self.path.display(),
e,
);
}
}
}
if skipped > 0 {
eprintln!(
"[treeship] event_log: {} malformed line(s) skipped while reading {} (kept {} valid event(s))",
skipped,
self.path.display(),
events.len(),
);
}
Ok((events, skipped))
}
pub fn event_count(&self) -> u64 {
self.sequence.load(Ordering::SeqCst)
}
pub fn path(&self) -> &Path {
&self.path
}
}
#[cfg(all(not(target_family = "wasm"), unix))]
fn open_lock_file(path: &Path) -> Result<std::fs::File, std::io::Error> {
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::os::unix::io::AsRawFd;
let file = std::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.mode(0o600)
.open(path)?;
if let Ok(meta) = file.metadata() {
let mode = meta.permissions().mode() & 0o777;
let owned_by_us = meta.uid() == nix_uid();
if owned_by_us && mode != 0o600 {
let fd = file.as_raw_fd();
let rc = unsafe { libc_fchmod(fd, 0o600) };
if rc != 0 {
let err = std::io::Error::last_os_error();
eprintln!(
"[treeship] warning: could not tighten lock file perms on {} \
to 0o600 (current: 0o{:o}). Error: {}. Lock still functions; \
only the privacy of the sidecar is affected. Common cause: \
NFS mount or filesystem without full POSIX perm support.",
path.display(), mode, err
);
}
}
}
Ok(file)
}
#[cfg(all(not(target_family = "wasm"), unix))]
fn libc_fchmod(fd: i32, mode: u32) -> i32 {
unsafe extern "C" {
fn fchmod(fd: i32, mode: u32) -> i32;
}
unsafe { fchmod(fd, mode) }
}
#[cfg(all(not(target_family = "wasm"), unix))]
fn nix_uid() -> u32 {
unsafe extern "C" {
fn geteuid() -> u32;
}
unsafe { geteuid() }
}
#[cfg(all(not(target_family = "wasm"), not(unix)))]
fn open_lock_file(path: &Path) -> Result<std::fs::File, std::io::Error> {
std::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(path)
}
fn counter_path(events_path: &Path) -> PathBuf {
events_path.with_extension("jsonl.count")
}
#[cfg(not(target_family = "wasm"))]
fn read_counter_consistent(events_path: &Path) -> Option<u64> {
let counter = counter_path(events_path);
let bytes = std::fs::read(&counter).ok()?;
if bytes.len() != 16 {
return None;
}
let count = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
let recorded_size = u64::from_le_bytes(bytes[8..16].try_into().ok()?);
let actual_size = match std::fs::metadata(events_path) {
Ok(m) => m.len(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => 0,
Err(_) => return None,
};
if actual_size != recorded_size {
return None;
}
Some(count)
}
#[cfg(not(target_family = "wasm"))]
fn read_counter_or_recount(events_path: &Path) -> Result<u64, EventLogError> {
if let Some(count) = read_counter_consistent(events_path) {
return Ok(count);
}
let count = if events_path.exists() {
let f = std::fs::File::open(events_path)?;
let r = std::io::BufReader::new(f);
r.lines().filter(|l| l.is_ok()).count() as u64
} else {
0
};
let size = std::fs::metadata(events_path).map(|m| m.len()).unwrap_or(0);
let _ = write_counter(events_path, count, size);
Ok(count)
}
#[cfg(target_family = "wasm")]
fn read_counter_or_recount(_events_path: &Path) -> Result<u64, EventLogError> {
Ok(0)
}
#[cfg(not(target_family = "wasm"))]
fn write_counter(events_path: &Path, count: u64, byte_size: u64) -> Result<(), std::io::Error> {
use std::io::Write as _;
let counter = counter_path(events_path);
let dir = counter.parent().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "counter path has no parent")
})?;
std::fs::create_dir_all(dir)?;
let mut buf = [0u8; 16];
buf[0..8].copy_from_slice(&count.to_le_bytes());
buf[8..16].copy_from_slice(&byte_size.to_le_bytes());
let tmp = counter.with_extension("count.tmp");
{
let mut f = open_counter_tmp(&tmp)?;
f.write_all(&buf)?;
f.sync_all()?;
}
std::fs::rename(&tmp, &counter)?;
Ok(())
}
#[cfg(all(not(target_family = "wasm"), unix))]
fn open_counter_tmp(path: &Path) -> Result<std::fs::File, std::io::Error> {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o600)
.open(path)
}
#[cfg(all(not(target_family = "wasm"), not(unix)))]
fn open_counter_tmp(path: &Path) -> Result<std::fs::File, std::io::Error> {
std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::event::*;
fn make_event(session_id: &str, event_type: EventType) -> SessionEvent {
SessionEvent {
session_id: session_id.into(),
event_id: generate_event_id(),
timestamp: "2026-04-05T08:00:00Z".into(),
sequence_no: 0,
trace_id: generate_trace_id(),
span_id: generate_span_id(),
parent_span_id: None,
agent_id: "agent://test".into(),
agent_instance_id: "ai_test_1".into(),
agent_name: "test-agent".into(),
agent_role: None,
host_id: "host_test".into(),
tool_runtime_id: None,
event_type,
artifact_ref: None,
meta: None,
}
}
#[test]
fn append_and_read_back() {
let dir = std::env::temp_dir().join(format!("treeship-evtlog-test-{}", rand::random::<u32>()));
let log = EventLog::open(&dir).unwrap();
let mut e1 = make_event("ssn_001", EventType::SessionStarted);
let mut e2 = make_event("ssn_001", EventType::AgentStarted {
parent_agent_instance_id: None,
});
log.append(&mut e1).unwrap();
log.append(&mut e2).unwrap();
assert_eq!(log.event_count(), 2);
assert_eq!(e1.sequence_no, 0);
assert_eq!(e2.sequence_no, 1);
let events = log.read_all().unwrap();
assert_eq!(events.len(), 2);
assert_eq!(events[0].sequence_no, 0);
assert_eq!(events[1].sequence_no, 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn read_all_skips_malformed_lines() {
let dir = std::env::temp_dir().join(format!("treeship-evtlog-malformed-{}", rand::random::<u32>()));
let log = EventLog::open(&dir).unwrap();
let mut good1 = make_event(
"ssn_001",
EventType::AgentWroteFile {
file_path: "src/before.rs".into(),
digest: None,
operation: None,
additions: None,
deletions: None,
},
);
let mut good2 = make_event(
"ssn_001",
EventType::AgentWroteFile {
file_path: "src/after.rs".into(),
digest: None,
operation: None,
additions: None,
deletions: None,
},
);
log.append(&mut good1).unwrap();
log.append(&mut good2).unwrap();
let path = log.path().to_path_buf();
let original = std::fs::read_to_string(&path).unwrap();
let mut lines: Vec<&str> = original.lines().collect();
lines.insert(1, r#"{"session_id":"ssn_001","event_id":"evt_bad","timestamp":"2026-04-26T00:00:00Z","sequence_no":1,"trace_id":"x","span_id":"y","agent_id":"a","agent_instance_id":"i","agent_name":"n","host_id":"h","type":"custom.weird","payload":42}"#);
std::fs::write(&path, lines.join("\n") + "\n").unwrap();
let events = log.read_all().unwrap();
assert_eq!(events.len(), 2, "expected the two valid events to come through; got {}", events.len());
let written_paths: Vec<&str> = events
.iter()
.filter_map(|e| match &e.event_type {
EventType::AgentWroteFile { file_path, .. } => Some(file_path.as_str()),
_ => None,
})
.collect();
assert_eq!(written_paths, vec!["src/before.rs", "src/after.rs"]);
let (events2, skipped) = log.read_all_with_stats().unwrap();
assert_eq!(events2.len(), 2);
assert_eq!(skipped, 1, "exactly one malformed line was injected; expected skipped == 1");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn read_all_with_stats_reports_zero_when_clean() {
let dir = std::env::temp_dir().join(format!("treeship-evtlog-clean-{}", rand::random::<u32>()));
let log = EventLog::open(&dir).unwrap();
let mut e = make_event(
"ssn_001",
EventType::AgentWroteFile {
file_path: "x.rs".into(),
digest: None, operation: None, additions: None, deletions: None,
},
);
log.append(&mut e).unwrap();
let (events, skipped) = log.read_all_with_stats().unwrap();
assert_eq!(events.len(), 1);
assert_eq!(skipped, 0);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn reopen_preserves_sequence() {
let dir = std::env::temp_dir().join(format!("treeship-evtlog-reopen-{}", rand::random::<u32>()));
{
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_001", EventType::SessionStarted);
log.append(&mut e).unwrap();
}
let log = EventLog::open(&dir).unwrap();
assert_eq!(log.event_count(), 1);
let mut e2 = make_event("ssn_001", EventType::AgentStarted {
parent_agent_instance_id: None,
});
log.append(&mut e2).unwrap();
assert_eq!(e2.sequence_no, 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn concurrent_appends_have_unique_sequence_numbers() {
use std::sync::Arc;
use std::thread;
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-race-{}", rand::random::<u32>()));
std::fs::create_dir_all(&dir).unwrap();
const WRITERS: usize = 16;
let dir = Arc::new(dir);
let mut handles = Vec::with_capacity(WRITERS);
for _ in 0..WRITERS {
let dir = Arc::clone(&dir);
handles.push(thread::spawn(move || {
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_race", EventType::SessionStarted);
log.append(&mut e).unwrap();
e.sequence_no
}));
}
let mut seqs: Vec<u64> = handles.into_iter().map(|h| h.join().unwrap()).collect();
seqs.sort();
let expected: Vec<u64> = (0..WRITERS as u64).collect();
assert_eq!(seqs, expected, "sequence_no collisions under contention");
let log = EventLog::open(&dir).unwrap();
let read = log.read_all().unwrap();
assert_eq!(read.len(), WRITERS);
let mut on_disk: Vec<u64> = read.iter().map(|e| e.sequence_no).collect();
on_disk.sort();
assert_eq!(on_disk, expected);
let _ = std::fs::remove_dir_all(&*dir);
}
#[cfg(all(not(target_family = "wasm"), unix))]
#[test]
fn lock_file_has_owner_only_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-perms-{}", rand::random::<u32>()));
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_perms", EventType::SessionStarted);
log.append(&mut e).unwrap();
let lock_path = log.path().with_extension("jsonl.lock");
let meta = std::fs::metadata(&lock_path).expect("lock file must exist after first append");
let mode = meta.permissions().mode() & 0o777;
assert_eq!(
mode, 0o600,
"lock file mode is {:o}, expected 0o600 (owner-only)",
mode
);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(all(not(target_family = "wasm"), unix))]
#[test]
fn existing_lock_file_is_re_tightened() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-retighten-{}", rand::random::<u32>()));
std::fs::create_dir_all(&dir).unwrap();
let lock_path = dir.join("events.jsonl.lock");
std::fs::write(&lock_path, b"").unwrap();
std::fs::set_permissions(&lock_path, std::fs::Permissions::from_mode(0o644)).unwrap();
let pre_mode = std::fs::metadata(&lock_path).unwrap().permissions().mode() & 0o777;
assert_eq!(pre_mode, 0o644, "test setup: pre-existing perms should be 0o644");
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_retighten", EventType::SessionStarted);
log.append(&mut e).unwrap();
let post_mode = std::fs::metadata(&lock_path).unwrap().permissions().mode() & 0o777;
assert_eq!(
post_mode, 0o600,
"lock file should be re-tightened to 0o600 after open; got {:o}",
post_mode
);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn counter_sidecar_written_after_append() {
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-counter-{}", rand::random::<u32>()));
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_counter", EventType::SessionStarted);
log.append(&mut e).unwrap();
let counter = log.path().with_extension("jsonl.count");
let bytes = std::fs::read(&counter).expect("counter sidecar must exist after append");
assert_eq!(bytes.len(), 16, "counter sidecar must be 16 bytes");
let count = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
let recorded_size = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
let actual_size = std::fs::metadata(log.path()).unwrap().len();
assert_eq!(count, 1, "counter must reflect the one appended event");
assert_eq!(
recorded_size, actual_size,
"counter byte_size ({}) must match events.jsonl size ({})",
recorded_size, actual_size
);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn counter_sidecar_recovers_when_missing() {
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-missing-counter-{}", rand::random::<u32>()));
{
let log = EventLog::open(&dir).unwrap();
let mut e1 = make_event("ssn_x", EventType::SessionStarted);
let mut e2 = make_event("ssn_x", EventType::AgentStarted {
parent_agent_instance_id: None,
});
log.append(&mut e1).unwrap();
log.append(&mut e2).unwrap();
}
let counter = dir.join("events.jsonl.count");
std::fs::remove_file(&counter).expect("counter must exist before deletion");
let log = EventLog::open(&dir).unwrap();
assert_eq!(log.event_count(), 2, "open() must recount when counter is missing");
let mut e3 = make_event("ssn_x", EventType::SessionClosed {
summary: None,
duration_ms: None,
});
log.append(&mut e3).unwrap();
assert_eq!(e3.sequence_no, 2);
assert!(counter.exists(), "counter must be rewritten after recount");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn counter_sidecar_recovers_when_corrupt() {
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-corrupt-counter-{}", rand::random::<u32>()));
{
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_corrupt", EventType::SessionStarted);
log.append(&mut e).unwrap();
}
let counter = dir.join("events.jsonl.count");
std::fs::write(&counter, b"junk").unwrap();
let log = EventLog::open(&dir).unwrap();
assert_eq!(log.event_count(), 1, "short-read counter must be ignored, recount kicks in");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn counter_sidecar_recovers_when_size_disagrees() {
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-stale-counter-{}", rand::random::<u32>()));
{
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_stale", EventType::SessionStarted);
log.append(&mut e).unwrap();
}
let events_path = dir.join("events.jsonl");
let mut extra = make_event("ssn_stale", EventType::AgentStarted {
parent_agent_instance_id: None,
});
extra.sequence_no = 999; let mut line = serde_json::to_vec(&extra).unwrap();
line.push(b'\n');
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&events_path)
.unwrap();
std::io::Write::write_all(&mut f, &line).unwrap();
std::io::Write::flush(&mut f).unwrap();
let log = EventLog::open(&dir).unwrap();
assert_eq!(
log.event_count(),
2,
"size mismatch must force recount, ignoring stale counter"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn counter_sidecar_preserves_concurrent_uniqueness() {
use std::sync::Arc;
use std::thread;
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-counter-race-{}", rand::random::<u32>()));
std::fs::create_dir_all(&dir).unwrap();
const WRITERS: usize = 16;
let dir = Arc::new(dir);
let mut handles = Vec::with_capacity(WRITERS);
for _ in 0..WRITERS {
let dir = Arc::clone(&dir);
handles.push(thread::spawn(move || {
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_counter_race", EventType::SessionStarted);
log.append(&mut e).unwrap();
e.sequence_no
}));
}
let mut seqs: Vec<u64> = handles.into_iter().map(|h| h.join().unwrap()).collect();
seqs.sort();
let expected: Vec<u64> = (0..WRITERS as u64).collect();
assert_eq!(seqs, expected, "counter must not bypass the flock race protection");
let log = EventLog::open(&dir).unwrap();
assert_eq!(log.event_count(), WRITERS as u64);
let _ = std::fs::remove_dir_all(&*dir);
}
#[cfg(all(not(target_family = "wasm"), unix))]
#[test]
fn counter_sidecar_has_owner_only_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir()
.join(format!("treeship-evtlog-counter-perms-{}", rand::random::<u32>()));
let log = EventLog::open(&dir).unwrap();
let mut e = make_event("ssn_counter_perms", EventType::SessionStarted);
log.append(&mut e).unwrap();
let counter = log.path().with_extension("jsonl.count");
let mode = std::fs::metadata(&counter).unwrap().permissions().mode() & 0o777;
assert_eq!(
mode, 0o600,
"counter sidecar mode is {:o}, expected 0o600 (owner-only)",
mode
);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn p0_no_duplicate_sequence_under_burst_contention() {
use std::sync::Arc;
use std::thread;
const THREADS: usize = 8;
const PER_THREAD: usize = 25;
const EXPECTED: usize = THREADS * PER_THREAD;
let dir = std::env::temp_dir().join(format!(
"treeship-evtlog-p0-burst-{}",
rand::random::<u32>()
));
std::fs::create_dir_all(&dir).unwrap();
let dir = Arc::new(dir);
let mut handles = Vec::with_capacity(THREADS);
for t in 0..THREADS {
let dir = Arc::clone(&dir);
handles.push(thread::spawn(move || -> Vec<u64> {
let log = EventLog::open(&dir).unwrap();
let mut seen = Vec::with_capacity(PER_THREAD);
for i in 0..PER_THREAD {
let mut e =
make_event(&format!("ssn_burst_{}_{}", t, i), EventType::SessionStarted);
log.append(&mut e).unwrap();
seen.push(e.sequence_no);
}
seen
}));
}
let mut all_returned: Vec<u64> = handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect();
all_returned.sort();
let expected: Vec<u64> = (0..EXPECTED as u64).collect();
assert_eq!(
all_returned, expected,
"returned sequence_no values must be a contiguous range 0..{} \
with no duplicates and no gaps",
EXPECTED
);
let log = EventLog::open(&dir).unwrap();
let events = log.read_all().unwrap();
assert_eq!(
events.len(),
EXPECTED,
"on-disk event count must be exactly {} (got {})",
EXPECTED,
events.len()
);
let mut on_disk: Vec<u64> = events.iter().map(|e| e.sequence_no).collect();
on_disk.sort();
assert_eq!(
on_disk, expected,
"on-disk sequence_no must be a contiguous range with no duplicates and no gaps"
);
assert_eq!(log.event_count(), EXPECTED as u64);
let _ = std::fs::remove_dir_all(&*dir);
}
#[cfg(not(target_family = "wasm"))]
#[test]
fn lock_file_handles_drop_cleanly_under_churn() {
let dir = std::env::temp_dir().join(format!(
"treeship-evtlog-fd-churn-{}",
rand::random::<u32>()
));
std::fs::create_dir_all(&dir).unwrap();
const ITERS: usize = 500;
for i in 0..ITERS {
let log = EventLog::open(&dir).unwrap();
let mut e = make_event(&format!("ssn_churn_{}", i), EventType::SessionStarted);
log.append(&mut e).unwrap();
}
let log = EventLog::open(&dir).unwrap();
let events = log.read_all().unwrap();
assert_eq!(events.len(), ITERS);
let mut seqs: Vec<u64> = events.iter().map(|e| e.sequence_no).collect();
seqs.sort();
let expected: Vec<u64> = (0..ITERS as u64).collect();
assert_eq!(
seqs, expected,
"no FD leak should still produce contiguous seqs"
);
let _ = std::fs::remove_dir_all(&dir);
}
}