//! The memory messages against byte-exact fixtures, each refusing an unknown key: the eight of M5a //! (M5a spec, section 7) and the session-state question of M5c (M5c spec, section 7). Do not edit //! these or the fixtures. use bxcore::{ CallId, DataClass, Empty, Envelope, MemoryAdmin, MemoryAdminReply, MemoryBlockReply, MemoryCall, MemoryCommand, Message, Reindexed, SessionId, SessionState, SessionStateReply, SessionUpdated, ToolRequest, }; const SESSION: &str = "mm-r0000000000000000000000001"; fn fixture(name: &str) -> String { let path = format!("{}/tests/fixtures/wire/{name}", env!("CARGO_MANIFEST_DIR")); let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}")); text.trim_end_matches('\n').to_string() } /// The fixture must decode to `msg`, `msg` must encode to exactly the fixture's bytes, and the /// body must refuse an extra key. fn check(name: &str, id: u64, msg: Message) { let want = Envelope { v: 1, id, r#final: true, msg, }; let text = fixture(name); let got: Envelope = serde_json::from_str(&text).unwrap_or_else(|e| panic!("{name}: {e}")); assert_eq!(got, want, "{name}: decoded value"); assert_eq!( serde_json::to_string(&want).unwrap(), text, "{name}: encoded bytes" ); let extra = text.replacen("\"body\":{", "\"body\":{\"extra\":1,", 1); assert!( serde_json::from_str::(&extra).is_err(), "{name}: an unknown key is refused" ); } #[test] fn the_eight_messages() { check("memory_block.json", 1, Message::MemoryBlock(Empty {})); check( "memory_block_reply.json", 1, Message::MemoryBlockReply(MemoryBlockReply { text: "# soul.md\nYou are Boxmaker.\n\n# memory.md (0 of 2,200 characters)\n(empty)\n\n# user.md (0 of 1,375 characters)\n(empty)".to_string(), }), ); check( "session_updated.json", 1, Message::SessionUpdated(SessionUpdated { session: SessionId::new(SESSION).unwrap(), turn: 3, }), ); check( "memory_call.json", 3, Message::MemoryCall(MemoryCall { request: ToolRequest { session: SessionId::new(SESSION).unwrap(), call: CallId(3), tool: "memory".to_string(), arguments: r#"{"action":"note","text":"example.org says March."}"#.to_string(), }, taint: DataClass::Private, untrusted: true, }), ); check("reindex.json", 1, Message::Reindex(Empty {})); check( "reindexed.json", 1, Message::Reindexed(Reindexed { logs: 42, records: 1208, notes: 7, }), ); check( "memory_admin.json", 1, Message::MemoryAdmin(MemoryAdmin { command: MemoryCommand::Undo, arg: "2026-09-24-0007".to_string(), }), ); check( "memory_admin_reply.json", 1, Message::MemoryAdminReply(MemoryAdminReply { text: "undone: 2026-09-24-0007".to_string(), }), ); } /// `turn` has no default: a `session_updated` without it, as a `loopd` from before M5c sent, is /// refused, never read as turn 0 (M5c spec, section 5). #[test] fn a_session_updated_without_turn_is_refused() { let old = r#"{"v":1,"id":1,"final":true,"msg":{"kind":"session_updated","body":{"session":"mm-r0000000000000000000000001"}}}"#; let error = serde_json::from_str::(old).unwrap_err(); assert!( error.to_string().contains("missing field `turn`"), "{error}" ); } /// `memoryd` asks `brokerd` for a session's state on `state.sock` (M5c spec, section 7). #[test] fn the_session_state_messages() { check( "session_state.json", 1, Message::SessionState(SessionState { session: SessionId::new(SESSION).unwrap(), }), ); check( "session_state_reply.json", 1, Message::SessionStateReply(SessionStateReply { class: DataClass::Private, untrusted: true, }), ); } /// Neither field of the reply has a default: a reply without one is refused, never read as a /// clean or `public` session. #[test] fn a_session_state_reply_needs_both_fields() { let text = fixture("session_state_reply.json"); for (field, without) in [ ("class", text.replacen(r#""class":"private","#, "", 1)), ("untrusted", text.replacen(r#","untrusted":true"#, "", 1)), ] { assert_ne!(without, text, "{field}: the fixture holds the field"); let error = serde_json::from_str::(&without).unwrap_err(); assert!( error .to_string() .contains(&format!("missing field `{field}`")), "{field}: {error}" ); } } #[test] fn every_command_has_its_name() { for (command, name) in [ (MemoryCommand::Log, "log"), (MemoryCommand::Undo, "undo"), (MemoryCommand::Pending, "pending"), (MemoryCommand::Accept, "accept"), (MemoryCommand::Reject, "reject"), ] { assert_eq!( serde_json::to_string(&command).unwrap(), format!("\"{name}\"") ); } }