Files
boxmaker/crates/memoryd/tests/honcho_ledger.rs
T

452 lines
17 KiB
Rust

//! The Honcho ledger, `memory/honcho.jsonl` (M5c spec, section 5): its lines, exactly, and the
//! rules of `append`, the outbox, the sent ids, the live conclusions and the counts. Reading the
//! file back is in `honcho_ledger_file.rs`. Do not edit.
#[path = "support/ledger.rs"]
mod ledger;
#[path = "support/tmp.rs"]
mod tmp;
use ledger::{
Home, conclude, decided, edit, failed, sent, sid, skip, to_trusted, turn, unconclude,
};
use memoryd::honcho::ledger::{Action, Counts, Item, LedgerError, Record, Workspace};
/// One record of each kind, and of each action, with its line exactly.
fn examples() -> Vec<(Record, &'static str)> {
vec![
(
decided(&turn("chat-1", 3), to_trusted("kyle")),
r#"{"type":"decided","time":"2026-09-28T10:00:00.000Z","item":{"type":"turn","session":"chat-1","turn":3},"action":{"type":"turn","workspace":"trusted","speaker":"kyle"}}"#,
),
(
decided(
&turn("chat-2", 1),
Action::Turn {
workspace: Workspace::Untrusted,
speaker: "ana_b".to_string(),
},
),
r#"{"type":"decided","time":"2026-09-28T10:00:00.000Z","item":{"type":"turn","session":"chat-2","turn":1},"action":{"type":"turn","workspace":"untrusted","speaker":"ana_b"}}"#,
),
(
decided(
&edit("2026-09-28-0001", 0),
conclude(Workspace::Trusted, "Kyle takes his coffee black."),
),
r#"{"type":"decided","time":"2026-09-28T10:00:00.000Z","item":{"type":"edit","id":"2026-09-28-0001","part":0},"action":{"type":"conclude","workspace":"trusted","peer":"kyle","session":"chat-1","text":"Kyle takes his coffee black."}}"#,
),
(
decided(
&edit("2026-09-28-0002", 1),
unconclude(Workspace::Trusted, "Kyle takes his coffee black."),
),
r#"{"type":"decided","time":"2026-09-28T10:00:00.000Z","item":{"type":"edit","id":"2026-09-28-0002","part":1},"action":{"type":"unconclude","workspace":"trusted","peer":"kyle","text":"Kyle takes his coffee black."}}"#,
),
(
decided(
&edit("2026-09-28-0003", 0),
Action::Fleet {
session: sid("chat-2"),
text: "The build host is \"straylight\".\nIt runs NixOS.".to_string(),
},
),
r#"{"type":"decided","time":"2026-09-28T10:00:00.000Z","item":{"type":"edit","id":"2026-09-28-0003","part":0},"action":{"type":"fleet","session":"chat-2","text":"The build host is \"straylight\".\nIt runs NixOS."}}"#,
),
(
decided(
&edit("2026-09-28-0004", 0),
Action::Retract {
session: sid("chat-2"),
text: "The build host is titan.".to_string(),
},
),
r#"{"type":"decided","time":"2026-09-28T10:00:00.000Z","item":{"type":"edit","id":"2026-09-28-0004","part":0},"action":{"type":"retract","session":"chat-2","text":"The build host is titan."}}"#,
),
(
decided(&turn("chat-3", 7), skip("the session is secret")),
r#"{"type":"decided","time":"2026-09-28T10:00:00.000Z","item":{"type":"turn","session":"chat-3","turn":7},"action":{"type":"skip","why":"the session is secret"}}"#,
),
(
sent(&turn("chat-1", 3), &["msg-a1", "msg-a2"]),
r#"{"type":"sent","time":"2026-09-28T10:00:00.000Z","item":{"type":"turn","session":"chat-1","turn":3},"ids":["msg-a1","msg-a2"]}"#,
),
(
failed(&edit("2026-09-28-0001", 0), Some(422)),
r#"{"type":"failed","time":"2026-09-28T10:00:00.000Z","item":{"type":"edit","id":"2026-09-28-0001","part":0},"status":422,"kind":"refused"}"#,
),
(
failed(&turn("chat-2", 1), None),
r#"{"type":"failed","time":"2026-09-28T10:00:00.000Z","item":{"type":"turn","session":"chat-2","turn":1},"status":null,"kind":"refused"}"#,
),
]
}
#[test]
fn each_record_is_one_line_exactly_as_given() {
for (record, text) in examples() {
assert_eq!(serde_json::to_string(&record).unwrap(), text);
assert_eq!(
serde_json::from_str::<Record>(text).unwrap(),
record,
"{text}"
);
}
for (workspace, text) in [
(Workspace::Trusted, "\"trusted\""),
(Workspace::Untrusted, "\"untrusted\""),
(Workspace::Fleet, "\"fleet\""),
] {
assert_eq!(serde_json::to_string(&workspace).unwrap(), text);
assert_eq!(serde_json::from_str::<Workspace>(text).unwrap(), workspace);
}
}
#[test]
fn append_writes_the_line_and_a_newline() {
let home = Home::new("ledger-append");
let mut ledger = home.open();
let (first, second) = (examples().remove(0), examples().remove(7));
ledger.append(first.0).unwrap();
assert_eq!(home.text(), format!("{}\n", first.1));
ledger.append(second.0).unwrap();
assert_eq!(home.text(), format!("{}\n{}\n", first.1, second.1));
}
#[test]
fn an_unknown_field_or_value_is_refused() {
for (_, text) in examples() {
let mut broken = vec![format!("{},\"extra\":1}}", &text[..text.len() - 1])];
broken.push(text.replacen("\"item\":{", "\"item\":{\"extra\":1,", 1));
if text.contains("\"action\":{") {
broken.push(text.replacen("\"action\":{", "\"action\":{\"extra\":1,", 1));
}
for line in broken {
let err = serde_json::from_str::<Record>(&line).unwrap_err();
assert!(
err.to_string().contains("unknown field `extra`"),
"{line}: {err}"
);
}
}
let (_, text) = examples().remove(0);
for (from, to) in [
("\"trusted\"", "\"shared\""),
("\"type\":\"decided\"", "\"type\":\"queued\""),
(
"\"type\":\"turn\",\"session\"",
"\"type\":\"call\",\"session\"",
),
("\"session\":\"chat-1\"", "\"session\":\"Chat 1\""),
(
"\"time\":\"2026-09-28T10:00:00.000Z\"",
"\"time\":\"today\"",
),
] {
assert_eq!(text.matches(from).count(), 1, "{from}");
let line = text.replace(from, to);
assert!(serde_json::from_str::<Record>(&line).is_err(), "{line}");
}
}
#[test]
fn the_outbox_is_what_is_decided_and_not_skipped_sent_or_failed() {
let home = Home::new("ledger-outbox");
let mut ledger = home.open();
let (a, b, c, d, e) = (
turn("chat-1", 1),
turn("chat-1", 2),
edit("2026-09-28-0001", 0),
edit("2026-09-28-0002", 0),
turn("chat-1", 3),
);
ledger.append(decided(&a, to_trusted("kyle"))).unwrap();
ledger.append(decided(&b, skip("no speaker"))).unwrap();
ledger
.append(decided(&c, conclude(Workspace::Trusted, "one")))
.unwrap();
ledger
.append(decided(&d, conclude(Workspace::Trusted, "two")))
.unwrap();
ledger.append(decided(&e, to_trusted("ana_b"))).unwrap();
ledger.append(sent(&c, &["c-1"])).unwrap();
ledger.append(failed(&d, Some(400))).unwrap();
assert_eq!(
ledger.outbox(),
vec![
(a.clone(), to_trusted("kyle")),
(e.clone(), to_trusted("ana_b"))
]
);
for item in [&a, &b, &c, &d, &e] {
assert!(ledger.is_decided(item), "{item:?}");
}
assert!(!ledger.is_decided(&turn("chat-1", 4)));
assert!(!ledger.is_decided(&edit("2026-09-28-0001", 1)));
}
#[test]
fn the_outbox_keeps_the_order_of_decision() {
let home = Home::new("ledger-order");
let mut ledger = home.open();
let items = [
turn("chat-b", 2),
turn("chat-a", 9),
edit("2026-09-28-0002", 0),
turn("chat-b", 1),
edit("2026-09-28-0001", 3),
edit("2026-09-28-0001", 0),
];
for item in &items {
ledger.append(decided(item, skip("placeholder"))).unwrap();
}
assert!(ledger.outbox().is_empty());
let home = Home::new("ledger-order-2");
let mut ledger = home.open();
for item in &items {
ledger.append(decided(item, to_trusted("kyle"))).unwrap();
}
let order: Vec<Item> = ledger.outbox().into_iter().map(|(item, _)| item).collect();
assert_eq!(order, items.to_vec());
ledger.append(sent(&items[1], &["m"])).unwrap();
let order: Vec<Item> = ledger.outbox().into_iter().map(|(item, _)| item).collect();
let mut left = items.to_vec();
left.remove(1);
assert_eq!(order, left);
}
#[test]
fn a_second_decision_is_refused_and_writes_nothing() {
let home = Home::new("ledger-twice");
let mut ledger = home.open();
let item = turn("chat-1", 1);
ledger.append(decided(&item, to_trusted("kyle"))).unwrap();
let before = home.text();
for action in [to_trusted("kyle"), skip("again")] {
let err = ledger.append(decided(&item, action)).unwrap_err();
assert!(
matches!(&err, LedgerError::AlreadyDecided(i) if *i == item),
"{err:?}"
);
assert_eq!(err.to_string(), "turn 1 of session chat-1: decided already");
}
assert_eq!(home.text(), before);
assert_eq!(ledger.outbox(), vec![(item, to_trusted("kyle"))]);
assert_eq!(ledger.counts().decided, 1);
}
#[test]
fn sent_or_failed_before_the_decision_is_refused() {
let home = Home::new("ledger-undecided");
let mut ledger = home.open();
let item = edit("2026-09-28-0001", 2);
for record in [sent(&item, &["x"]), failed(&item, Some(404))] {
let err = ledger.append(record).unwrap_err();
assert!(
matches!(&err, LedgerError::NotWaiting(i) if *i == item),
"{err:?}"
);
assert_eq!(
err.to_string(),
"part 2 of edit 2026-09-28-0001: not waiting to be sent"
);
}
assert!(!home.path.exists(), "a refused append wrote the file");
assert_eq!(ledger.counts(), Counts::default());
assert_eq!(ledger.sent_ids(&item), None);
}
#[test]
fn an_item_is_sent_or_failed_once_and_never_when_skipped() {
let home = Home::new("ledger-once");
let mut ledger = home.open();
let (s, f, k) = (turn("chat-1", 1), turn("chat-1", 2), turn("chat-1", 3));
ledger.append(decided(&s, to_trusted("kyle"))).unwrap();
ledger.append(decided(&f, to_trusted("kyle"))).unwrap();
ledger.append(decided(&k, skip("secret"))).unwrap();
ledger.append(sent(&s, &["m1", "m2"])).unwrap();
ledger.append(failed(&f, None)).unwrap();
let before = home.text();
for record in [
sent(&s, &["m3"]),
failed(&s, Some(500)),
sent(&f, &["m4"]),
failed(&f, Some(400)),
sent(&k, &["m5"]),
failed(&k, None),
] {
let err = ledger.append(record.clone()).unwrap_err();
assert!(
matches!(err, LedgerError::NotWaiting(_)),
"{record:?}: {err:?}"
);
}
assert_eq!(home.text(), before);
assert_eq!(
ledger.sent_ids(&s),
Some(&["m1".to_string(), "m2".to_string()][..])
);
}
#[test]
fn sent_ids_are_those_of_the_sent_record() {
let home = Home::new("ledger-ids");
let mut ledger = home.open();
let (waiting, done, refused, skipped) = (
turn("chat-1", 1),
turn("chat-1", 2),
turn("chat-1", 3),
turn("chat-1", 4),
);
for item in [&waiting, &done, &refused] {
ledger.append(decided(item, to_trusted("kyle"))).unwrap();
}
ledger
.append(decided(&skipped, skip("no speaker")))
.unwrap();
ledger.append(sent(&done, &["m-1", "m-2"])).unwrap();
ledger.append(failed(&refused, Some(413))).unwrap();
assert_eq!(
ledger.sent_ids(&done),
Some(&["m-1".to_string(), "m-2".to_string()][..])
);
for item in [&waiting, &refused, &skipped, &turn("chat-9", 1)] {
assert_eq!(ledger.sent_ids(item), None, "{item:?}");
}
}
#[test]
fn a_conclusion_is_the_latest_one_not_undone() {
let home = Home::new("ledger-latest");
let mut ledger = home.open();
let text = "Kyle lives in Oakland.";
let (first, second, undo1, undo2) = (
edit("2026-09-28-0001", 0),
edit("2026-09-28-0002", 0),
edit("2026-09-28-0003", 0),
edit("2026-09-28-0004", 0),
);
ledger
.append(decided(&first, conclude(Workspace::Trusted, text)))
.unwrap();
assert_eq!(ledger.conclusion(Workspace::Trusted, "kyle", text), None);
ledger.append(sent(&first, &["c-1"])).unwrap();
let found = |id: &str, item: &Item| Some((item.clone(), id.to_string()));
assert_eq!(
ledger.conclusion(Workspace::Trusted, "kyle", text),
found("c-1", &first)
);
ledger
.append(decided(&second, conclude(Workspace::Trusted, text)))
.unwrap();
ledger.append(sent(&second, &["c-2"])).unwrap();
assert_eq!(
ledger.conclusion(Workspace::Trusted, "kyle", text),
found("c-2", &second)
);
ledger
.append(decided(&undo1, unconclude(Workspace::Trusted, text)))
.unwrap();
assert_eq!(
ledger.conclusion(Workspace::Trusted, "kyle", text),
found("c-2", &second)
);
ledger.append(sent(&undo1, &[])).unwrap();
assert_eq!(
ledger.conclusion(Workspace::Trusted, "kyle", text),
found("c-1", &first)
);
ledger
.append(decided(&undo2, unconclude(Workspace::Trusted, text)))
.unwrap();
ledger.append(sent(&undo2, &[])).unwrap();
assert_eq!(ledger.conclusion(Workspace::Trusted, "kyle", text), None);
// The latest is the one sent last, not the one decided last.
let (early, late) = (edit("2026-09-28-0005", 0), edit("2026-09-28-0006", 0));
for item in [&early, &late] {
ledger
.append(decided(item, conclude(Workspace::Trusted, "y")))
.unwrap();
}
ledger.append(sent(&late, &["c-late"])).unwrap();
ledger.append(sent(&early, &["c-early"])).unwrap();
assert_eq!(
ledger.conclusion(Workspace::Trusted, "kyle", "y"),
found("c-early", &early)
);
}
#[test]
fn a_conclusion_is_found_only_by_its_workspace_and_text_once_sent() {
let home = Home::new("ledger-where");
let mut ledger = home.open();
let (sent_one, unsent, empty, other, undo_elsewhere, undo_unsent) = (
edit("2026-09-28-0001", 0),
edit("2026-09-28-0002", 0),
edit("2026-09-28-0003", 0),
edit("2026-09-28-0004", 0),
edit("2026-09-28-0005", 0),
edit("2026-09-28-0006", 0),
);
let actions = [
(&sent_one, conclude(Workspace::Trusted, "a")),
(&unsent, conclude(Workspace::Trusted, "b")),
(&empty, conclude(Workspace::Trusted, "c")),
(&other, conclude(Workspace::Untrusted, "a")),
(&undo_elsewhere, unconclude(Workspace::Untrusted, "a")),
(&undo_unsent, unconclude(Workspace::Trusted, "a")),
];
for (item, action) in actions {
ledger.append(decided(item, action)).unwrap();
}
ledger.append(sent(&sent_one, &["c-a"])).unwrap();
ledger.append(sent(&empty, &[])).unwrap();
ledger.append(sent(&other, &["c-other"])).unwrap();
ledger.append(sent(&undo_elsewhere, &[])).unwrap();
let a = Some((sent_one.clone(), "c-a".to_string()));
assert_eq!(ledger.conclusion(Workspace::Trusted, "kyle", "a"), a);
assert_eq!(ledger.conclusion(Workspace::Trusted, "kyle", "b"), None);
assert_eq!(ledger.conclusion(Workspace::Trusted, "kyle", "c"), None);
assert_eq!(ledger.conclusion(Workspace::Trusted, "kyle", "A"), None);
assert_eq!(ledger.conclusion(Workspace::Untrusted, "kyle", "a"), None);
assert_eq!(ledger.conclusion(Workspace::Fleet, "kyle", "a"), None);
ledger.append(sent(&undo_unsent, &[])).unwrap();
assert_eq!(ledger.conclusion(Workspace::Trusted, "kyle", "a"), None);
}
#[test]
fn the_counts_add_up() {
let home = Home::new("ledger-counts");
let mut ledger = home.open();
assert_eq!(ledger.counts(), Counts::default());
let items: Vec<Item> = (1..=9).map(|n| turn("chat-1", n)).collect();
for (n, item) in items.iter().enumerate() {
let action = if n < 2 {
skip("secret")
} else {
to_trusted("kyle")
};
ledger.append(decided(item, action)).unwrap();
}
for item in &items[2..5] {
ledger.append(sent(item, &["m"])).unwrap();
}
ledger.append(failed(&items[5], Some(400))).unwrap();
let counts = ledger.counts();
assert_eq!(
counts,
Counts {
decided: 9,
sent: 3,
waiting: 3,
failed: 1,
skipped: 2,
}
);
assert_eq!(
counts.decided,
counts.sent + counts.waiting + counts.failed + counts.skipped
);
assert_eq!(ledger.outbox().len(), counts.waiting);
}