//! A memory write wakes the Honcho worker (M5c spec, section 4: a write reaches Honcho "at once"; //! review of M5c-1, finding 3): with the worker asleep for its 30 s idle wait, the memory tool's //! conclusion and the owner's undo's deletion both reach the fake Honcho within 5 s. #[path = "support/honcho.rs"] mod honcho; #[path = "support/tmp.rs"] mod tmp; use std::os::unix::net::UnixStream; use std::path::Path; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use bxcore::{ CallId, DataClass, Envelope, MemoryAdmin, MemoryCall, MemoryCommand, Message, SessionId, ToolRequest, read_frame, write_frame, }; use honcho::FakeHoncho; use memoryd::config::Config; use memoryd::honcho::worker::Tuning; use memoryd::serve; use tmp::TempDir; fn ask(socket: &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: 1, r#final: true, msg, }; write_frame(&mut stream, &env).unwrap(); read_frame(&mut stream).unwrap().msg } /// How many requests of `method` to a path ending `suffix` the fake has had. fn count(fake: &FakeHoncho, method: &str, suffix: &str) -> usize { fake.requests() .iter() .filter(|r| { r.method == method && r.path.contains("/conclusions") && r.path.ends_with(suffix) }) .count() } /// Waits up to 5 s for `done`; true if it came. fn within_5s(done: impl Fn() -> bool) -> bool { let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { if done() { return true; } std::thread::sleep(Duration::from_millis(10)); } done() } #[test] fn a_memory_write_and_an_undo_wake_the_worker() { // The worker `serve` starts waits this long when idle: far past every deadline here. assert!(Tuning::DEFAULT.idle >= Duration::from_secs(30)); let fake = FakeHoncho::start(); let dir = TempDir::new("wake-serve"); let soul = dir.write("etc/soul.md", "You are Boxmaker.\n"); let text = format!( "[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n[honcho]\nurl = \"{}\"\nagent = \"boxmaker0\"\nowner = \"kyle\"\ntrusted = \"boxmaker0-trusted\"\nuntrusted = \"boxmaker0-untrusted\"\nfleet = \"fleet\"\n", dir.path().display(), soul.display(), fake.url ); let config = Config::parse(&text).unwrap(); let lines = Arc::new(Mutex::new(Vec::new())); let sink = Arc::clone(&lines); let log: serve::Log = Arc::new(move |l: &str| sink.lock().unwrap().push(l.to_string())); let running = serve::start(config.clone(), log).unwrap(); std::thread::spawn(move || running.run()); // Let the worker find nothing to send and fall asleep. std::thread::sleep(Duration::from_millis(200)); 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, }); assert!(matches!( ask(&config.broker_socket(), call), Message::ToolResponse(_) )); assert!( within_5s(|| count(&fake, "POST", "/conclusions") == 1), "the conclusion waited: {:?} {:?}", fake.requests(), lines.lock().unwrap() ); std::thread::sleep(Duration::from_millis(200)); let log = ask( &config.admin_socket(), Message::MemoryAdmin(MemoryAdmin { command: MemoryCommand::Log, arg: String::new(), }), ); let Message::MemoryAdminReply(reply) = log else { panic!("{log:?}") }; let id = reply.text.split(" ").next().unwrap().to_string(); let undone = ask( &config.admin_socket(), Message::MemoryAdmin(MemoryAdmin { command: MemoryCommand::Undo, arg: id, }), ); assert!(matches!(undone, Message::MemoryAdminReply(_)), "{undone:?}"); assert!( within_5s(|| count(&fake, "DELETE", "") == 1), "the deletion waited: {:?} {:?}", fake.requests(), lines.lock().unwrap() ); }