295 lines
8.9 KiB
Rust
295 lines
8.9 KiB
Rust
//! 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(peer: &str, text: &str) -> Action {
|
|
Action::Unconclude {
|
|
workspace: Workspace::Trusted,
|
|
peer: peer.to_string(),
|
|
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(OWNER_PEER, "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(AGENT, "gone")]);
|
|
let reorder = record(
|
|
"e2",
|
|
"consolidate",
|
|
Some("memory.md"),
|
|
&["a", "b"],
|
|
&["b", "a"],
|
|
);
|
|
assert!(of(&reorder).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn a_deletion_names_the_peer_of_its_file() {
|
|
// The same text in both files: each file's removal deletes its own peer's conclusion.
|
|
let user = record("e1", "remove", Some("user.md"), &["Uses tabs."], &[]);
|
|
assert_eq!(of(&user), vec![unconclude(OWNER_PEER, "Uses tabs.")]);
|
|
let memory = record("e2", "remove", Some("memory.md"), &["Uses tabs."], &[]);
|
|
assert_eq!(of(&memory), vec![unconclude(AGENT, "Uses tabs.")]);
|
|
}
|
|
|
|
#[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(AGENT, "old"),
|
|
unconclude(AGENT, "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(OWNER_PEER, "added")],
|
|
),
|
|
(
|
|
&["x", "new"][..],
|
|
&["x", "old"][..],
|
|
vec![unconclude(OWNER_PEER, "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(OWNER_PEER, "new"),
|
|
Action::Skip {
|
|
why: NO_SESSION.to_string()
|
|
}
|
|
]
|
|
);
|
|
}
|