About 10,000 test leftovers had built up in titan's /tmp. loopd's Home and FakeServer, its fake broker's socket, bxctl's temp sockets, fake loopd and command homes, inferproxy's socket, and a few brokerd and toolkit tests created uniquely named directories or sockets and never removed them, or removed them by hand at the end, which a failing test skips. Each is now a guard removed on drop (bxctl gains tests/support/temp.rs). Two races also left gatewayd homes behind: the test Running only signalled its server to stop, and two Stage structs declared the home first, so it was removed while the server could still write into it. Running's drop now waits for the thread (with a 5-second limit), and every home is declared last. Checked: make gate twice, then the gate and three rounds of gatewayd and loopd tests; no new entry in /tmp; 944 tests pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
213 lines
6.9 KiB
Rust
213 lines
6.9 KiB
Rust
//! Scripted stand-ins for `brokerd`'s admin socket and for `loopd`, for tests. Do not edit.
|
|
|
|
#![allow(dead_code)] // each test file uses a different part of this module
|
|
|
|
use proto::{
|
|
ApprovalList, CallId, DataClass, Envelope, ErrorCode, Message, PROTOCOL_VERSION,
|
|
PendingApproval, SessionId, Timestamp, TurnDone, TurnEvent, Usage, WireError, read_frame,
|
|
write_frame,
|
|
};
|
|
use std::os::unix::net::UnixListener;
|
|
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
|
|
static NEXT: AtomicU32 = AtomicU32::new(0);
|
|
|
|
/// A socket path in a fresh temporary directory, which is removed when the value is dropped
|
|
/// (tip T44).
|
|
pub struct TempSocket {
|
|
dir: PathBuf,
|
|
path: PathBuf,
|
|
}
|
|
|
|
impl std::ops::Deref for TempSocket {
|
|
type Target = std::path::Path;
|
|
fn deref(&self) -> &std::path::Path {
|
|
&self.path
|
|
}
|
|
}
|
|
|
|
impl AsRef<std::path::Path> for TempSocket {
|
|
fn as_ref(&self) -> &std::path::Path {
|
|
&self.path
|
|
}
|
|
}
|
|
|
|
impl Drop for TempSocket {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.dir);
|
|
}
|
|
}
|
|
|
|
pub fn temp_socket(name: &str) -> TempSocket {
|
|
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
|
let dir = std::env::temp_dir().join(format!("bxctl-m3a-{}-{n}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join(name);
|
|
TempSocket { dir, path }
|
|
}
|
|
|
|
pub fn ts(text: &str) -> Timestamp {
|
|
Timestamp::parse(text).unwrap()
|
|
}
|
|
|
|
pub struct FakeBrokerd {
|
|
pub socket: TempSocket,
|
|
/// Every request message received, in order.
|
|
pub requests: Arc<Mutex<Vec<Message>>>,
|
|
}
|
|
|
|
impl FakeBrokerd {
|
|
pub fn requests(&self) -> Vec<Message> {
|
|
self.requests.lock().unwrap().clone()
|
|
}
|
|
}
|
|
|
|
/// Serves `admin.sock`: one request per connection, answered with the frames `answer` returns,
|
|
/// written exactly as given (so a test can send a wrong id or a frame that is not final). An
|
|
/// empty list closes the connection without an answer.
|
|
pub fn fake_brokerd_frames(
|
|
answer: impl Fn(&Envelope) -> Vec<Envelope> + Send + 'static,
|
|
) -> FakeBrokerd {
|
|
let socket = temp_socket("admin.sock");
|
|
let listener = UnixListener::bind(&socket).unwrap();
|
|
let requests = Arc::new(Mutex::new(Vec::new()));
|
|
let seen = Arc::clone(&requests);
|
|
thread::spawn(move || {
|
|
for stream in listener.incoming() {
|
|
let Ok(mut stream) = stream else { continue };
|
|
let Ok(request) = read_frame(&mut stream) else {
|
|
continue;
|
|
};
|
|
seen.lock().unwrap().push(request.msg.clone());
|
|
for frame in answer(&request) {
|
|
if write_frame(&mut stream, &frame).is_err() {
|
|
break; // the client went away; the next connection is still served
|
|
}
|
|
}
|
|
}
|
|
});
|
|
FakeBrokerd { socket, requests }
|
|
}
|
|
|
|
/// The usual case: one final frame with the request's id.
|
|
pub fn fake_brokerd(answer: impl Fn(&Message) -> Message + Send + 'static) -> FakeBrokerd {
|
|
fake_brokerd_frames(move |request| {
|
|
vec![Envelope {
|
|
v: PROTOCOL_VERSION,
|
|
id: request.id,
|
|
r#final: true,
|
|
msg: answer(&request.msg),
|
|
}]
|
|
})
|
|
}
|
|
|
|
pub fn wire_error(code: ErrorCode, detail: &str) -> Message {
|
|
Message::Error(WireError {
|
|
code,
|
|
detail: detail.to_string(),
|
|
})
|
|
}
|
|
|
|
/// A pending approval created at 12:00:00 that expires at 12:15:00 on 2026-09-18.
|
|
pub fn pending(approval: u64, tool: &str, arguments: &str) -> PendingApproval {
|
|
PendingApproval {
|
|
approval,
|
|
session: SessionId::new("chat-1758196800-123456789").unwrap(),
|
|
call: CallId(7),
|
|
tool: tool.to_string(),
|
|
arguments: arguments.to_string(),
|
|
grant: "shell-scratch".to_string(),
|
|
taint: DataClass::Private,
|
|
created: ts("2026-09-18T12:00:00.000Z"),
|
|
expires: ts("2026-09-18T12:15:00.000Z"),
|
|
}
|
|
}
|
|
|
|
/// A `brokerd` with these approvals pending. `approve` is answered with `outcome`, `refuse` with
|
|
/// `ok`; both answer `no_such_approval` for an id that is not in the list.
|
|
pub fn brokerd_with(items: Vec<PendingApproval>, outcome: proto::DecisionRecord) -> FakeBrokerd {
|
|
fake_brokerd(move |msg| {
|
|
let known = |id: u64| items.iter().any(|item| item.approval == id);
|
|
match msg {
|
|
Message::Approvals(_) => Message::ApprovalList(ApprovalList {
|
|
items: items.clone(),
|
|
}),
|
|
Message::Approve(a) if known(a.approval) => {
|
|
Message::ApproveResult(proto::ApproveResult {
|
|
outcome: outcome.clone(),
|
|
})
|
|
}
|
|
Message::Refuse(r) if known(r.approval) => Message::Ok(proto::Empty {}),
|
|
Message::Approve(_) | Message::Refuse(_) => {
|
|
wire_error(ErrorCode::NoSuchApproval, "no such approval")
|
|
}
|
|
_ => wire_error(ErrorCode::BadMessage, "not an admin request"),
|
|
}
|
|
})
|
|
}
|
|
|
|
pub struct FakeLoopd {
|
|
pub socket: TempSocket,
|
|
/// The content of every turn received, in order.
|
|
pub turns: Arc<Mutex<Vec<String>>>,
|
|
}
|
|
|
|
pub fn usage() -> Usage {
|
|
Usage {
|
|
cache_n: 10,
|
|
prompt_n: 5,
|
|
predicted_n: 7,
|
|
reasoning_tokens: 3,
|
|
thinking_capped: false,
|
|
}
|
|
}
|
|
|
|
/// Serves `loop.sock`: every turn gets the same events, then `turn_done` with `answer`. It does
|
|
/// not wait for an approval to be answered: `bxctl` deals with each event before it reads the
|
|
/// next frame, so the order of what it does is fixed all the same.
|
|
pub fn fake_loopd(events: Vec<TurnEvent>, answer: &str) -> FakeLoopd {
|
|
let socket = temp_socket("loop.sock");
|
|
let listener = UnixListener::bind(&socket).unwrap();
|
|
let turns = Arc::new(Mutex::new(Vec::new()));
|
|
let seen = Arc::clone(&turns);
|
|
let answer = answer.to_string();
|
|
thread::spawn(move || {
|
|
for stream in listener.incoming() {
|
|
let Ok(mut stream) = stream else { continue };
|
|
let Ok(request) = read_frame(&mut stream) else {
|
|
continue;
|
|
};
|
|
let Message::Turn(turn) = request.msg else {
|
|
continue;
|
|
};
|
|
seen.lock().unwrap().push(turn.content);
|
|
let mut frames: Vec<(bool, Message)> = events
|
|
.iter()
|
|
.map(|e| (false, Message::TurnEvent(e.clone())))
|
|
.collect();
|
|
frames.push((
|
|
true,
|
|
Message::TurnDone(TurnDone {
|
|
content: answer.clone(),
|
|
usage: usage(),
|
|
}),
|
|
));
|
|
for (last, msg) in frames {
|
|
let frame = Envelope {
|
|
v: PROTOCOL_VERSION,
|
|
id: request.id,
|
|
r#final: last,
|
|
msg,
|
|
};
|
|
if write_frame(&mut stream, &frame).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
FakeLoopd { socket, turns }
|
|
}
|