//! Tests for multi-epoch session open, turns, trust and call ids. Do not edit. mod support; use bxcore::{CallId, DataClass, Epoch, LogRecord, SessionId, Timestamp}; use loopd::baseline::Baseline; use loopd::session::{Session, SessionError}; use support::Home; fn baseline() -> Baseline { Baseline { system: "sys".to_string(), tools: vec![], } } fn later() -> Baseline { Baseline { system: "sys-epoch-1".to_string(), tools: vec![], } } fn id(s: &str) -> SessionId { SessionId::new(s).unwrap() } fn ts() -> Timestamp { Timestamp::parse("2026-09-18T08:00:00.000Z").unwrap() } fn user(content: &str) -> LogRecord { LogRecord::User { time: ts(), content: content.to_string(), speaker: None, } } fn write_jsonl(home: &Home, session: &str, epoch: u32, records: &[LogRecord]) { let text: String = records .iter() .map(|r| format!("{}\n", serde_json::to_string(r).unwrap())) .collect(); home.write(&format!("sessions/{session}/{epoch}.jsonl"), &text); } fn write_baseline(home: &Home, session: &str, epoch: u32, b: &Baseline) { home.write( &format!("sessions/{session}/{epoch}.baseline.json"), &b.to_json().unwrap(), ); } fn start(session: &str, epoch: u32, slot: u32, b: &Baseline) -> LogRecord { LogRecord::SessionStart { time: ts(), session: id(session), epoch: Epoch(epoch), slot, baseline: b.hash().unwrap(), } } fn epoch_end(next: u32, summary: &str) -> LogRecord { LogRecord::EpochEnd { time: ts(), next: Epoch(next), summary: summary.to_string(), } } fn tool_result(call: u64, untrusted: bool) -> LogRecord { LogRecord::ToolResult { time: ts(), call: CallId(call), tool_call_id: format!("c{call}"), content: "out".to_string(), class: DataClass::Public, untrusted, truncated: false, } } #[test] fn open_loads_the_highest_complete_epoch_and_its_carry() { let home = Home::new(); let mut s = Session::create(&home.dir, id("a"), baseline(), 3).unwrap(); s.append(user("one")).unwrap(); s.append(user("two")).unwrap(); s.append(epoch_end(1, "carry-text")).unwrap(); drop(s); write_baseline(&home, "a", 1, &later()); write_jsonl(&home, "a", 1, &[start("a", 1, 3, &later()), user("three")]); let s = Session::open(&home.dir, id("a")).unwrap(); assert_eq!(s.epoch(), Epoch(1)); assert_eq!(s.slot(), 3); assert_eq!(s.carry(), Some("carry-text")); assert_eq!(s.baseline(), &later()); assert_eq!(s.records().len(), 2); assert!(matches!(s.records()[1], LogRecord::User { ref content, .. } if content == "three")); } #[test] fn a_missing_carry_on_epoch_1_is_an_error() { let home = Home::new(); let s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap(); drop(s); write_baseline(&home, "a", 1, &later()); write_jsonl(&home, "a", 1, &[start("a", 1, 0, &later())]); match Session::open(&home.dir, id("a")) { Err(SessionError::NoCarry { ref path, .. }) => { assert!(path.ends_with("0.jsonl"), "{path:?}"); } other => panic!("{other:?}"), } let e: Box = Box::new(Session::open(&home.dir, id("a")).map(|_| ()).unwrap_err()); assert!(e.to_string().contains("0.jsonl"), "{e}"); assert!( e.to_string() .contains("see docs/runbook.md#session-log-damaged"), "{e}" ); } #[test] fn epoch_0_has_no_carry() { let home = Home::new(); let s = Session::create(&home.dir, id("a"), baseline(), 7).unwrap(); drop(s); let s = Session::open(&home.dir, id("a")).unwrap(); assert_eq!(s.epoch(), Epoch(0)); assert_eq!(s.carry(), None); assert_eq!(s.slot(), 7); } #[test] fn turns_count_users_across_every_epoch() { let home = Home::new(); let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap(); s.append(user("one")).unwrap(); s.append(user("two")).unwrap(); s.append(epoch_end(1, "sum")).unwrap(); drop(s); write_baseline(&home, "a", 1, &later()); write_jsonl(&home, "a", 1, &[start("a", 1, 0, &later()), user("three")]); let s = Session::open(&home.dir, id("a")).unwrap(); assert_eq!(s.turns(), 3); assert_eq!( s.records() .iter() .filter(|r| matches!(r, LogRecord::User { .. })) .count(), 1, "records() is the current epoch only" ); } #[test] fn untrusted_reads_every_epoch() { let home = Home::new(); let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap(); s.append(tool_result(1, true)).unwrap(); s.append(epoch_end(1, "sum")).unwrap(); drop(s); write_baseline(&home, "a", 1, &later()); write_jsonl( &home, "a", 1, &[start("a", 1, 0, &later()), tool_result(2, false)], ); let s = Session::open(&home.dir, id("a")).unwrap(); assert!(s.untrusted(), "an untrusted result in epoch 0 still taints"); assert!( !s.records().iter().any(|r| matches!( r, LogRecord::ToolResult { untrusted: true, .. } )), "the current epoch's records look clean" ); } #[test] fn call_ids_continue_across_epochs() { let home = Home::new(); let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap(); s.append(tool_result(4, false)).unwrap(); s.append(epoch_end(1, "sum")).unwrap(); drop(s); write_baseline(&home, "a", 1, &later()); write_jsonl( &home, "a", 1, &[start("a", 1, 0, &later()), tool_result(2, false)], ); let mut s = Session::open(&home.dir, id("a")).unwrap(); assert_eq!( s.next_call(), CallId(5), "one more than the highest call in any epoch" ); } #[test] fn append_writes_the_current_epoch_file() { let home = Home::new(); let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap(); s.append(epoch_end(1, "sum")).unwrap(); drop(s); write_baseline(&home, "a", 1, &later()); write_jsonl(&home, "a", 1, &[start("a", 1, 0, &later())]); let mut s = Session::open(&home.dir, id("a")).unwrap(); s.append(user("after")).unwrap(); let text = home.read("sessions/a/1.jsonl"); assert!(text.contains("after"), "{text}"); let old = home.read("sessions/a/0.jsonl"); assert!(!old.contains("after"), "{old}"); } #[test] fn an_incomplete_next_epoch_is_ignored() { let home = Home::new(); let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap(); s.append(user("still-here")).unwrap(); drop(s); write_baseline(&home, "a", 1, &later()); let s = Session::open(&home.dir, id("a")).unwrap(); assert_eq!(s.epoch(), Epoch(0)); assert_eq!(s.carry(), None); assert_eq!(s.turns(), 1); drop(s); write_jsonl(&home, "a", 2, &[start("a", 2, 0, &later())]); let s = Session::open(&home.dir, id("a")).unwrap(); assert_eq!(s.epoch(), Epoch(0), "a log without its baseline is ignored"); } #[test] fn epoch_end_without_the_next_files_stays_on_the_old_epoch() { let home = Home::new(); let mut s = Session::create(&home.dir, id("a"), baseline(), 0).unwrap(); s.append(user("x")).unwrap(); s.append(epoch_end(1, "sum")).unwrap(); drop(s); let s = Session::open(&home.dir, id("a")).unwrap(); assert_eq!(s.epoch(), Epoch(0)); assert_eq!(s.carry(), None); assert_eq!(s.turns(), 1); assert!( matches!(s.records().last(), Some(LogRecord::EpochEnd { next, .. }) if *next == Epoch(1)) ); }