M5c-2 plan: task 03, brokerd's wait for ask_memory

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-28 17:39:39 -07:00
co-authored by Claude Opus 5.5
parent 77a7e8854b
commit 73a94e5106
2 changed files with 398 additions and 0 deletions
@@ -0,0 +1,131 @@
//! How long `brokerd` waits for `memoryd`, per tool: 30 s for `recall` and `memory`, 170 s for
//! `ask_memory`, whose two Honcho chats have 150 s each (M5c spec, sections 9 and 11). The waits
//! are shortened here with `with_ask_wait` and `new`'s own wait, so no test waits long. Do not
//! edit.
#[path = "support/build.rs"]
mod build;
#[path = "support/tmp.rs"]
mod tmp;
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::time::Duration;
use brokerd::args::ToolName;
use brokerd::memory_port::{ASK_MEMORY_WAIT, MEMORY_WAIT, MemoryPort, UNREACHABLE};
use brokerd::policy::decide;
use brokerd::runner::run;
use build::{allowed, now, private, request, set};
use bxcore::{DataClass, Envelope, Message, ToolRequest, ToolResponse, read_frame, write_frame};
use tmp::TempDir;
/// How long the slow fake `memoryd` takes to answer.
const SLOW: Duration = Duration::from_millis(1_500);
/// A wait shorter than `SLOW`, and one far longer.
const SHORT: Duration = Duration::from_millis(300);
const LONG: Duration = Duration::from_secs(20);
fn found() -> ToolResponse {
ToolResponse::Result {
content: "own: memory · The owner prefers UTC.".to_string(),
class: DataClass::Private,
untrusted: false,
truncated: false,
}
}
/// A fake `memoryd` that answers one call after `SLOW`. A failed write is ignored: the port has
/// stopped waiting, which is what some tests expect.
fn slow_memoryd(socket: &Path) {
let listener = UnixListener::bind(socket).unwrap();
std::thread::spawn(move || {
let Ok((mut stream, _)) = listener.accept() else {
return;
};
let Ok(env) = read_frame(&mut stream) else {
return;
};
std::thread::sleep(SLOW);
let reply = Envelope {
v: 1,
id: env.id,
r#final: true,
msg: Message::ToolResponse(found()),
};
let _ = write_frame(&mut stream, &reply);
});
}
/// One allowed call of `req` through `port`, to a slow fake `memoryd` of its own.
fn ask(
dir: &TempDir,
name: &str,
port: impl Fn(&Path) -> MemoryPort,
req: ToolRequest,
) -> ToolResponse {
let socket = dir.path().join(name);
slow_memoryd(&socket);
let decision = allowed(decide(req, &set(vec![]), private(), now()));
run(decision, &port(&socket))
}
fn unreachable() -> ToolResponse {
ToolResponse::Failed {
message: UNREACHABLE.to_string(),
}
}
const QUESTION: &str = r#"{"question":"What does the owner prefer?"}"#;
const QUERY: &str = r#"{"query":"owner"}"#;
#[test]
fn the_waits_and_which_tool_gets_which() {
assert_eq!(MEMORY_WAIT, Duration::from_secs(30));
assert_eq!(ASK_MEMORY_WAIT, Duration::from_secs(170));
let port = MemoryPort::new("memory.sock".into(), MEMORY_WAIT);
assert_eq!(port.wait_for(ToolName::Recall), MEMORY_WAIT);
assert_eq!(port.wait_for(ToolName::Memory), MEMORY_WAIT);
assert_eq!(port.wait_for(ToolName::AskMemory), ASK_MEMORY_WAIT);
let port = MemoryPort::new("memory.sock".into(), SHORT);
assert_eq!(port.wait_for(ToolName::Recall), SHORT);
assert_eq!(port.wait_for(ToolName::Memory), SHORT);
assert_eq!(port.wait_for(ToolName::AskMemory), ASK_MEMORY_WAIT);
let port = MemoryPort::new("memory.sock".into(), MEMORY_WAIT).with_ask_wait(LONG);
assert_eq!(port.wait_for(ToolName::Recall), MEMORY_WAIT);
assert_eq!(port.wait_for(ToolName::Memory), MEMORY_WAIT);
assert_eq!(port.wait_for(ToolName::AskMemory), LONG);
}
/// A slow answer reaches `ask_memory`, whose wait is long, and not `recall` or `memory`, whose
/// wait is short: the wait is chosen by the call's tool.
#[test]
fn a_slow_answer_reaches_ask_memory_only_when_its_wait_is_longer() {
let dir = TempDir::new("wait-ask");
let port = |socket: &Path| MemoryPort::new(socket.to_path_buf(), SHORT).with_ask_wait(LONG);
assert_eq!(
ask(&dir, "a.sock", port, request("ask_memory", QUESTION)),
found()
);
assert_eq!(
ask(&dir, "r.sock", port, request("recall", QUERY)),
unreachable()
);
let note = r#"{"action":"note","text":"x"}"#;
assert_eq!(
ask(&dir, "m.sock", port, request("memory", note)),
unreachable()
);
}
/// And the other way round: a short wait for `ask_memory` gives up, while `recall` waits.
#[test]
fn a_slow_answer_reaches_recall_only_when_its_wait_is_longer() {
let dir = TempDir::new("wait-recall");
let port = |socket: &Path| MemoryPort::new(socket.to_path_buf(), LONG).with_ask_wait(SHORT);
assert_eq!(ask(&dir, "r.sock", port, request("recall", QUERY)), found());
assert_eq!(
ask(&dir, "a.sock", port, request("ask_memory", QUESTION)),
unreachable()
);
}