187 lines
6.4 KiB
Rust
187 lines
6.4 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.
|
|
pub fn temp_socket(name: &str) -> PathBuf {
|
|
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();
|
|
dir.join(name)
|
|
}
|
|
|
|
pub fn ts(text: &str) -> Timestamp {
|
|
Timestamp::parse(text).unwrap()
|
|
}
|
|
|
|
pub struct FakeBrokerd {
|
|
pub socket: PathBuf,
|
|
/// 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: PathBuf,
|
|
/// 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 }
|
|
}
|