//! The owner's commands on `memoryd`'s admin socket, end to end (M5a spec, section 10): `log` //! shows the last ten edits unless told how many; `reject` discards a held write and `accept` //! applies one; what an accepted write moves to `notes.md` is found by `recall` at once; a //! reindex counts the notes. They pin `serve/admin.rs`, which M5c-2 task 11 moved out of //! `serve.rs`. Do not edit. #[path = "support/tmp.rs"] mod tmp; use std::os::unix::net::UnixStream; use std::path::Path; use std::sync::Arc; use std::time::Duration; use bxcore::{ CallId, DataClass, Empty, Envelope, MemoryAdmin, MemoryCall, MemoryCommand, Message, SessionId, ToolRequest, ToolResponse, read_frame, write_frame, }; use memoryd::config::Config; use memoryd::serve; use tmp::TempDir; struct Running { config: Config, /// Removed when the test ends; declared last, so it outlives what writes into it (T44). dir: TempDir, } fn start(tag: &str, review: bool) -> Running { let dir = TempDir::new(tag); dir.write("etc/soul.md", "You are Boxmaker.\n"); let text = format!( "[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\nreview = {review}\n", dir.path().display(), dir.path().join("etc/soul.md").display() ); 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()); Running { config, dir } } 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: 9, r#final: true, msg, }; write_frame(&mut stream, &env).unwrap(); read_frame(&mut stream).unwrap().msg } /// A `memory` or `recall` call from a clean session; the answer's text. fn call(r: &Running, tool: &str, arguments: &str) -> String { let msg = Message::MemoryCall(MemoryCall { request: ToolRequest { session: SessionId::new("mm-a").unwrap(), call: CallId(1), tool: tool.to_string(), arguments: arguments.to_string(), }, taint: DataClass::Private, untrusted: false, }); match ask(&r.config.broker_socket(), msg) { Message::ToolResponse(ToolResponse::Result { content, .. }) => content, other => panic!("not a result: {other:?}"), } } fn admin(r: &Running, command: MemoryCommand, arg: &str) -> String { let msg = Message::MemoryAdmin(MemoryAdmin { command, arg: arg.to_string(), }); match ask(&r.config.admin_socket(), msg) { Message::MemoryAdminReply(reply) => reply.text, other => panic!("{other:?}"), } } fn memory_md(r: &Running) -> String { std::fs::read_to_string(r.dir.path().join("memory/memory.md")).unwrap_or_default() } /// The id of the newest write waiting for review. fn newest_pending(r: &Running) -> String { let pending = admin(r, MemoryCommand::Pending, ""); let last = pending.lines().last().unwrap_or_default(); last.split_whitespace() .next() .unwrap_or_default() .to_string() } #[test] fn log_shows_the_last_ten_edits_unless_told_how_many() { let r = start("admin-log", false); for n in 1..=12 { call( &r, "memory", &format!(r#"{{"action":"note","text":"note {n}"}}"#), ); } assert_eq!(admin(&r, MemoryCommand::Log, "").lines().count(), 10); assert_eq!(admin(&r, MemoryCommand::Log, "3").lines().count(), 3); assert_eq!(admin(&r, MemoryCommand::Log, "20").lines().count(), 12); } #[test] fn reject_discards_a_held_write_and_accept_applies_one() { let r = start("admin-review", true); call( &r, "memory", r#"{"action":"add","file":"memory","text":"Rejected fact."}"#, ); let rejected = newest_pending(&r); assert!(admin(&r, MemoryCommand::Reject, &rejected).starts_with("rejected: ")); assert_eq!(memory_md(&r), ""); call( &r, "memory", r#"{"action":"add","file":"memory","text":"Accepted fact."}"#, ); let accepted = newest_pending(&r); assert!(admin(&r, MemoryCommand::Accept, &accepted).starts_with("accepted: ")); assert_eq!(memory_md(&r), "Accepted fact.\n"); } /// An accepted `consolidate` moves an entry to `notes.md`; the admin command indexes it, so /// `recall` finds it at once, as identity moved out of `memory.md`. #[test] fn what_an_accepted_write_moves_to_notes_is_recalled_at_once() { let r = start("admin-notes", true); call( &r, "memory", r#"{"action":"add","file":"memory","text":"Zebra crossings are painted on Mondays."}"#, ); admin(&r, MemoryCommand::Accept, &newest_pending(&r)); assert_eq!( call(&r, "recall", r#"{"query":"zebra"}"#), "(no matches for \"zebra\")" ); call( &r, "memory", r#"{"action":"consolidate","file":"memory","entries":[{"old":"Zebra crossings are painted on Mondays.","move":true}]}"#, ); admin(&r, MemoryCommand::Accept, &newest_pending(&r)); assert_eq!(memory_md(&r), ""); let found = call(&r, "recall", r#"{"query":"zebra"}"#); assert!(found.starts_with("[1] identity ยท moved "), "{found}"); } #[test] fn a_reindex_counts_the_notes() { let r = start("admin-reindex", false); call(&r, "memory", r#"{"action":"note","text":"one note"}"#); let Message::Reindexed(counts) = ask(&r.config.admin_socket(), Message::Reindex(Empty {})) else { panic!("not reindexed") }; assert_eq!((counts.logs, counts.records, counts.notes), (0, 0, 1)); }