//! `memoryd` answering `recall` with `[honcho]` (M5c spec, section 8), through its `brokerd` //! socket, against a fake Honcho: the local index's hits, then the read workspace's and the //! fleet's, the answer untrusted when a fleet hit is shown (T58); a hanging source holds up no //! memory call, because the state lock is released before Honcho is asked; without `[honcho]`, //! `recall` answers as before. Every wait has a deadline. Do not edit. #[path = "support/honcho.rs"] mod honcho; #[path = "support/logs.rs"] mod logs; #[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, MemoryCall, Message, SessionId, ToolRequest, ToolResponse, read_frame, write_frame, }; use honcho::{FakeHoncho, Mode}; use logs::{user, write_log}; use memoryd::config::Config; use memoryd::serve; use tmp::TempDir; /// `[honcho]` for `url`, reading `boxmaker1`'s workspace, labelled trusted. fn section(url: &str) -> String { format!( "[honcho]\nurl = \"{url}\"\nagent = \"boxmaker0\"\nowner = \"kyle\"\ntrusted = \"boxmaker0-trusted\"\nuntrusted = \"boxmaker0-untrusted\"\nfleet = \"fleet\"\n[[honcho.read]]\nagent = \"boxmaker1\"\nworkspace = \"boxmaker1-trusted\"\ntrust = \"trusted\"\n" ) } struct Home { config: Config, /// Removed when the test ends; declared last, so it outlives what writes into it (T44). _dir: TempDir, } impl Home { /// A home whose index holds one line the owner said, `tea time`; with `[honcho]` at `url` /// when there is one. fn new(tag: &str, url: Option<&str>) -> Home { let dir = TempDir::new(tag); let soul = dir.write("etc/soul.md", "You are Boxmaker.\n"); write_log(dir.path(), "mm-a", &[user(1, "tea time")]); let text = format!( "[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n{}", dir.path().display(), soul.display(), url.map(section).unwrap_or_default() ); Home { config: Config::parse(&text).unwrap(), _dir: dir, } } /// `serve::start`, run on a thread; its log lines are collected. fn serve(&self) -> Arc>> { 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(self.config.clone(), log).unwrap(); std::thread::spawn(move || running.run()); lines } } /// One call on `brokerd`'s socket and its answer, within 20 s; how long it took. fn call(socket: &Path, tool: &str, arguments: &str) -> (ToolResponse, Duration) { call_at(socket, tool, arguments, DataClass::Private) } /// `call`, with the session's class on the `MemoryCall`. fn call_at( socket: &Path, tool: &str, arguments: &str, taint: DataClass, ) -> (ToolResponse, Duration) { let started = Instant::now(); let mut stream = UnixStream::connect(socket).unwrap(); stream .set_read_timeout(Some(Duration::from_secs(20))) .unwrap(); let env = Envelope { v: 1, id: 3, r#final: true, msg: Message::MemoryCall(MemoryCall { request: ToolRequest { session: SessionId::new("mm-now").unwrap(), call: CallId(3), tool: tool.to_string(), arguments: arguments.to_string(), }, taint, untrusted: false, }), }; write_frame(&mut stream, &env).unwrap(); match read_frame(&mut stream).unwrap().msg { Message::ToolResponse(response) => (response, started.elapsed()), other => panic!("{other:?}"), } } fn result(response: ToolResponse) -> (String, DataClass, bool) { match response { ToolResponse::Result { content, class, untrusted, .. } => (content, class, untrusted), other => panic!("{other:?}"), } } /// The paths of the fake's search and conclusion-query requests. fn reads(fake: &FakeHoncho) -> Vec { fake.requests() .into_iter() .map(|r| r.path) .filter(|p| p.ends_with("/search") || p.ends_with("/conclusions/query")) .collect() } /// T58, end to end: a fleet hit shown makes `memoryd`'s answer untrusted, as `brokerd` will label /// the session (task 02); hits only from a workspace labelled trusted do not. #[test] fn a_fleet_hit_makes_the_answer_untrusted() { let fake = FakeHoncho::start(); let home = Home::new("recall-serve-fleet", Some(&fake.url)); let _lines = home.serve(); let socket = home.config.broker_socket(); let (content, class, untrusted) = result(call(&socket, "recall", r#"{"query":"tea","k":10}"#).0); assert!( content.starts_with("[1] memory · session mm-a, 2026-09-20 14:01 UTC, the owner said · private\n tea time\n[2] memory · boxmaker1: kyle said, "), "{content}" ); assert!( content.contains("\n[3] memory-tainted · fleet: kyle said, "), "{content}" ); // The conclusions about the owner's peer, `[honcho] owner`, are asked for and shown. assert!( content.contains("memory · boxmaker1: a conclusion about kyle, "), "{content}" ); assert_eq!((class, untrusted), (DataClass::Private, true)); assert!( !reads(&fake).iter().any(|p| p.contains("boxmaker0")), "{:?}", reads(&fake) ); fake.set_workspace("fleet", Mode::Missing); let (content, _, untrusted) = result(call(&socket, "recall", r#"{"query":"tea","k":10}"#).0); assert!(!content.contains("fleet"), "{content}"); assert!(!untrusted, "{content}"); } /// `recall` releases the state lock before it asks Honcho: while a source hangs, a memory call is /// answered at once, and the `recall` answers after the source's limit with its `unavailable` /// line. #[test] fn a_hanging_source_holds_up_no_memory_call() { let fake = FakeHoncho::start(); fake.set_workspace("fleet", Mode::Hang); let home = Home::new("recall-serve-hang", Some(&fake.url)); let lines = home.serve(); let socket = home.config.broker_socket(); let for_recall = socket.clone(); let recall = std::thread::spawn(move || call(&for_recall, "recall", r#"{"query":"tea"}"#)); let deadline = Instant::now() + Duration::from_secs(10); while !reads(&fake) .iter() .any(|p| p == "/v3/workspaces/fleet/search") { assert!(Instant::now() < deadline, "the fleet was never searched"); std::thread::sleep(Duration::from_millis(10)); } let (answer, took) = call( &socket, "memory", r#"{"action":"note","text":"tea is kept in the blue tin"}"#, ); assert!(matches!(answer, ToolResponse::Result { .. }), "{answer:?}"); assert!( took < Duration::from_secs(2), "the memory call waited {took:?}" ); assert!(!recall.is_finished(), "the recall is still waiting"); let (answer, took) = recall.join().unwrap(); let (content, _, untrusted) = result(answer); assert!(content.ends_with("\nfleet: unavailable"), "{content}"); assert!(took < Duration::from_secs(15), "{took:?}"); assert!(!untrusted, "no fleet hit was shown: {content}"); assert!( lines .lock() .unwrap() .iter() .any(|l| l.starts_with("memoryd: recall: fleet unavailable: ")), "{:?}", lines.lock().unwrap() ); } /// Nothing found anywhere: the sentence local `recall` gives. #[test] fn nothing_found_anywhere() { let fake = FakeHoncho::start(); let home = Home::new("recall-serve-nothing", Some(&fake.url)); let _lines = home.serve(); let (content, class, untrusted) = result( call( &home.config.broker_socket(), "recall", r#"{"query":"zebra"}"#, ) .0, ); assert_eq!(content, "(no matches for \"zebra\")"); assert_eq!((class, untrusted), (DataClass::Private, false)); } /// Without `[honcho]`, `recall` is the local index's, and nothing else is asked. #[test] fn without_honcho_recall_answers_as_before() { let home = Home::new("recall-serve-off", None); let lines = home.serve(); let socket = home.config.broker_socket(); let (content, class, untrusted) = result(call(&socket, "recall", r#"{"query":"tea"}"#).0); assert_eq!( content, "[1] memory · session mm-a, 2026-09-20 14:01 UTC, the owner said · private\n tea time" ); assert_eq!((class, untrusted), (DataClass::Private, false)); let (content, _, _) = result(call(&socket, "recall", r#"{"query":"zebra"}"#).0); assert_eq!(content, "(no matches for \"zebra\")"); assert!( lines.lock().unwrap().is_empty(), "{:?}", lines.lock().unwrap() ); } /// Spec 13: a `secret` session's query still reaches Honcho; only a query holding a secret is /// refused. #[test] fn a_secret_session_still_asks_honcho() { let fake = FakeHoncho::start(); let home = Home::new("recall-serve-secret", Some(&fake.url)); let _lines = home.serve(); let (content, class, untrusted) = result( call_at( &home.config.broker_socket(), "recall", r#"{"query":"tea","k":10}"#, DataClass::Secret, ) .0, ); assert!(!reads(&fake).is_empty(), "Honcho was not asked: {content}"); assert!(content.contains("tea"), "{content}"); assert_eq!((class, untrusted), (DataClass::Private, true)); }