//! What `bxctl memory sync` prints (M5c spec, section 5): the ledger's counts, and each failed //! item, oldest first, by its label, its kind and its status, exactly; the same after a restart; //! and `memoryd` answering it, with `[honcho]` and without. Do not edit. #[path = "support/tmp.rs"] mod tmp; use std::os::unix::net::UnixStream; use std::sync::Arc; use std::time::Duration; use bxcore::{ CallId, DataClass, Envelope, MemoryAdmin, MemoryCall, MemoryCommand, Message, SessionId, Timestamp, ToolRequest, read_frame, write_frame, }; use memoryd::config::Config; use memoryd::honcho::ledger::{Action, Failure, Item, Ledger, Record, Workspace, label}; use memoryd::honcho::report::{NOT_CONFIGURED, report}; use memoryd::serve; use tmp::TempDir; fn at(minute: u32) -> Timestamp { Timestamp::parse(&format!("2026-09-28T10:{minute:02}:00.000Z")).unwrap() } fn turn(n: u64) -> Item { Item::Turn { session: SessionId::new("mm-a").unwrap(), turn: n, } } fn edit(id: &str) -> Item { Item::Edit { id: id.to_string(), part: 0, } } fn to_trusted() -> Action { Action::Turn { workspace: Workspace::Trusted, speaker: "kyle".to_string(), } } /// A ledger with every state: 2 sent, 1 waiting, 2 failed (the later-decided one failing first), /// 1 skipped. fn mixed(dir: &TempDir) -> Ledger { let mut l = Ledger::open(&dir.path().join("honcho.jsonl")).unwrap(); l.append(Record::Began { time: at(0) }).unwrap(); let decided = |item: Item, action: Action| Record::Decided { time: at(0), item, action, }; let records = vec![ decided(turn(1), to_trusted()), decided(turn(2), to_trusted()), decided( edit("2026-09-28-0001"), Action::Conclude { workspace: Workspace::Trusted, peer: "kyle".to_string(), session: SessionId::new("mm-a").unwrap(), text: "a private fact".to_string(), }, ), decided( turn(3), Action::Skip { why: "the turn has no single speaker".to_string(), }, ), decided(turn(4), to_trusted()), decided(turn(5), to_trusted()), decided(turn(6), to_trusted()), Record::Sent { time: at(1), item: turn(1), ids: vec!["m1".to_string()], }, Record::Failed { time: at(2), item: turn(4), status: None, kind: "log".to_string(), }, Record::Failed { time: at(3), item: edit("2026-09-28-0001"), status: Some(422), kind: "status".to_string(), }, Record::Sent { time: at(4), item: turn(2), ids: vec!["m2".to_string()], }, ]; for r in records { l.append(r).unwrap(); } l } const MIXED: &str = "honcho sync: 7 decided: 2 sent, 2 waiting, 2 failed, 1 skipped failed, oldest first: 2026-09-28T10:02:00.000Z turn 4 of session mm-a log 2026-09-28T10:03:00.000Z part 0 of edit 2026-09-28-0001 status (HTTP 422)"; #[test] fn the_report_is_exactly_this() { let dir = TempDir::new("sync-report"); let l = mixed(&dir); assert_eq!(report(&l), MIXED); assert!(!report(&l).contains("private"), "no text is printed"); let empty = Ledger::open(&dir.path().join("none.jsonl")).unwrap(); assert_eq!( report(&empty), "honcho sync: 0 decided: 0 sent, 0 waiting, 0 failed, 0 skipped" ); assert_eq!( NOT_CONFIGURED, "honcho sync: not configured (memoryd.toml has no [honcho])" ); } #[test] fn the_failures_are_oldest_first_and_survive_a_restart() { let dir = TempDir::new("sync-failures"); let l = mixed(&dir); let expected = vec![ Failure { item: turn(4), time: at(2), status: None, kind: "log".to_string(), }, Failure { item: edit("2026-09-28-0001"), time: at(3), status: Some(422), kind: "status".to_string(), }, ]; assert_eq!(l.failures(), expected); let again = Ledger::open(&dir.path().join("honcho.jsonl")).unwrap(); assert_eq!(again.failures(), expected); assert_eq!(report(&again), MIXED); assert_eq!(label(&turn(4)), "turn 4 of session mm-a"); assert_eq!(label(&edit("e")), "part 0 of edit e"); } const SECTION: &str = "[honcho] url = \"http://127.0.0.1:9\" agent = \"boxmaker0\" owner = \"kyle\" trusted = \"boxmaker0-trusted\" untrusted = \"boxmaker0-untrusted\" fleet = \"fleet\" "; fn ask(socket: &std::path::Path, msg: Message) -> Message { let mut stream = UnixStream::connect(socket).unwrap(); stream .set_read_timeout(Some(Duration::from_secs(10))) .unwrap(); let env = Envelope { v: 1, id: 5, r#final: true, msg, }; write_frame(&mut stream, &env).unwrap(); read_frame(&mut stream).unwrap().msg } fn sync(config: &Config) -> String { let msg = Message::MemoryAdmin(MemoryAdmin { command: MemoryCommand::Sync, arg: String::new(), }); match ask(&config.admin_socket(), msg) { Message::MemoryAdminReply(reply) => reply.text, other => panic!("{other:?}"), } } fn serve(dir: &TempDir, honcho: bool) -> Config { let soul = dir.write("etc/soul.md", "You are Boxmaker.\n"); let text = format!( "[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n{}", dir.path().display(), soul.display(), if honcho { SECTION } else { "" } ); let config = Config::parse(&text).unwrap(); let log: serve::Log = Arc::new(|_: &str| {}); let running = serve::start(config.clone(), log).unwrap(); std::thread::spawn(move || running.run()); config } #[test] fn memoryd_answers_with_the_report() { let dir = TempDir::new("sync-serve"); let config = serve(&dir, true); assert_eq!( sync(&config), "honcho sync: 0 decided: 0 sent, 0 waiting, 0 failed, 0 skipped" ); let call = Message::MemoryCall(MemoryCall { request: ToolRequest { session: SessionId::new("mm-a").unwrap(), call: CallId(1), tool: "memory".to_string(), arguments: r#"{"action":"add","file":"user","text":"Prefers UTC."}"#.to_string(), }, taint: DataClass::Private, untrusted: false, }); ask(&config.broker_socket(), call); // Honcho is not reachable here: the conclusion waits. assert_eq!( sync(&config), "honcho sync: 1 decided: 0 sent, 1 waiting, 0 failed, 0 skipped" ); } #[test] fn without_honcho_it_is_not_configured() { let dir = TempDir::new("sync-off"); let config = serve(&dir, false); assert_eq!(sync(&config), NOT_CONFIGURED); }