//! Memory writes from a `secret` session are never written to Honcho (M5c spec, section 3; review //! of M5c-1, finding 1): the journal records the call's class, and the mirror turns what such a //! write would conclude or send to the fleet into one `Skip`, while its deletions still go. #[path = "support/tmp.rs"] mod tmp; use bxcore::{DataClass, MemoryArgs, SessionId, Timestamp}; use memoryd::edit::{Context, apply}; use memoryd::files::Store; use memoryd::honcho::ledger::{Action, Workspace}; use memoryd::honcho::mirror::{SECRET_WRITE, actions}; use memoryd::journal::{self, Record}; use memoryd::notes::{Kind, Note}; use tmp::TempDir; 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 noted(class: DataClass, untrusted: bool, text: &str) -> String { Note { kind: Kind::Noted { class, untrusted }, time: "2026-09-28T10:00Z".to_string(), session: "mm-a".to_string(), text: text.to_string(), } .render() } fn record(file: Option<&str>, before: &[&str], after: &[&str], class: Option<&str>) -> Record { Record { id: "e1".to_string(), time: "2026-09-28T10:00:00.000Z".to_string(), session: "mm-a".to_string(), call: 1, action: "add".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, class: class.map(str::to_string), } } fn of(record: &Record) -> Vec { actions(record, Some(&sid("mm-a")), AGENT, OWNER_PEER) } fn skip() -> Vec { vec![Action::Skip { why: SECRET_WRITE.to_string(), }] } #[test] fn a_secret_sessions_notes_are_skipped() { let mut clean = record(None, &[], &[], Some("secret")); clean.action = "note".to_string(); clean.notes = vec![ noted(DataClass::Secret, false, "The vault code is 4411."), noted(DataClass::Secret, false, "And a second one."), ]; assert_eq!(of(&clean), skip(), "no conclusion, one Skip for the record"); let mut untrusted = clean.clone(); untrusted.untrusted = true; untrusted.notes = vec![noted(DataClass::Secret, true, "The page says 3.2.")]; assert_eq!(of(&untrusted), skip(), "no fleet message"); } #[test] fn a_note_marked_secret_is_skipped_without_the_records_class() { // A line from before the journal had a class: the note's own header still says secret. let mut old = record(None, &[], &[], None); old.notes = vec![noted(DataClass::Secret, false, "A secret fact.")]; assert_eq!(of(&old), skip()); } #[test] fn a_secret_identity_add_is_skipped() { let add = record( Some("user.md"), &["x"], &["x", "Kyle's PIN is 1234."], Some("secret"), ); assert_eq!(of(&add), skip()); } #[test] fn a_secret_identity_remove_still_deletes() { let remove = record(Some("memory.md"), &["x", "gone"], &["x"], Some("secret")); assert_eq!( of(&remove), vec![Action::Unconclude { workspace: Workspace::Trusted, peer: AGENT.to_string(), text: "gone".to_string(), }] ); // A replace: the deletion first, then one Skip in place of the conclusion. let replace = record(Some("user.md"), &["old"], &["new"], Some("secret")); assert_eq!( of(&replace), vec![ Action::Unconclude { workspace: Workspace::Trusted, peer: OWNER_PEER.to_string(), text: "old".to_string(), }, Action::Skip { why: SECRET_WRITE.to_string() } ] ); } #[test] fn public_and_private_writes_are_unchanged() { for class in ["public", "private"] { let add = record(Some("user.md"), &[], &["Prefers UTC."], Some(class)); assert_eq!( of(&add), vec![Action::Conclude { workspace: Workspace::Trusted, peer: OWNER_PEER.to_string(), session: sid("mm-a"), text: "Prefers UTC.".to_string(), }], "{class}" ); } } #[test] fn old_journal_lines_without_a_class_still_read() { let line = r#"{"id":"2026-09-24-0001","time":"2026-09-24T20:14:37.123Z","session":"mm-s1","call":7,"action":"add","file":"memory.md","to":"memory.md","before":[],"after":["a"],"notes":[],"untrusted":false}"#; let record: Record = serde_json::from_str(line).unwrap(); assert_eq!(record.class, None); assert!(!serde_json::to_string(&record).unwrap().contains("class")); } #[test] fn the_journal_records_the_calls_class() { let dir = TempDir::new("mirror-secret-journal"); let starter = dir.write("etc/soul.md", "Soul.\n"); let store = Store::new(&dir.path().join("memory")); store.init(&starter).unwrap(); for (taint, name) in [ (DataClass::Public, "public"), (DataClass::Private, "private"), (DataClass::Secret, "secret"), ] { let ctx = Context { session: "mm-s1".to_string(), call: 7, taint, untrusted: false, now: Timestamp::parse("2026-09-24T20:14:37.123Z").unwrap(), }; let args: MemoryArgs = serde_json::from_str(&format!( r#"{{"action":"add","file":"memory","text":"from a {name} session"}}"# )) .unwrap(); let outcome = apply(&store, &args, &ctx).unwrap(); journal::record(&store, &ctx, &outcome).unwrap(); let last = journal::records(&store).unwrap().pop().unwrap(); assert_eq!(last.class.as_deref(), Some(name)); } // An owner's undo has no call, so no class. let last = journal::records(&store).unwrap().pop().unwrap(); journal::undo( &store, &last.id, Timestamp::parse("2026-09-24T20:15:00.000Z").unwrap(), ) .unwrap(); let undo = journal::records(&store).unwrap().pop().unwrap(); assert_eq!((undo.action.as_str(), undo.class), ("undo", None)); }