//! The worker sending memory writes (M5c spec, sections 4 and 5), stepped by hand against the fake //! Honcho: a conclusion made in the session's `_memory` session with the agent's and the //! owner's peers; one deleted by the id its making got, or nothing to delete; a 404 on delete as //! done; an untrusted note as a fleet message from the agent; the text neutralised; one outbox //! across turns and edits; errors as for turns; a restart sending nothing again. 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 bxcore::sanitize::TAGS; use bxcore::{SessionId, Timestamp}; use honcho::{FakeHoncho, Mode, Request}; use logs::assistant; use memoryd::honcho::edits::{MEMORY_SUFFIX, memory_session}; use memoryd::honcho::ledger::{Action, Item, Record, Workspace}; use memoryd::honcho::worker::{MAX_PART, Step}; use serde_json::{Value, json}; use turns::spoken; use worker::{FAST, Home}; /// The id fixture 11 gives the first conclusion it makes. const CONCLUSION: &str = "YUBMqIqHhuJprV4o-inrg"; fn sid(s: &str) -> SessionId { SessionId::new(s).unwrap() } fn edit(id: &str, part: u32) -> Item { Item::Edit { id: id.to_string(), part, } } fn conclude(peer: &str, text: &str) -> Action { Action::Conclude { workspace: Workspace::Trusted, peer: peer.to_string(), session: sid("mm-a"), text: text.to_string(), } } /// The deletion of the owner's conclusion `text` (a `user.md` entry dropped). fn unconclude(text: &str) -> Action { unconclude_about("kyle", text) } fn unconclude_about(peer: &str, text: &str) -> Action { Action::Unconclude { workspace: Workspace::Trusted, peer: peer.to_string(), text: text.to_string(), } } fn fleet(text: &str) -> Action { Action::Fleet { session: sid("mm-c"), text: text.to_string(), } } /// Decide `action` for `item`, as task 12 does when an edit is journalled. fn decide(home: &Home, item: Item, action: Action) { let sync = home.sync.lock().unwrap(); sync.ledger() .append(Record::Decided { time: Timestamp::parse("2026-09-28T10:00:00.000Z").unwrap(), item, action, }) .unwrap(); } fn sent_ids(home: &Home, item: &Item) -> Option> { home.records().into_iter().find_map(|r| match r { Record::Sent { item: i, ids, .. } if i == *item => Some(ids), _ => None, }) } fn posts(fake: &FakeHoncho, suffix: &str) -> Vec { fake.requests() .into_iter() .filter(|r| r.path.ends_with(suffix)) .collect() } #[test] fn the_memory_session_is_named_for_the_boxmaker_session() { assert_eq!(MEMORY_SUFFIX, "_memory"); assert_eq!(memory_session(&sid("mm-a")), "mm-a_memory"); } #[test] fn a_conclusion_goes_into_the_sessions_memory_session() { let fake = FakeHoncho::start(); let home = Home::new("edits-conclude"); decide(&home, edit("e1", 0), conclude("kyle", "Prefers UTC.")); let mut w = home.worker(&fake, FAST); assert_eq!(w.step(), Step::Done); let w_path = "/v3/workspaces/boxmaker0-trusted"; let seen: Vec<(String, Value)> = fake .requests() .into_iter() .map(|r| (r.path, r.body.unwrap_or(Value::Null))) .collect(); assert_eq!( seen, vec![ ( "/v3/workspaces".to_string(), json!({ "id": "boxmaker0-trusted" }) ), ( format!("{w_path}/peers"), json!({ "id": "boxmaker0", "configuration": { "observe_me": false } }) ), (format!("{w_path}/peers"), json!({ "id": "kyle" })), ( format!("{w_path}/sessions"), json!({ "id": "mm-a_memory", "peers": { "boxmaker0": { "observe_me": false, "observe_others": false }, "kyle": { "observe_me": false, "observe_others": false }, } }) ), ( format!("{w_path}/conclusions"), json!({ "conclusions": [{ "content": "Prefers UTC.", "observer_id": "kyle", "observed_id": "kyle", "session_id": "mm-a_memory", }] }) ), ] ); assert_eq!( sent_ids(&home, &edit("e1", 0)), Some(vec![CONCLUSION.to_string()]) ); let sync = home.sync.lock().unwrap(); assert_eq!( sync.ledger() .conclusion(Workspace::Trusted, "kyle", "Prefers UTC."), Some((edit("e1", 0), CONCLUSION.to_string())) ); } #[test] fn a_replace_deletes_then_makes_and_an_undo_reverses_it() { let fake = FakeHoncho::start(); let home = Home::new("edits-replace"); decide(&home, edit("e1", 0), conclude("kyle", "old")); decide(&home, edit("e2", 0), unconclude("old")); decide(&home, edit("e2", 1), conclude("kyle", "new")); // The owner's undo of e2: the new deleted, the old made again. decide(&home, edit("e3", 0), unconclude("new")); decide(&home, edit("e3", 1), conclude("kyle", "old")); let mut w = home.worker(&fake, FAST); for _ in 0..5 { assert_eq!(w.step(), Step::Done); } assert_eq!(w.step(), Step::Idle); let order: Vec<(String, String)> = fake .requests() .into_iter() .filter(|r| r.path.contains("/conclusions")) .map(|r| { let text = r.body.map_or(String::new(), |b| { b["conclusions"][0]["content"].as_str().unwrap().to_string() }); (r.method, text) }) .collect(); let post = |t: &str| ("POST".to_string(), t.to_string()); let delete = ("DELETE".to_string(), String::new()); assert_eq!( order, vec![ post("old"), delete.clone(), post("new"), delete, post("old") ] ); // Each deletion names the id its conclusion got, and records it. let deletes = fake .requests() .into_iter() .filter(|r| r.method == "DELETE") .map(|r| r.path) .collect::>(); let path = format!("/v3/workspaces/boxmaker0-trusted/conclusions/{CONCLUSION}"); assert_eq!(deletes, vec![path.clone(), path]); // The memory session, its workspace and peers are made sure of once in this run. assert_eq!(posts(&fake, "/sessions").len(), 1); assert_eq!(posts(&fake, "/peers").len(), 2); assert_eq!( sent_ids(&home, &edit("e2", 0)), Some(vec![CONCLUSION.to_string()]) ); let sync = home.sync.lock().unwrap(); assert_eq!( sync.ledger().conclusion(Workspace::Trusted, "kyle", "new"), None ); assert_eq!( sync.ledger().conclusion(Workspace::Trusted, "kyle", "old"), Some((edit("e3", 1), CONCLUSION.to_string())) ); } #[test] fn a_deletion_with_nothing_to_delete_is_sent_with_no_id() { let fake = FakeHoncho::start(); let home = Home::new("edits-nothing"); decide(&home, edit("e1", 0), unconclude("never made")); // A conclusion that failed is not live either. decide(&home, edit("e2", 0), conclude("kyle", "refused")); decide(&home, edit("e3", 0), unconclude("refused")); let mut w = home.worker(&fake, FAST); assert_eq!(w.step(), Step::Done); assert!(fake.requests().is_empty()); assert_eq!(sent_ids(&home, &edit("e1", 0)), Some(Vec::new())); fake.set(Mode::Status(422)); assert_eq!(w.step(), Step::Done); fake.set(Mode::Fixtures); fake.clear(); assert_eq!(w.step(), Step::Done); assert!(fake.requests().is_empty(), "{:?}", fake.requests()); assert_eq!(sent_ids(&home, &edit("e3", 0)), Some(Vec::new())); assert_eq!( home.lines().len(), 1, "only the refusal: {:?}", home.lines() ); } #[test] fn the_same_text_about_another_peer_is_not_deleted() { // "Uses tabs." is in user.md (about the owner) and in memory.md (about the agent); the // user.md entry is removed: only the owner's conclusion goes. let fake = FakeHoncho::start(); let home = Home::new("edits-peers"); decide(&home, edit("e1", 0), conclude("kyle", "Uses tabs.")); decide(&home, edit("e2", 0), conclude("boxmaker0", "Uses tabs.")); decide(&home, edit("e3", 0), unconclude_about("kyle", "Uses tabs.")); let mut w = home.worker(&fake, FAST); for _ in 0..3 { assert_eq!(w.step(), Step::Done); } let sync = home.sync.lock().unwrap(); assert_eq!( sync.ledger() .conclusion(Workspace::Trusted, "kyle", "Uses tabs."), None ); assert_eq!( sync.ledger() .conclusion(Workspace::Trusted, "boxmaker0", "Uses tabs."), Some((edit("e2", 0), CONCLUSION.to_string())) ); assert_eq!( sent_ids(&home, &edit("e3", 0)), Some(vec![CONCLUSION.to_string()]) ); // Nothing about the owner is left to delete for this text any more. drop(sync); decide(&home, edit("e4", 0), unconclude_about("kyle", "Uses tabs.")); fake.clear(); assert_eq!(w.step(), Step::Done); assert!(fake.requests().is_empty(), "{:?}", fake.requests()); // The memory.md entry removed: the agent's conclusion goes. decide( &home, edit("e5", 0), unconclude_about("boxmaker0", "Uses tabs."), ); assert_eq!(w.step(), Step::Done); assert_eq!(fake.requests().len(), 1, "{:?}", fake.requests()); assert_eq!(fake.requests()[0].method, "DELETE"); let sync = home.sync.lock().unwrap(); assert_eq!( sync.ledger() .conclusion(Workspace::Trusted, "boxmaker0", "Uses tabs."), None ); } #[test] fn a_404_on_delete_is_done() { let fake = FakeHoncho::start(); let home = Home::new("edits-404"); decide(&home, edit("e1", 0), conclude("kyle", "x")); decide(&home, edit("e2", 0), unconclude("x")); let mut w = home.worker(&fake, FAST); assert_eq!(w.step(), Step::Done); fake.set(Mode::Status(404)); assert_eq!(w.step(), Step::Done); assert_eq!( sent_ids(&home, &edit("e2", 0)), Some(vec![CONCLUSION.to_string()]) ); assert!(home.lines().is_empty(), "{:?}", home.lines()); } #[test] fn an_untrusted_note_is_a_fleet_message_from_the_agent() { let fake = FakeHoncho::start(); let home = Home::new("edits-fleet"); decide(&home, edit("e1", 0), fleet("The page says 3.2.")); let mut w = home.worker(&fake, FAST); assert_eq!(w.step(), Step::Done); let seen: Vec<(String, Value)> = fake .requests() .into_iter() .map(|r| (r.path, r.body.unwrap_or(Value::Null))) .collect(); assert_eq!(seen.len(), 4); assert_eq!(seen[0].1["configuration"]["reasoning"]["enabled"], false); assert_eq!(seen[2].0, "/v3/workspaces/fleet/sessions"); assert_eq!( seen[2].1, json!({ "id": "mm-c", "peers": { "boxmaker0": { "observe_me": false, "observe_others": false }, } }) ); assert_eq!( seen[3], ( "/v3/workspaces/fleet/sessions/mm-c/messages".to_string(), json!({ "messages": [{ "content": "The page says 3.2.", "peer_id": "boxmaker0", "metadata": { "agent": "boxmaker0", "boxmaker_session": "mm-c", "edit": "e1" }, }] }) ) ); assert_eq!(sent_ids(&home, &edit("e1", 0)).map(|i| i.len()), Some(1)); // A long note in parts, in one batch. decide(&home, edit("e2", 0), fleet(&"n".repeat(MAX_PART + 1))); assert_eq!(w.step(), Step::Done); let last = posts(&fake, "/messages").pop().unwrap().body.unwrap(); assert_eq!(last["messages"].as_array().unwrap().len(), 2); } #[test] fn a_note_too_long_for_one_batch_is_marked_failed() { let fake = FakeHoncho::start(); let home = Home::new("edits-long"); decide(&home, edit("e1", 0), fleet(&"n".repeat(MAX_PART * 100 + 1))); let mut w = home.worker(&fake, FAST); assert_eq!(w.step(), Step::Done); assert!(matches!( home.records().last(), Some(Record::Failed { status: None, kind, .. }) if kind == "too-long" )); assert!(fake.requests().is_empty()); } #[test] fn the_text_is_neutralised() { let fake = FakeHoncho::start(); let home = Home::new("edits-neutral"); decide( &home, edit("e1", 0), conclude("boxmaker0", &format!("a{}b", TAGS[0].text)), ); decide(&home, edit("e2", 0), fleet(&format!("c{}d", TAGS[1].text))); let mut w = home.worker(&fake, FAST); assert_eq!(w.step(), Step::Done); assert_eq!(w.step(), Step::Done); let concluded = posts(&fake, "/conclusions")[0].body.clone().unwrap(); let content = concluded["conclusions"][0]["content"].as_str().unwrap(); assert!( !content.contains(TAGS[0].text) && content.contains("<"), "{content}" ); let message = posts(&fake, "/messages")[0].body.clone().unwrap(); let content = message["messages"][0]["content"].as_str().unwrap(); assert!( !content.contains(TAGS[1].text) && content.contains("<"), "{content}" ); } #[test] fn one_outbox_in_order_across_turns_and_edits() { let fake = FakeHoncho::start(); let mut home = Home::new("edits-order"); home.log( "mm-a", &[ vec![spoken("one", Some("kyle")), assistant(2, Some("a"), &[])], vec![spoken("two", Some("kyle")), assistant(2, Some("b"), &[])], ], ); home.report("mm-a", 1); decide(&home, edit("e1", 0), conclude("kyle", "Prefers UTC.")); home.report("mm-a", 2); let mut w = home.worker(&fake, FAST); for _ in 0..3 { assert_eq!(w.step(), Step::Done); } let order: Vec = fake .requests() .into_iter() .filter(|r| r.path.ends_with("/messages") || r.path.ends_with("/conclusions")) .map(|r| r.path) .collect(); assert_eq!( order, vec![ "/v3/workspaces/boxmaker0-trusted/sessions/mm-a/messages".to_string(), "/v3/workspaces/boxmaker0-trusted/conclusions".to_string(), "/v3/workspaces/boxmaker0-trusted/sessions/mm-a/messages".to_string(), ] ); // A restart sends nothing again. home.restart(); let fake2 = FakeHoncho::start(); let mut w = home.worker(&fake2, FAST); assert_eq!(w.step(), Step::Idle); assert!(fake2.requests().is_empty()); } #[test] fn errors_as_for_turns() { let fake = FakeHoncho::start(); let home = Home::new("edits-errors"); decide(&home, edit("e1", 0), conclude("kyle", "private-fact")); decide(&home, edit("e2", 0), fleet("private-note")); decide(&home, edit("e3", 0), conclude("kyle", "second")); let mut w = home.worker(&fake, FAST); fake.set(Mode::Status(503)); assert!(matches!(w.step(), Step::Backoff(_))); fake.set(Mode::Status(422)); assert_eq!(w.step(), Step::Done); assert!(matches!( home.records().last(), Some(Record::Failed { status: Some(422), kind, .. }) if kind == "status" )); fake.set(Mode::Status(401)); assert_eq!(w.step(), Step::Done, "the fleet's key refused"); fake.set(Mode::Fixtures); assert_eq!(w.step(), Step::Done, "the trusted workspace goes on"); assert!(sent_ids(&home, &edit("e3", 0)).is_some()); assert_eq!(w.step(), Step::Idle, "the fleet note waits"); let lines = home.lines(); assert_eq!(lines.len(), 4, "{lines:?}"); assert!( lines[2].starts_with("memoryd: part 0 of edit e1 was not sent: "), "{lines:?}" ); assert!(lines[3].contains("workspace fleet"), "{lines:?}"); for line in lines { assert!(!line.contains("private"), "{line}"); } }