Done the way the owner asked and the spike mapped: the crate and the manifests renamed, then what the build reported fixed. Layers: 138 "proto unresolved" errors across 169 files; cargo fmt reordering imports; six fixture paths built as strings in brokerd's and bxctl's tests, which the compiler cannot see, and comments naming crates/proto/; the crate-dependency check and its self-test. One more the spike did not reach: a `\nproto =` key inside a printf format in the self-test, which a word-boundary replacement skips because `n` and `p` are both word characters. make gate: gate: ok, 944 tests; no new entry in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
189 lines
6.0 KiB
Rust
189 lines
6.0 KiB
Rust
//! The edit journal, `log`, `undo`, and review (M5a spec, section 5). Do not edit.
|
|
|
|
#[path = "support/tmp.rs"]
|
|
mod tmp;
|
|
|
|
use bxcore::{DataClass, MemoryArgs, MemoryFile, Timestamp};
|
|
use memoryd::edit::{Context, apply};
|
|
use memoryd::files::Store;
|
|
use memoryd::{journal, review};
|
|
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::<String>::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<String> = journal::records(&store)
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|r| r.action)
|
|
.collect();
|
|
assert_eq!(actions, ["add", "reject"]);
|
|
assert_eq!(store.entries(MemoryFile::Memory).unwrap(), ["a"]);
|
|
}
|