236 lines
6.8 KiB
Rust
236 lines
6.8 KiB
Rust
//! What `bxctl memory sync` prints (M5c spec, section 5): the ledger's counts, and each failed
|
|
//! item, oldest first, by its label, its kind and its status, exactly; the same after a restart;
|
|
//! and `memoryd` answering it, with `[honcho]` and without. Do not edit.
|
|
|
|
#[path = "support/tmp.rs"]
|
|
mod tmp;
|
|
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use bxcore::{
|
|
CallId, DataClass, Envelope, MemoryAdmin, MemoryCall, MemoryCommand, Message, SessionId,
|
|
Timestamp, ToolRequest, read_frame, write_frame,
|
|
};
|
|
use memoryd::config::Config;
|
|
use memoryd::honcho::ledger::{Action, Failure, Item, Ledger, Record, Workspace, label};
|
|
use memoryd::honcho::report::{NOT_CONFIGURED, report};
|
|
use memoryd::serve;
|
|
use tmp::TempDir;
|
|
|
|
fn at(minute: u32) -> Timestamp {
|
|
Timestamp::parse(&format!("2026-09-28T10:{minute:02}:00.000Z")).unwrap()
|
|
}
|
|
|
|
fn turn(n: u64) -> Item {
|
|
Item::Turn {
|
|
session: SessionId::new("mm-a").unwrap(),
|
|
turn: n,
|
|
}
|
|
}
|
|
|
|
fn edit(id: &str) -> Item {
|
|
Item::Edit {
|
|
id: id.to_string(),
|
|
part: 0,
|
|
}
|
|
}
|
|
|
|
fn to_trusted() -> Action {
|
|
Action::Turn {
|
|
workspace: Workspace::Trusted,
|
|
speaker: "kyle".to_string(),
|
|
}
|
|
}
|
|
|
|
/// A ledger with every state: 2 sent, 1 waiting, 2 failed (the later-decided one failing first),
|
|
/// 1 skipped.
|
|
fn mixed(dir: &TempDir) -> Ledger {
|
|
let mut l = Ledger::open(&dir.path().join("honcho.jsonl")).unwrap();
|
|
let decided = |item: Item, action: Action| Record::Decided {
|
|
time: at(0),
|
|
item,
|
|
action,
|
|
};
|
|
let records = vec![
|
|
decided(turn(1), to_trusted()),
|
|
decided(turn(2), to_trusted()),
|
|
decided(
|
|
edit("2026-09-28-0001"),
|
|
Action::Conclude {
|
|
workspace: Workspace::Trusted,
|
|
peer: "kyle".to_string(),
|
|
session: SessionId::new("mm-a").unwrap(),
|
|
text: "a private fact".to_string(),
|
|
},
|
|
),
|
|
decided(
|
|
turn(3),
|
|
Action::Skip {
|
|
why: "the turn has no single speaker".to_string(),
|
|
},
|
|
),
|
|
decided(turn(4), to_trusted()),
|
|
decided(turn(5), to_trusted()),
|
|
decided(turn(6), to_trusted()),
|
|
Record::Sent {
|
|
time: at(1),
|
|
item: turn(1),
|
|
ids: vec!["m1".to_string()],
|
|
},
|
|
Record::Failed {
|
|
time: at(2),
|
|
item: turn(4),
|
|
status: None,
|
|
kind: "log".to_string(),
|
|
},
|
|
Record::Failed {
|
|
time: at(3),
|
|
item: edit("2026-09-28-0001"),
|
|
status: Some(422),
|
|
kind: "status".to_string(),
|
|
},
|
|
Record::Sent {
|
|
time: at(4),
|
|
item: turn(2),
|
|
ids: vec!["m2".to_string()],
|
|
},
|
|
];
|
|
for r in records {
|
|
l.append(r).unwrap();
|
|
}
|
|
l
|
|
}
|
|
|
|
const MIXED: &str = "honcho sync: 7 decided: 2 sent, 2 waiting, 2 failed, 1 skipped
|
|
failed, oldest first:
|
|
2026-09-28T10:02:00.000Z turn 4 of session mm-a log
|
|
2026-09-28T10:03:00.000Z part 0 of edit 2026-09-28-0001 status (HTTP 422)";
|
|
|
|
#[test]
|
|
fn the_report_is_exactly_this() {
|
|
let dir = TempDir::new("sync-report");
|
|
let l = mixed(&dir);
|
|
assert_eq!(report(&l), MIXED);
|
|
assert!(!report(&l).contains("private"), "no text is printed");
|
|
let empty = Ledger::open(&dir.path().join("none.jsonl")).unwrap();
|
|
assert_eq!(
|
|
report(&empty),
|
|
"honcho sync: 0 decided: 0 sent, 0 waiting, 0 failed, 0 skipped"
|
|
);
|
|
assert_eq!(
|
|
NOT_CONFIGURED,
|
|
"honcho sync: not configured (memoryd.toml has no [honcho])"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_failures_are_oldest_first_and_survive_a_restart() {
|
|
let dir = TempDir::new("sync-failures");
|
|
let l = mixed(&dir);
|
|
let expected = vec![
|
|
Failure {
|
|
item: turn(4),
|
|
time: at(2),
|
|
status: None,
|
|
kind: "log".to_string(),
|
|
},
|
|
Failure {
|
|
item: edit("2026-09-28-0001"),
|
|
time: at(3),
|
|
status: Some(422),
|
|
kind: "status".to_string(),
|
|
},
|
|
];
|
|
assert_eq!(l.failures(), expected);
|
|
let again = Ledger::open(&dir.path().join("honcho.jsonl")).unwrap();
|
|
assert_eq!(again.failures(), expected);
|
|
assert_eq!(report(&again), MIXED);
|
|
assert_eq!(label(&turn(4)), "turn 4 of session mm-a");
|
|
assert_eq!(label(&edit("e")), "part 0 of edit e");
|
|
}
|
|
|
|
const SECTION: &str = "[honcho]
|
|
url = \"http://127.0.0.1:9\"
|
|
agent = \"boxmaker0\"
|
|
owner = \"kyle\"
|
|
trusted = \"boxmaker0-trusted\"
|
|
untrusted = \"boxmaker0-untrusted\"
|
|
fleet = \"fleet\"
|
|
";
|
|
|
|
fn ask(socket: &std::path::Path, msg: Message) -> Message {
|
|
let mut stream = UnixStream::connect(socket).unwrap();
|
|
stream
|
|
.set_read_timeout(Some(Duration::from_secs(10)))
|
|
.unwrap();
|
|
let env = Envelope {
|
|
v: 1,
|
|
id: 5,
|
|
r#final: true,
|
|
msg,
|
|
};
|
|
write_frame(&mut stream, &env).unwrap();
|
|
read_frame(&mut stream).unwrap().msg
|
|
}
|
|
|
|
fn sync(config: &Config) -> String {
|
|
let msg = Message::MemoryAdmin(MemoryAdmin {
|
|
command: MemoryCommand::Sync,
|
|
arg: String::new(),
|
|
});
|
|
match ask(&config.admin_socket(), msg) {
|
|
Message::MemoryAdminReply(reply) => reply.text,
|
|
other => panic!("{other:?}"),
|
|
}
|
|
}
|
|
|
|
fn serve(dir: &TempDir, honcho: bool) -> Config {
|
|
let soul = dir.write("etc/soul.md", "You are Boxmaker.\n");
|
|
let text = format!(
|
|
"[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n{}",
|
|
dir.path().display(),
|
|
soul.display(),
|
|
if honcho { SECTION } else { "" }
|
|
);
|
|
let config = Config::parse(&text).unwrap();
|
|
let log: serve::Log = Arc::new(|_: &str| {});
|
|
let running = serve::start(config.clone(), log).unwrap();
|
|
std::thread::spawn(move || running.run());
|
|
config
|
|
}
|
|
|
|
#[test]
|
|
fn memoryd_answers_with_the_report() {
|
|
let dir = TempDir::new("sync-serve");
|
|
let config = serve(&dir, true);
|
|
assert_eq!(
|
|
sync(&config),
|
|
"honcho sync: 0 decided: 0 sent, 0 waiting, 0 failed, 0 skipped"
|
|
);
|
|
let call = Message::MemoryCall(MemoryCall {
|
|
request: ToolRequest {
|
|
session: SessionId::new("mm-a").unwrap(),
|
|
call: CallId(1),
|
|
tool: "memory".to_string(),
|
|
arguments: r#"{"action":"add","file":"user","text":"Prefers UTC."}"#.to_string(),
|
|
},
|
|
taint: DataClass::Private,
|
|
untrusted: false,
|
|
});
|
|
ask(&config.broker_socket(), call);
|
|
// Honcho is not reachable here: the conclusion waits.
|
|
assert_eq!(
|
|
sync(&config),
|
|
"honcho sync: 1 decided: 0 sent, 1 waiting, 0 failed, 0 skipped"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn without_honcho_it_is_not_configured() {
|
|
let dir = TempDir::new("sync-off");
|
|
let config = serve(&dir, false);
|
|
assert_eq!(sync(&config), NOT_CONFIGURED);
|
|
}
|