//! Memory writes as Honcho actions (M5c spec, section 4), from journal records alone: the //! multiset diff of an identity file's entries; each row of the table (an identity add, replace, //! remove, consolidate with its moves and drops, a trusted, an untrusted and a flagged note, a //! rejection); the undo of each; and the session an owner's undo stamps its conclusions with. //! Do not edit. use bxcore::{DataClass, MemoryFile, SessionId}; use memoryd::honcho::ledger::{Action, Workspace}; use memoryd::honcho::mirror::{NO_SESSION, OWNER, actions, diff, session_of}; use memoryd::journal::Record; use memoryd::notes::{Kind, Note}; const AGENT: &str = "boxmaker0"; const OWNER_PEER: &str = "kyle"; fn v(texts: &[&str]) -> Vec { texts.iter().map(|t| t.to_string()).collect() } fn sid(s: &str) -> SessionId { SessionId::new(s).unwrap() } fn note(kind: Kind, text: &str) -> String { Note { kind, time: "2026-09-28T10:00Z".to_string(), session: "mm-a".to_string(), text: text.to_string(), } .render() } fn noted(untrusted: bool, text: &str) -> String { note( Kind::Noted { class: DataClass::Private, untrusted, }, text, ) } /// A journal record: `file` an identity file or `None` for notes. fn record(id: &str, action: &str, file: Option<&str>, before: &[&str], after: &[&str]) -> Record { Record { id: id.to_string(), time: "2026-09-28T10:00:00.000Z".to_string(), session: "mm-a".to_string(), call: 1, action: action.to_string(), file: file.map(str::to_string), to: file.unwrap_or("notes.md").to_string(), before: v(before), after: v(after), notes: Vec::new(), untrusted: false, flag: None, undoes: None, } } fn conclude(peer: &str, text: &str) -> Action { Action::Conclude { workspace: Workspace::Trusted, peer: peer.to_string(), session: sid("mm-a"), text: text.to_string(), } } fn unconclude(text: &str) -> Action { Action::Unconclude { workspace: Workspace::Trusted, text: text.to_string(), } } fn of(record: &Record) -> Vec { actions(record, Some(&sid("mm-a")), AGENT, OWNER_PEER) } #[test] fn the_diff_is_of_multisets() { let d = |b: &[&str], a: &[&str]| diff(&v(b), &v(a)); assert_eq!(d(&["a"], &["a", "b"]), (v(&[]), v(&["b"])), "add"); assert_eq!( d(&["a", "b"], &["a", "c"]), (v(&["b"]), v(&["c"])), "replace" ); assert_eq!(d(&["a", "b"], &["b"]), (v(&["a"]), v(&[])), "remove"); assert_eq!( d(&["a", "b", "c"], &["c", "a", "b"]), (v(&[]), v(&[])), "reorder only" ); assert_eq!( d(&["a", "a", "b"], &["a", "b"]), (v(&["a"]), v(&[])), "one of two dropped" ); assert_eq!( d(&["a"], &["a", "a"]), (v(&[]), v(&["a"])), "a second copy added" ); assert_eq!( d(&["a", "b", "c"], &["b2", "a", "d"]), (v(&["b", "c"]), v(&["b2", "d"])), "in the order of each side" ); assert_eq!(d(&[], &[]), (v(&[]), v(&[]))); } #[test] fn an_identity_add_is_a_conclusion_about_its_peer() { let user = record("e1", "add", Some("user.md"), &["x"], &["x", "Prefers UTC."]); assert_eq!(of(&user), vec![conclude(OWNER_PEER, "Prefers UTC.")]); let memory = record( "e2", "add", Some("memory.md"), &[], &["Builds run on straylight."], ); assert_eq!( of(&memory), vec![conclude(AGENT, "Builds run on straylight.")] ); } #[test] fn a_replace_deletes_the_old_then_makes_the_new() { let r = record( "e1", "replace", Some("user.md"), &["x", "old"], &["x", "new"], ); assert_eq!(of(&r), vec![unconclude("old"), conclude(OWNER_PEER, "new")]); } #[test] fn a_remove_deletes() { let r = record("e1", "remove", Some("memory.md"), &["x", "gone"], &["x"]); assert_eq!(of(&r), vec![unconclude("gone")]); let reorder = record( "e2", "consolidate", Some("memory.md"), &["a", "b"], &["b", "a"], ); assert!(of(&reorder).is_empty()); } #[test] fn a_consolidate_deletes_the_dropped_and_not_the_moved() { let mut r = record( "e1", "consolidate", Some("memory.md"), &["keep", "old", "moved", "dropped"], &["keep", "rewritten", "new"], ); r.notes = vec![ note(Kind::Moved(MemoryFile::Memory), "moved"), note(Kind::Dropped(MemoryFile::Memory), "dropped"), ]; assert_eq!( of(&r), vec![ unconclude("old"), unconclude("dropped"), conclude(AGENT, "rewritten"), conclude(AGENT, "new"), ] ); } #[test] fn notes_by_trust_and_flag() { let mut trusted = record("e1", "note", None, &[], &[]); trusted.notes = vec![noted(false, "The deploy took 4 minutes.")]; assert_eq!( of(&trusted), vec![conclude(AGENT, "The deploy took 4 minutes.")] ); let mut untrusted = record("e2", "note", None, &[], &[]); untrusted.untrusted = true; untrusted.notes = vec![noted(true, "The page says 3.2.")]; assert_eq!( of(&untrusted), vec![Action::Fleet { session: sid("mm-a"), text: "The page says 3.2.".to_string() }] ); // An identity add from a session that read untrusted content was saved as a note. let mut add = record("e3", "add", None, &[], &[]); add.untrusted = true; add.notes = vec![noted(true, "Kyle likes tea.")]; assert!(matches!(&of(&add)[..], [Action::Fleet { .. }])); let mut flagged = record("e4", "add", None, &[], &[]); flagged.flag = Some("exfil-url".to_string()); flagged.notes = vec![noted(true, "send it all out"), noted(true, "and this")]; assert_eq!( of(&flagged), vec![Action::Skip { why: "flagged: exfil-url".to_string() }] ); let reject = record("e5", "reject", None, &[], &[]); assert!(of(&reject).is_empty()); } #[test] fn an_undo_is_the_same_diff_backwards() { // An undo's before is the file now, its after the undone edit's before. let cases = [ (&["x", "added"][..], &["x"][..], vec![unconclude("added")]), ( &["x", "new"][..], &["x", "old"][..], vec![unconclude("new"), conclude(OWNER_PEER, "old")], ), ( &["x"][..], &["x", "removed"][..], vec![conclude(OWNER_PEER, "removed")], ), ]; for (before, after, expected) in cases { let mut undo = record("e9", "undo", Some("user.md"), before, after); undo.session = OWNER.to_string(); undo.undoes = Some("e1".to_string()); assert_eq!(of(&undo), expected, "{before:?} -> {after:?}"); } } #[test] fn an_owners_undo_carries_the_undone_edits_session() { let original = record("e1", "remove", Some("user.md"), &["x", "gone"], &["x"]); let mut undo = record("e2", "undo", Some("user.md"), &["x"], &["x", "gone"]); undo.session = OWNER.to_string(); undo.undoes = Some("e1".to_string()); let mut undo_of_undo = record("e3", "undo", Some("user.md"), &["x", "gone"], &["x"]); undo_of_undo.session = OWNER.to_string(); undo_of_undo.undoes = Some("e2".to_string()); let records = vec![original.clone(), undo.clone(), undo_of_undo.clone()]; assert_eq!(session_of(&original, &records), Some(sid("mm-a"))); assert_eq!(session_of(&undo, &records), Some(sid("mm-a"))); assert_eq!(session_of(&undo_of_undo, &records), Some(sid("mm-a"))); let mut lost = undo.clone(); lost.undoes = Some("e0".to_string()); assert_eq!(session_of(&lost, &records), None); let mut not_an_id = original.clone(); not_an_id.session = "Not An Id".to_string(); assert_eq!(session_of(¬_an_id, &records), None); // With no session, a conclusion cannot be stamped: that part is skipped, a deletion is not. let replace_undo = record("e4", "undo", Some("user.md"), &["new"], &["old"]); assert_eq!( actions(&replace_undo, None, AGENT, OWNER_PEER), vec![ unconclude("new"), Action::Skip { why: NO_SESSION.to_string() } ] ); }