memoryd: memory writes decided into the Honcho ledger as they are journalled
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
//! 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<String> {
|
||||
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<Action> {
|
||||
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()
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user