Files
boxmaker/docs/plans/M5d1/files/crates/loopd/tests/session_epochs.rs
T
kyleandGrok 4.6 c3b05cf003 docs: M5d-1 checklist fixes before hand-over
Given tests: Action::Turn (not To), send_turn of epoch 1, clippy on
compact_summary, jsonl-without-baseline, live turn after compact sends
the carry. send_turn maps Damaged to Local("log") as read_turn did.
Gate counts 1484 → 1508. Mutations recorded in the README.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-02 22:36:51 -07:00

260 lines
7.6 KiB
Rust

//! 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<dyn std::error::Error> =
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))
);
}