//! 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>, 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 { 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); }