//! The edit journal, `log`, `undo`, and review (M5a spec, section 5). Do not edit. #[path = "support/tmp.rs"] mod tmp; use memoryd::edit::{Context, apply}; use memoryd::files::Store; use memoryd::{journal, review}; use proto::{DataClass, MemoryArgs, MemoryFile, Timestamp}; use tmp::TempDir; fn now() -> Timestamp { Timestamp::parse("2026-09-24T20:14:37.123Z").unwrap() } fn setup(tag: &str) -> (TempDir, Store) { let dir = TempDir::new(tag); let starter = dir.write("etc/soul.md", "Soul.\n"); let store = Store::new(&dir.path().join("memory")); store.init(&starter).unwrap(); (dir, store) } fn ctx(untrusted: bool) -> Context { Context { session: "mm-s1".to_string(), call: 7, taint: DataClass::Private, untrusted, now: now(), } } /// Apply and journal, as `memoryd` does for every write. fn write(store: &Store, json: &str, untrusted: bool) -> String { let args: MemoryArgs = serde_json::from_str(json).unwrap(); let c = ctx(untrusted); let outcome = apply(store, &args, &c).unwrap(); journal::record(store, &c, &outcome).unwrap() } #[test] fn every_write_is_journalled_with_a_daily_id() { let (dir, store) = setup("journal-ids"); assert_eq!( write( &store, r#"{"action":"add","file":"memory","text":"a"}"#, false ), "2026-09-24-0001" ); assert_eq!( write(&store, r#"{"action":"note","text":"seen"}"#, true), "2026-09-24-0002" ); let records = journal::records(&store).unwrap(); assert_eq!(records.len(), 2); assert_eq!( ( records[0].action.as_str(), records[0].to.as_str(), records[0].before.clone(), records[0].after.clone() ), ("add", "memory.md", vec![], vec!["a".to_string()]) ); assert_eq!( (records[1].to.as_str(), records[1].untrusted), ("notes.md", true) ); assert!(records[1].notes[0].starts_with("ยง noted 2026-09-24T20:14Z in session mm-s1")); assert!(dir.path().join("memory/edits/2026-09.jsonl").exists()); assert_eq!( journal::log(&store, 10).unwrap(), "2026-09-24-0001 2026-09-24T20:14:37.123Z mm-s1 add memory.md\n2026-09-24-0002 2026-09-24T20:14:37.123Z mm-s1 note notes.md untrusted" ); assert_eq!(journal::log(&store, 1).unwrap().lines().count(), 1); } #[test] fn undo_reverts_the_latest_edit_of_a_file_only() { let (_dir, store) = setup("journal-undo"); let first = write( &store, r#"{"action":"add","file":"memory","text":"a"}"#, false, ); let second = write( &store, r#"{"action":"add","file":"memory","text":"b"}"#, false, ); let note = write(&store, r#"{"action":"note","text":"n"}"#, false); assert_eq!( journal::undo(&store, &first, now()), Err(format!("{first} is not the latest edit of memory.md")) ); assert_eq!( journal::undo(&store, ¬e, now()), Err(format!( "{note} changed no identity file; notes are append-only" )) ); assert_eq!( journal::undo(&store, "2026-01-01-0001", now()), Err("no edit 2026-01-01-0001".to_string()) ); let done = journal::undo(&store, &second, now()).unwrap(); assert!( done.starts_with(&format!("undone: {second} (memory.md restored")), "{done}" ); assert_eq!(store.entries(MemoryFile::Memory).unwrap(), ["a"]); let last = journal::records(&store).unwrap().pop().unwrap(); assert_eq!( (last.action.as_str(), last.undoes.as_deref()), ("undo", Some(second.as_str())) ); } #[test] fn undo_refuses_when_the_file_changed_since() { let (dir, store) = setup("journal-changed"); let id = write( &store, r#"{"action":"add","file":"user","text":"a"}"#, false, ); std::fs::write(dir.path().join("memory/user.md"), "edited by hand\n").unwrap(); assert_eq!( journal::undo(&store, &id, now()), Err(format!( "user.md has changed since {id}; nothing was undone" )) ); assert_eq!(store.entries(MemoryFile::User).unwrap(), ["edited by hand"]); } #[test] fn review_stages_accepts_and_rejects() { let (_dir, store) = setup("review"); let json = r#"{"action":"add","file":"memory","text":"a"}"#; let args: MemoryArgs = serde_json::from_str(json).unwrap(); let answer = review::submit(&store, &args, json, &ctx(false)).unwrap(); assert_eq!( answer, "waiting for the owner's review (p-2026-09-24-0001); nothing is changed until it is accepted" ); let other = r#"{"action":"add","file":"memory","text":"b"}"#; let args: MemoryArgs = serde_json::from_str(other).unwrap(); review::submit(&store, &args, other, &ctx(false)).unwrap(); assert_eq!( store.entries(MemoryFile::Memory).unwrap(), Vec::::new() ); let listed = review::list(&store).unwrap(); assert_eq!(listed.lines().count(), 2); assert!( listed.starts_with("p-2026-09-24-0001 mm-s1 {"), "{listed}" ); let accepted = review::accept(&store, "p-2026-09-24-0001", now()).unwrap(); assert!( accepted.starts_with("accepted: p-2026-09-24-0001 as 2026-09-24-0001: memory.md updated"), "{accepted}" ); assert_eq!(store.entries(MemoryFile::Memory).unwrap(), ["a"]); assert_eq!( review::reject(&store, "p-2026-09-24-0002", now()), Ok("rejected: p-2026-09-24-0002".to_string()) ); assert_eq!( review::list(&store).unwrap(), "nothing is waiting for review" ); assert_eq!( review::accept(&store, "p-2026-09-24-0002", now()), Err("nothing waiting as p-2026-09-24-0002".to_string()) ); let actions: Vec = journal::records(&store) .unwrap() .into_iter() .map(|r| r.action) .collect(); assert_eq!(actions, ["add", "reject"]); assert_eq!(store.entries(MemoryFile::Memory).unwrap(), ["a"]); }