//! `bxctl reindex` and `bxctl memory`: parsing, what is sent to `memoryd`, and what is printed //! (M5a spec, section 10). Do not edit. use std::os::unix::net::UnixListener; use std::path::{Path, PathBuf}; use std::sync::mpsc; use std::time::Duration; use bxctl::cli::{Command, UsageError, parse}; use bxctl::memory::{Ask, run}; use proto::{ Envelope, ErrorCode, MemoryAdminReply, MemoryCommand, Message, Reindexed, WireError, read_frame, write_frame, }; fn args(line: &str) -> Vec { line.split_whitespace().map(str::to_string).collect() } fn home() -> PathBuf { PathBuf::from("/srv/bx") } fn memory(ask: Ask) -> Result { Ok(Command::Memory { socket: PathBuf::from("/srv/bx/run/owner-memory/admin.sock"), ask, }) } #[test] fn the_commands_parse() { let h = home(); assert_eq!(parse(&args("reindex"), &h), memory(Ask::Reindex)); assert_eq!( parse(&args("memory log"), &h), memory(Ask::Admin(MemoryCommand::Log, String::new())) ); assert_eq!( parse(&args("memory log 3"), &h), memory(Ask::Admin(MemoryCommand::Log, "3".to_string())) ); assert_eq!( parse(&args("memory undo 2026-09-24-0007"), &h), memory(Ask::Admin( MemoryCommand::Undo, "2026-09-24-0007".to_string() )) ); assert_eq!( parse(&args("memory pending"), &h), memory(Ask::Admin(MemoryCommand::Pending, String::new())) ); assert_eq!( parse(&args("memory accept p-1"), &h), memory(Ask::Admin(MemoryCommand::Accept, "p-1".to_string())) ); assert_eq!( parse(&args("memory reject p-1"), &h), memory(Ask::Admin(MemoryCommand::Reject, "p-1".to_string())) ); assert_eq!( parse(&args("reindex --memory-socket /r/m.sock"), &h), Ok(Command::Memory { socket: PathBuf::from("/r/m.sock"), ask: Ask::Reindex }) ); assert_eq!( parse(&args("memory --memory-socket /r/m.sock undo x"), &h), Ok(Command::Memory { socket: PathBuf::from("/r/m.sock"), ask: Ask::Admin(MemoryCommand::Undo, "x".to_string()) }) ); } #[test] fn bad_lines_are_usage_errors() { for line in [ "memory", "memory log ten", "memory log 1 2", "memory undo", "memory undo a b", "memory forget x", "memory pending x", "reindex now", "reindex --memory-socket", "reindex --memory-socket /a --memory-socket /b", ] { assert_eq!(parse(&args(line), &home()), Err(UsageError), "{line}"); } } /// A fake `memoryd` answering one request with `answer`; hands back what it was sent. fn fake(socket: &Path, answer: Message) -> mpsc::Receiver { let listener = UnixListener::bind(socket).unwrap(); let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { let (mut stream, _) = listener.accept().unwrap(); let env = read_frame(&mut stream).unwrap(); tx.send(env.msg).unwrap(); let reply = Envelope { v: 1, id: env.id, r#final: true, msg: answer, }; write_frame(&mut stream, &reply).unwrap(); }); rx } fn dir(tag: &str) -> PathBuf { let path = std::env::temp_dir().join(format!("bx-mem-{tag}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&path); std::fs::create_dir_all(&path).unwrap(); path } #[test] fn reindex_prints_the_counts() { let d = dir("reindex"); let socket = d.join("admin.sock"); let seen = fake( &socket, Message::Reindexed(Reindexed { logs: 42, records: 1208, notes: 7, }), ); let mut out = Vec::new(); run(&socket, &Ask::Reindex, Duration::from_secs(5), &mut out).unwrap(); assert_eq!( String::from_utf8(out).unwrap(), "reindexed: 42 logs, 1,208 records, 7 notes\n" ); assert!(matches!(seen.recv().unwrap(), Message::Reindex(_))); } #[test] fn an_admin_command_prints_the_reply_and_an_error_is_escaped() { let d = dir("admin"); let socket = d.join("admin.sock"); let seen = fake( &socket, Message::MemoryAdminReply(MemoryAdminReply { text: "undone: 2026-09-24-0007".to_string(), }), ); let mut out = Vec::new(); let ask = Ask::Admin(MemoryCommand::Undo, "2026-09-24-0007".to_string()); run(&socket, &ask, Duration::from_secs(5), &mut out).unwrap(); assert_eq!(String::from_utf8(out).unwrap(), "undone: 2026-09-24-0007\n"); match seen.recv().unwrap() { Message::MemoryAdmin(m) => { assert_eq!( (m.command, m.arg.as_str()), (MemoryCommand::Undo, "2026-09-24-0007") ) } other => panic!("{other:?}"), } let socket = d.join("refuses.sock"); let _seen = fake( &socket, Message::Error(WireError { code: ErrorCode::Internal, detail: "user.md has changed\u{1b}[31m".to_string(), }), ); let err = run(&socket, &ask, Duration::from_secs(5), &mut Vec::new()).unwrap_err(); assert!(err.starts_with("memoryd: user.md has changed"), "{err}"); assert!(!err.contains('\u{1b}'), "escaped: {err}"); let err = run( &d.join("nobody.sock"), &ask, Duration::from_secs(1), &mut Vec::new(), ) .unwrap_err(); assert!(err.starts_with("cannot reach memoryd at "), "{err}"); }