Files
boxmaker/crates/memoryd/tests/honcho_epochs.rs
T
2026-10-02 23:54:38 -07:00

226 lines
6.9 KiB
Rust

//! Honcho reads every epoch file of a session, in order. Do not edit.
#[path = "support/honcho.rs"]
mod honcho;
#[path = "support/logs.rs"]
mod logs;
#[path = "support/tmp.rs"]
mod tmp;
#[path = "support/turns.rs"]
mod turns;
#[path = "support/worker.rs"]
mod worker;
use std::cell::RefCell;
use bxcore::{Epoch, LogRecord, SessionId, Timestamp};
use honcho::FakeHoncho;
use logs::{assistant, write_log};
use memoryd::honcho::decide::{turn_records, turns};
use memoryd::honcho::ledger::{Action, Item, Ledger, Record, Workspace};
use memoryd::honcho::turns::{LOG_RUNBOOK, TurnSync, read_session};
use memoryd::honcho::worker::Step;
use tmp::TempDir;
use turns::{clean, sid, spoken};
use worker::{FAST, Home as WorkerHome};
const LEDGER: &str = "honcho.jsonl";
fn epoch_end(summary: &str) -> LogRecord {
LogRecord::EpochEnd {
time: Timestamp::parse("2026-09-28T10:00:00.000Z").unwrap(),
next: Epoch(1),
summary: summary.to_string(),
}
}
fn write_epoch(home: &std::path::Path, session: &str, epoch: u32, records: &[LogRecord]) {
let dir = home.join("sessions").join(session);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join(format!("{epoch}.jsonl"));
let text: String = records
.iter()
.map(|r| format!("{}\n", serde_json::to_string(r).unwrap()))
.collect();
std::fs::write(path, text).unwrap();
}
struct World {
sync: TurnSync,
answer: RefCell<Result<bxcore::SessionStateReply, String>>,
dir: TempDir,
}
impl World {
fn new(tag: &str) -> World {
let dir = TempDir::new(tag);
std::fs::create_dir_all(dir.path().join("memory")).unwrap();
let mut ledger = Ledger::open(&dir.path().join("memory").join(LEDGER)).unwrap();
ledger
.append(Record::Began {
time: Timestamp::parse("2026-09-28T09:00:00.000Z").unwrap(),
})
.unwrap();
World {
sync: TurnSync::new(ledger, "boxmaker0", &dir.path().join("sessions")),
answer: RefCell::new(Ok(clean())),
dir,
}
}
fn updated(&mut self, session: &str, turn: u64) {
let ask = |s: &SessionId| {
let _ = s;
self.answer.borrow().clone()
};
let log = |_line: &str| {};
let now = Timestamp::parse("2026-09-28T10:00:00.000Z").unwrap();
self.sync.updated(&sid(session), turn, &ask, now, &log);
}
fn action(&self, session: &str, turn: u64) -> Option<Action> {
let item = Item::Turn {
session: sid(session),
turn,
};
let path = self.dir.path().join("memory").join(LEDGER);
let text = std::fs::read_to_string(path).unwrap_or_default();
let key = serde_json::to_string(&item).unwrap();
let line = text.lines().find(|l| l.contains(&key))?;
let value: serde_json::Value = serde_json::from_str(line).unwrap();
Some(serde_json::from_value(value["action"].clone()).unwrap())
}
}
fn to(workspace: Workspace, speaker: &str) -> Action {
Action::Turn {
workspace,
speaker: speaker.to_string(),
}
}
#[test]
fn concatenated_logs_count_three_turns() {
let dir = TempDir::new("honcho-epochs-count");
let home = dir.path();
let epoch0 = vec![
spoken("one", Some("kyle")),
assistant(2, Some("ok"), &[]),
spoken("two", Some("kyle")),
assistant(3, Some("ok"), &[]),
epoch_end("sum"),
];
write_log(home, "mm-a", &epoch0);
write_epoch(
home,
"mm-a",
1,
&[spoken("three", Some("kyle")), assistant(4, Some("ok"), &[])],
);
let records = read_session(&home.join("sessions"), &sid("mm-a")).unwrap();
assert_eq!(turns(&records), 3);
let turn3 = turn_records(&records, 3).unwrap();
assert!(
matches!(turn3[0], LogRecord::User { ref content, .. } if content == "three"),
"{turn3:?}"
);
}
#[test]
fn epoch_0_alone_still_reads() {
let dir = TempDir::new("honcho-epochs-zero");
let home = dir.path();
write_log(home, "mm-a", &[spoken("only", Some("kyle"))]);
let records = read_session(&home.join("sessions"), &sid("mm-a")).unwrap();
assert_eq!(turns(&records), 1);
}
#[test]
fn a_damaged_later_epoch_is_an_error_naming_that_file() {
let dir = TempDir::new("honcho-epochs-damaged");
let home = dir.path();
write_log(home, "mm-a", &[spoken("one", Some("kyle"))]);
let path = home.join("sessions/mm-a/1.jsonl");
std::fs::write(&path, "{\"type\":\"zz\"}\n").unwrap();
let err = read_session(&home.join("sessions"), &sid("mm-a")).unwrap_err();
assert!(err.to_string().contains("1.jsonl"), "{err}");
assert!(
err.to_string().contains(LOG_RUNBOOK) || err.to_string().contains("session-log-damaged"),
"{err}"
);
}
#[test]
fn turn_sync_decides_a_turn_that_lives_in_epoch_1() {
let mut w = World::new("honcho-epochs-sync");
write_log(
w.dir.path(),
"mm-a",
&[
spoken("one", Some("kyle")),
assistant(2, Some("ok"), &[]),
spoken("two", Some("kyle")),
assistant(3, Some("ok"), &[]),
epoch_end("sum"),
],
);
write_epoch(
w.dir.path(),
"mm-a",
1,
&[spoken("three", Some("kyle")), assistant(4, Some("ok"), &[])],
);
w.updated("mm-a", 3);
assert_eq!(w.action("mm-a", 3), Some(to(Workspace::Trusted, "kyle")));
}
#[test]
fn a_missing_session_directory_is_empty() {
let dir = TempDir::new("honcho-epochs-missing");
let records = read_session(&dir.path().join("sessions"), &sid("mm-none")).unwrap();
assert!(records.is_empty());
}
#[test]
fn send_turn_reads_a_turn_that_lives_in_epoch_1() {
let fake = FakeHoncho::start();
let home = WorkerHome::new("honcho-epochs-send");
write_log(
home.dir.path(),
"mm-a",
&[
spoken("one", Some("kyle")),
assistant(2, Some("ok"), &[]),
spoken("two", Some("kyle")),
assistant(3, Some("ok"), &[]),
epoch_end("sum"),
],
);
write_epoch(
home.dir.path(),
"mm-a",
1,
&[spoken("three", Some("kyle")), assistant(4, Some("ok"), &[])],
);
home.report("mm-a", 3);
let mut w = home.worker(&fake, FAST);
assert_eq!(w.step(), Step::Done);
assert_eq!(w.step(), Step::Done);
assert_eq!(w.step(), Step::Done);
let posts: Vec<_> = fake
.requests()
.into_iter()
.filter(|r| r.path.ends_with("/messages"))
.collect();
assert_eq!(posts.len(), 3, "{posts:?}");
let body = posts[2].body.as_ref().expect("message batch");
let contents: Vec<&str> = body["messages"]
.as_array()
.unwrap()
.iter()
.map(|m| m["content"].as_str().unwrap())
.collect();
assert_eq!(contents, ["three", "ok"]);
assert_eq!(body["messages"][0]["metadata"]["turn"], 3);
}