Add bxctl approvals, approve, refuse and grants check

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-20 23:25:05 -07:00
parent 469be2c0a1
commit 2d94067b52
12 changed files with 1780 additions and 79 deletions
+497
View File
@@ -0,0 +1,497 @@
//! Tests for `bxctl`'s admin client and the commands built on it, against a fake `brokerd`.
//! Do not edit.
mod support;
use bxctl::admin::{
AdminError, cmd_approvals, cmd_approve, cmd_grants_check, cmd_refuse, list, reason_name,
request, write_block,
};
use proto::{
Approve, DecisionRecord, DenyReason, Empty, Envelope, ErrorCode, GrantProblem, GrantsReport,
Message, PROTOCOL_VERSION, Refuse,
};
use std::process::Command;
use support::{brokerd_with, fake_brokerd, fake_brokerd_frames, pending, ts, wire_error};
const BACKSLASH: char = '\\';
/// The escape for one code point, built and never spelled out (see `tests/escape.rs`).
fn esc(code: u32) -> String {
format!("{BACKSLASH}u{code:04x}")
}
fn text(out: Vec<u8>) -> String {
String::from_utf8(out).unwrap()
}
const ALLOWED: DecisionRecord = DecisionRecord::Allowed {};
// ---- the client ----
#[test]
fn request_sends_one_final_frame_with_id_1_and_returns_the_answer() {
let fake = fake_brokerd_frames(|request| {
assert_eq!(request.v, PROTOCOL_VERSION);
assert_eq!(request.id, 1);
assert!(request.r#final);
vec![Envelope {
v: PROTOCOL_VERSION,
id: 1,
r#final: true,
msg: Message::Ok(Empty {}),
}]
});
let answer = request(&fake.socket, Message::CheckGrants(Empty {})).unwrap();
assert_eq!(answer, Message::Ok(Empty {}));
assert_eq!(fake.requests(), vec![Message::CheckGrants(Empty {})]);
}
#[test]
fn an_error_frame_is_refused_with_its_code_and_detail() {
let fake = fake_brokerd(|_| wire_error(ErrorCode::Forbidden, "not on this socket"));
match request(&fake.socket, Message::Approvals(Empty {})) {
Err(AdminError::Refused(w)) => {
assert_eq!(w.code, ErrorCode::Forbidden);
assert_eq!(w.detail, "not on this socket");
}
other => panic!("{other:?}"),
}
let e = request(&fake.socket, Message::Approvals(Empty {})).unwrap_err();
assert_eq!(e.to_string(), "forbidden: not on this socket");
}
#[test]
fn an_answer_that_is_not_final_or_has_another_id_is_a_protocol_error() {
let not_final = fake_brokerd_frames(|request| {
vec![Envelope {
v: PROTOCOL_VERSION,
id: request.id,
r#final: false,
msg: Message::Ok(Empty {}),
}]
});
assert!(matches!(
request(&not_final.socket, Message::Approvals(Empty {})),
Err(AdminError::Protocol(_))
));
let other_id = fake_brokerd_frames(|request| {
vec![Envelope {
v: PROTOCOL_VERSION,
id: request.id + 1,
r#final: true,
msg: Message::Ok(Empty {}),
}]
});
assert!(matches!(
request(&other_id.socket, Message::Approvals(Empty {})),
Err(AdminError::Protocol(_))
));
}
#[test]
fn a_connection_closed_without_an_answer_is_a_frame_error() {
let fake = fake_brokerd_frames(|_| Vec::new());
assert!(matches!(
request(&fake.socket, Message::Approvals(Empty {})),
Err(AdminError::Frame(_))
));
}
#[test]
fn no_brokerd_is_a_connect_error_that_names_the_socket() {
let missing = support::temp_socket("nobody-listens.sock");
let e = request(&missing, Message::Approvals(Empty {})).unwrap_err();
assert!(matches!(e, AdminError::Connect(_, _)), "{e:?}");
let message = e.to_string();
assert!(message.starts_with("cannot reach brokerd at "), "{message}");
assert!(message.contains(missing.to_str().unwrap()), "{message}");
}
#[test]
fn list_returns_the_items_and_rejects_any_other_kind() {
let items = vec![pending(41, "shell", "{}"), pending(44, "read_file", "{}")];
let fake = brokerd_with(items.clone(), ALLOWED);
assert_eq!(list(&fake.socket).unwrap(), items);
assert_eq!(fake.requests(), vec![Message::Approvals(Empty {})]);
let wrong = fake_brokerd(|_| Message::Ok(Empty {}));
assert!(matches!(list(&wrong.socket), Err(AdminError::Protocol(_))));
}
#[test]
fn reason_names_are_the_wire_names() {
for reason in [
DenyReason::NoGrant,
DenyReason::GrantExpired,
DenyReason::TaintTooHigh,
DenyReason::DeniedByGrant,
DenyReason::ApprovalRefused,
DenyReason::ApprovalExpired,
DenyReason::GrantsInvalid,
DenyReason::AuditUnavailable,
DenyReason::InvalidArguments,
DenyReason::StateUnreadable,
] {
let wire = serde_json::to_string(&reason).unwrap();
assert_eq!(format!("\"{}\"", reason_name(reason)), wire);
}
}
// ---- the block ----
fn block(item: &proto::PendingApproval, now: &str) -> String {
let mut out = Vec::new();
write_block(&mut out, item, ts(now)).unwrap();
text(out)
}
#[test]
fn the_block_is_two_lines_in_this_exact_form() {
let item = pending(
41,
"shell",
r#"{"command":"rm -rf /home/kyle/scratch/build","cwd":"/home/kyle/scratch"}"#,
);
assert_eq!(
block(&item, "2026-09-18T12:02:00.000Z"),
concat!(
"41 2 min ago expires in 13 min session chat-1758… grant shell-scratch taint private\n",
" shell {\"command\":\"rm -rf /home/kyle/scratch/build\",\"cwd\":\"/home/kyle/scratch\"}\n",
)
);
}
#[test]
fn times_are_whole_seconds_minutes_or_hours_rounded_down() {
let item = pending(41, "shell", "{}");
let first = |now: &str| block(&item, now).lines().next().unwrap().to_string();
// created 12:00:00, expires 12:15:00
assert!(first("2026-09-18T12:00:00.000Z").starts_with("41 0 s ago expires in 15 min "));
assert!(first("2026-09-18T12:00:59.999Z").starts_with("41 59 s ago expires in 14 min "));
assert!(first("2026-09-18T12:01:00.000Z").starts_with("41 1 min ago expires in 14 min "));
assert!(first("2026-09-18T12:14:30.000Z").starts_with("41 14 min ago expires in 30 s "));
// At or after `expires` there is nothing left to wait for.
assert!(first("2026-09-18T12:15:00.000Z").starts_with("41 15 min ago expired "));
assert!(first("2026-09-18T14:05:00.000Z").starts_with("41 2 h ago expired "));
// A clock that is behind the broker's must not underflow.
assert!(first("2026-09-18T11:59:00.000Z").starts_with("41 0 s ago expires in 16 min "));
let mut long = pending(41, "shell", "{}");
long.expires = ts("2026-09-18T15:30:00.000Z");
assert!(block(&long, "2026-09-18T12:00:00.000Z").starts_with("41 0 s ago expires in 3 h "));
}
#[test]
fn a_session_id_longer_than_ten_characters_is_cut_to_nine_and_an_ellipsis() {
let mut item = pending(41, "shell", "{}");
for (id, shown) in [
("s1", "session s1 "),
("abcdefghij", "session abcdefghij "),
("abcdefghijk", "session abcdefghi… "),
] {
item.session = proto::SessionId::new(id).unwrap();
let got = block(&item, "2026-09-18T12:00:00.000Z");
assert!(got.contains(shown), "{id}: {got}");
}
}
#[test]
fn taint_is_the_wire_name() {
let mut item = pending(41, "shell", "{}");
item.taint = proto::DataClass::Secret;
assert!(block(&item, "2026-09-18T12:00:00.000Z").contains(" taint secret\n"));
}
/// Whatever `brokerd` sends is printed as data: tool, grant and arguments all go through
/// `escape_json_text`.
#[test]
fn nothing_in_the_block_reaches_the_terminal_raw() {
let rlo = char::from_u32(0x202e).unwrap();
let zwsp = char::from_u32(0x200b).unwrap();
let isolate = char::from_u32(0x2066).unwrap();
let arguments =
format!("{{\"path\":\"/home/kyle/notes/{rlo}dm.terces{zwsp}{isolate}\x1b[8m\x1b[2J\"}}");
let mut item = pending(41, "read\x1b[1mfile", &arguments);
item.grant = "notes\nread".to_string();
let got = block(&item, "2026-09-18T12:00:00.000Z");
for c in ['\x1b', rlo, zwsp, isolate] {
assert!(!got.contains(c), "U+{:04X} in {got:?}", u32::from(c));
}
assert_eq!(got.matches('\n').count(), 2, "still two lines: {got:?}");
assert!(
got.contains(&format!(
"/home/kyle/notes/{}dm.terces{}{}{}[8m{}[2J",
esc(0x202e),
esc(0x200b),
esc(0x2066),
esc(0x1b),
esc(0x1b)
)),
"{got:?}"
);
assert!(
got.contains(&format!(" read{}[1mfile ", esc(0x1b))),
"{got:?}"
);
assert!(
got.contains(&format!("grant notes{}read ", esc(0x0a))),
"{got:?}"
);
}
// ---- the commands ----
#[test]
fn approvals_prints_one_block_per_item_in_the_order_given() {
let fake = brokerd_with(
vec![
pending(41, "shell", r#"{"command":"ls"}"#),
pending(44, "read_file", r#"{"path":"/home/kyle/notes/a.md"}"#),
],
ALLOWED,
);
let mut out = Vec::new();
let ok = cmd_approvals(&fake.socket, ts("2026-09-18T12:02:00.000Z"), &mut out).unwrap();
assert!(ok);
let got = text(out);
let lines: Vec<&str> = got.lines().collect();
assert_eq!(lines.len(), 4, "{got}");
assert!(lines[0].starts_with("41 2 min ago "), "{got}");
assert_eq!(lines[1], r#" shell {"command":"ls"}"#);
assert!(lines[2].starts_with("44 2 min ago "), "{got}");
assert_eq!(
lines[3],
r#" read_file {"path":"/home/kyle/notes/a.md"}"#
);
}
#[test]
fn approvals_with_nothing_pending_says_so() {
let fake = brokerd_with(Vec::new(), ALLOWED);
let mut out = Vec::new();
assert!(cmd_approvals(&fake.socket, ts("2026-09-18T12:00:00.000Z"), &mut out).unwrap());
assert_eq!(text(out), "no pending approvals\n");
}
#[test]
fn approve_reports_the_re_decision() {
// `ask` and `allowed` both let the call run; only a denial stops it.
for (outcome, line, ok) in [
(DecisionRecord::Allowed {}, "approved 41: runs\n", true),
(DecisionRecord::Ask {}, "approved 41: runs\n", true),
(
DecisionRecord::Denied {
reason: DenyReason::NoGrant,
},
"approved 41: denied (no_grant)\n",
false,
),
(
DecisionRecord::Denied {
reason: DenyReason::TaintTooHigh,
},
"approved 41: denied (taint_too_high)\n",
false,
),
(
DecisionRecord::Denied {
reason: DenyReason::AuditUnavailable,
},
"approved 41: denied (audit_unavailable)\n",
false,
),
] {
let fake = brokerd_with(vec![pending(41, "shell", "{}")], outcome);
let mut out = Vec::new();
assert_eq!(cmd_approve(&fake.socket, 41, &mut out).unwrap(), ok);
assert_eq!(text(out), line);
assert_eq!(
fake.requests(),
vec![Message::Approve(Approve { approval: 41 })]
);
}
}
#[test]
fn refuse_sends_the_reason_when_there_is_one() {
for reason in [None, Some("not on a Friday")] {
let fake = brokerd_with(vec![pending(41, "shell", "{}")], ALLOWED);
let mut out = Vec::new();
assert!(cmd_refuse(&fake.socket, 41, reason, &mut out).unwrap());
assert_eq!(text(out), "refused 41\n");
assert_eq!(
fake.requests(),
vec![Message::Refuse(Refuse {
approval: 41,
reason: reason.map(str::to_string)
})]
);
}
}
#[test]
fn an_unknown_or_answered_id_is_reported_the_same_way_by_both() {
let fake = brokerd_with(vec![pending(41, "shell", "{}")], ALLOWED);
let want = "99: no such approval (already answered or expired)\n";
let mut out = Vec::new();
assert!(!cmd_approve(&fake.socket, 99, &mut out).unwrap());
assert_eq!(text(out), want);
let mut out = Vec::new();
assert!(!cmd_refuse(&fake.socket, 99, None, &mut out).unwrap());
assert_eq!(text(out), want);
}
/// Every exit of every command: any other error frame is an error, and so is an answer of the
/// wrong kind. Nothing is printed for them.
#[test]
fn other_errors_and_wrong_kinds_are_errors_for_every_command() {
let refused = fake_brokerd(|_| wire_error(ErrorCode::Internal, "boom"));
let wrong = fake_brokerd(|_| {
Message::GrantsReport(GrantsReport {
problems: Vec::new(),
})
});
let wrong_for_grants = fake_brokerd(|_| Message::Ok(Empty {}));
let now = ts("2026-09-18T12:00:00.000Z");
let mut out = Vec::new();
assert!(matches!(
cmd_approvals(&refused.socket, now, &mut out),
Err(AdminError::Refused(_))
));
assert!(matches!(
cmd_approve(&refused.socket, 41, &mut out),
Err(AdminError::Refused(_))
));
assert!(matches!(
cmd_refuse(&refused.socket, 41, None, &mut out),
Err(AdminError::Refused(_))
));
assert!(matches!(
cmd_grants_check(&refused.socket, &mut out),
Err(AdminError::Refused(_))
));
assert!(matches!(
cmd_approvals(&wrong.socket, now, &mut out),
Err(AdminError::Protocol(_))
));
assert!(matches!(
cmd_approve(&wrong.socket, 41, &mut out),
Err(AdminError::Protocol(_))
));
assert!(matches!(
cmd_refuse(&wrong.socket, 41, None, &mut out),
Err(AdminError::Protocol(_))
));
assert!(matches!(
cmd_grants_check(&wrong_for_grants.socket, &mut out),
Err(AdminError::Protocol(_))
));
assert_eq!(text(out), "", "an error prints nothing on the output");
}
#[test]
fn grants_check_prints_ok_or_every_problem() {
let fine = fake_brokerd(|_| {
Message::GrantsReport(GrantsReport {
problems: Vec::new(),
})
});
let mut out = Vec::new();
assert!(cmd_grants_check(&fine.socket, &mut out).unwrap());
assert_eq!(text(out), "grants: ok\n");
assert_eq!(fine.requests(), vec![Message::CheckGrants(Empty {})]);
let broken = fake_brokerd(|_| {
Message::GrantsReport(GrantsReport {
problems: vec![
GrantProblem {
file: "notes-read.toml".to_string(),
line: Some(3),
problem: "unknown field `mod`".to_string(),
},
GrantProblem {
file: "Bad Name.toml".to_string(),
line: None,
problem: "the file name is not a valid grant id".to_string(),
},
GrantProblem {
file: "x.toml".to_string(),
line: Some(1),
problem: "two\nlines".to_string(),
},
],
})
});
let mut out = Vec::new();
assert!(!cmd_grants_check(&broken.socket, &mut out).unwrap());
assert_eq!(
text(out),
format!(
"notes-read.toml:3: unknown field `mod`\n\
Bad Name.toml: the file name is not a valid grant id\n\
x.toml:1: two{}lines\n",
esc(0x0a)
),
"every problem, one line each; file and problem go through escape_json_text"
);
}
// ---- the binary ----
fn bxctl(args: &[&str], socket: &std::path::Path) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_bxctl"))
.args(args)
.arg("--admin-socket")
.arg(socket)
.output()
.unwrap()
}
#[test]
fn the_binary_prints_on_stdout_and_sets_the_exit_status() {
let fake = brokerd_with(
vec![pending(41, "shell", r#"{"command":"ls"}"#)],
DecisionRecord::Denied {
reason: DenyReason::DeniedByGrant,
},
);
let output = bxctl(&["approvals"], &fake.socket);
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.ends_with(" shell {\"command\":\"ls\"}\n"),
"{stdout}"
);
let output = bxctl(&["approve", "41"], &fake.socket);
assert_eq!(output.status.code(), Some(1));
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"approved 41: denied (denied_by_grant)\n"
);
let output = bxctl(&["refuse", "41", "--reason", "no"], &fake.socket);
assert_eq!(output.status.code(), Some(0));
assert_eq!(String::from_utf8_lossy(&output.stdout), "refused 41\n");
let output = bxctl(&["refuse", "7"], &fake.socket);
assert_eq!(output.status.code(), Some(1));
assert_eq!(
String::from_utf8_lossy(&output.stdout),
"7: no such approval (already answered or expired)\n"
);
}
#[test]
fn the_binary_reports_an_error_on_stderr_with_status_1() {
let fake = fake_brokerd(|_| wire_error(ErrorCode::Internal, "boom"));
let output = bxctl(&["grants", "check"], &fake.socket);
assert_eq!(output.status.code(), Some(1));
assert_eq!(String::from_utf8_lossy(&output.stdout), "");
assert_eq!(
String::from_utf8_lossy(&output.stderr),
"bxctl: internal: boom\n"
);
}
+283
View File
@@ -0,0 +1,283 @@
//! Tests for `bxctl`'s command line. Do not edit.
use bxctl::cli::{ChatOptions, Command, USAGE, UsageError, parse};
use proto::SessionId;
use std::path::{Path, PathBuf};
use std::process::Command as Process;
const HOME: &str = "/srv/bx";
fn args(words: &[&str]) -> Vec<String> {
words.iter().map(|w| w.to_string()).collect()
}
fn ok(words: &[&str]) -> Command {
parse(&args(words), Path::new(HOME)).unwrap_or_else(|_| panic!("{words:?} must parse"))
}
fn bad(words: &[&str]) {
assert_eq!(
parse(&args(words), Path::new(HOME)),
Err(UsageError),
"{words:?} must be a usage error"
);
}
fn default_admin() -> PathBuf {
PathBuf::from("/srv/bx/run/owner-broker/admin.sock")
}
#[test]
fn chat_defaults_come_from_home() {
assert_eq!(
ok(&["chat"]),
Command::Chat(ChatOptions {
socket: PathBuf::from("/srv/bx/run/loop/loop.sock"),
admin_socket: default_admin(),
session: None,
show_thinking: true,
say: None,
json: false,
})
);
}
#[test]
fn chat_takes_every_flag_in_any_order() {
assert_eq!(
ok(&[
"chat",
"--json",
"--admin-socket",
"/tmp/a.sock",
"--say",
"hello there",
"--no-thinking",
"--session",
"s-1",
"--socket",
"/tmp/l.sock",
]),
Command::Chat(ChatOptions {
socket: PathBuf::from("/tmp/l.sock"),
admin_socket: PathBuf::from("/tmp/a.sock"),
session: Some(SessionId::new("s-1").unwrap()),
show_thinking: false,
say: Some("hello there".to_string()),
json: true,
})
);
}
#[test]
fn chat_usage_errors() {
bad(&["chat", "--session", "Not Valid!"]);
bad(&["chat", "--session"]);
bad(&["chat", "--socket"]);
bad(&["chat", "--admin-socket"]);
bad(&["chat", "--say"]);
bad(&["chat", "--dance"]);
bad(&["chat", "stray"]);
}
#[test]
fn approvals() {
assert_eq!(
ok(&["approvals"]),
Command::Approvals {
admin_socket: default_admin()
}
);
assert_eq!(
ok(&["approvals", "--admin-socket", "/tmp/a.sock"]),
Command::Approvals {
admin_socket: PathBuf::from("/tmp/a.sock")
}
);
bad(&["approvals", "41"]);
bad(&["approvals", "--admin-socket"]);
bad(&["approvals", "--reason", "x"]);
}
#[test]
fn approve() {
assert_eq!(
ok(&["approve", "41"]),
Command::Approve {
admin_socket: default_admin(),
approval: 41
}
);
// The flag may come before or after the id.
for words in [
["approve", "--admin-socket", "/tmp/a.sock", "41"],
["approve", "41", "--admin-socket", "/tmp/a.sock"],
] {
assert_eq!(
ok(&words),
Command::Approve {
admin_socket: PathBuf::from("/tmp/a.sock"),
approval: 41
}
);
}
assert_eq!(
ok(&["approve", "18446744073709551615"]),
Command::Approve {
admin_socket: default_admin(),
approval: u64::MAX
}
);
}
/// An id is decimal digits and nothing else. `str::parse::<u64>` alone would accept `+41`.
#[test]
fn an_approval_id_is_only_digits() {
bad(&["approve"]);
bad(&["approve", "41", "42"]);
bad(&["approve", "+41"]);
bad(&["approve", "-1"]);
bad(&["approve", "4 1"]);
bad(&["approve", " 41"]);
bad(&["approve", "0x29"]);
bad(&["approve", "forty-one"]);
bad(&["approve", ""]);
bad(&["approve", "18446744073709551616"]);
bad(&["approve", "41", "--reason", "x"]);
bad(&["refuse"]);
bad(&["refuse", "+41"]);
bad(&["refuse", "41", "42"]);
}
#[test]
fn refuse() {
assert_eq!(
ok(&["refuse", "41"]),
Command::Refuse {
admin_socket: default_admin(),
approval: 41,
reason: None
}
);
assert_eq!(
ok(&[
"refuse",
"41",
"--reason",
"not on a Friday",
"--admin-socket",
"/tmp/a.sock"
]),
Command::Refuse {
admin_socket: PathBuf::from("/tmp/a.sock"),
approval: 41,
reason: Some("not on a Friday".to_string())
}
);
// A value is a value, even when it looks like a flag.
assert_eq!(
ok(&["refuse", "--reason", "--admin-socket", "41"]),
Command::Refuse {
admin_socket: default_admin(),
approval: 41,
reason: Some("--admin-socket".to_string())
}
);
bad(&["refuse", "41", "--reason"]);
bad(&["refuse", "41", "--reason", "a", "--reason", "b"]);
}
#[test]
fn grants_check_and_audit_verify() {
assert_eq!(
ok(&["grants", "check"]),
Command::GrantsCheck {
admin_socket: default_admin()
}
);
assert_eq!(
ok(&["grants", "check", "--admin-socket", "/tmp/a.sock"]),
Command::GrantsCheck {
admin_socket: PathBuf::from("/tmp/a.sock")
}
);
assert_eq!(
ok(&["audit", "verify"]),
Command::AuditVerify {
home: PathBuf::from(HOME)
}
);
assert_eq!(
ok(&["audit", "verify", "--home", "/tmp/h"]),
Command::AuditVerify {
home: PathBuf::from("/tmp/h")
}
);
bad(&["grants"]);
bad(&["grants", "list"]);
bad(&["grants", "check", "extra"]);
bad(&["audit"]);
bad(&["audit", "verify", "--home"]);
bad(&["audit", "verify", "--admin-socket", "/tmp/a.sock"]);
}
#[test]
fn anything_else_is_a_usage_error() {
bad(&[]);
bad(&["dance"]);
bad(&["--admin-socket", "/tmp/a.sock", "approvals"]);
}
#[test]
fn usage_names_every_command() {
for word in [
"bxctl chat",
"bxctl approvals",
"bxctl approve <id>",
"bxctl refuse <id>",
"bxctl grants check",
"bxctl audit verify",
"--admin-socket",
"--reason",
"--home",
] {
assert!(USAGE.contains(word), "usage lacks {word:?}");
}
}
#[test]
fn the_binary_prints_usage_and_exits_2() {
for words in [
vec![],
vec!["dance"],
vec!["approve", "+41"],
vec!["refuse"],
] {
let output = Process::new(env!("CARGO_BIN_EXE_bxctl"))
.args(&words)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(2), "{words:?}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "", "{words:?}");
assert!(
String::from_utf8_lossy(&output.stderr).contains("bxctl approve <id>"),
"{words:?}"
);
}
}
/// The default sockets are under `$BOXMAKER_HOME`. Nothing listens there, so the command fails
/// to connect, and says where it tried.
#[test]
fn the_binary_finds_the_admin_socket_under_boxmaker_home() {
let home = std::env::temp_dir().join(format!("bxctl-cli-home-{}", std::process::id()));
let output = Process::new(env!("CARGO_BIN_EXE_bxctl"))
.arg("approvals")
.env("BOXMAKER_HOME", &home)
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
let want = home.join("run/owner-broker/admin.sock");
assert!(stderr.contains(want.to_str().unwrap()), "{stderr}");
}
+150
View File
@@ -0,0 +1,150 @@
//! Tests for `bxctl::escape`: text the model wrote is printed as data. Do not edit.
//!
//! The expected escapes are built by `esc`, never spelled out, so that nothing that handles this
//! file can turn one into the character it stands for.
use bxctl::escape::{escape_json_text, escape_model_text};
const BACKSLASH: char = '\\';
/// The escape for one code point: a backslash, `u`, and four lowercase hex digits.
fn esc(code: u32) -> String {
format!("{BACKSLASH}u{code:04x}")
}
fn ch(code: u32) -> char {
char::from_u32(code).unwrap()
}
/// Every code point that must be escaped, as inclusive ranges.
const HIDDEN: [(u32, u32); 6] = [
(0x0000, 0x001f),
(0x007f, 0x009f),
(0x200b, 0x200f),
(0x2028, 0x202e),
(0x2060, 0x2069),
(0xfeff, 0xfeff),
];
fn hidden(code: u32) -> bool {
HIDDEN.iter().any(|(lo, hi)| (*lo..=*hi).contains(&code))
}
/// Walks every code point below U+11000, not a sample: each is either escaped exactly or left
/// exactly as it is.
#[test]
fn every_listed_code_point_is_escaped_and_no_other() {
let mut escaped = 0;
for code in 0..0x11000u32 {
let Some(c) = char::from_u32(code) else {
continue; // the surrogates are not characters
};
let text = format!("a{c}b");
let got = escape_json_text(&text);
if hidden(code) {
escaped += 1;
assert_eq!(got, format!("a{}b", esc(code)), "U+{code:04X}");
} else {
assert_eq!(got, text, "U+{code:04X} must pass through");
}
}
assert_eq!(escaped, 32 + 33 + 5 + 7 + 10 + 1, "the six ranges, in full");
}
#[test]
fn the_edges_of_each_range() {
for (lo, hi) in HIDDEN {
assert_eq!(escape_json_text(&ch(lo).to_string()), esc(lo));
assert_eq!(escape_json_text(&ch(hi).to_string()), esc(hi));
if lo > 0 {
let before = ch(lo - 1).to_string();
assert_eq!(escape_json_text(&before), before, "U+{:04X}", lo - 1);
}
let after = ch(hi + 1).to_string();
assert_eq!(escape_json_text(&after), after, "U+{:04X}", hi + 1);
}
}
#[test]
fn hex_digits_are_lowercase_and_there_are_always_four() {
assert_eq!(escape_json_text("\x1b"), esc(0x1b));
assert!(escape_json_text("\x1b").ends_with("001b"));
assert!(escape_json_text("\0").ends_with("0000"));
assert!(escape_json_text(&ch(0xfeff).to_string()).ends_with("feff"));
assert!(escape_json_text(&ch(0x202e).to_string()).ends_with("202e"));
}
#[test]
fn an_escape_sequence_cannot_reach_the_terminal() {
let text = "before\x1b[8mhidden\x1b[0m\x07after";
let got = escape_json_text(text);
assert!(!got.contains('\x1b') && !got.contains('\x07'), "{got:?}");
assert_eq!(
got,
format!(
"before{}[8mhidden{}[0m{}after",
esc(0x1b),
esc(0x1b),
esc(0x07)
)
);
}
#[test]
fn a_path_cannot_be_shown_backwards() {
// U+202E makes a terminal draw what follows from right to left.
let text = format!("/home/kyle/notes/{}dm.terces", ch(0x202e));
assert_eq!(
escape_json_text(&text),
format!("/home/kyle/notes/{}dm.terces", esc(0x202e))
);
let text = format!("a{}b{}c", ch(0x200b), ch(0x2066));
assert_eq!(
escape_json_text(&text),
format!("a{}b{}c", esc(0x200b), esc(0x2066))
);
}
#[test]
fn ordinary_text_is_unchanged() {
for text in [
"",
"plain",
r#"{"command":"ls -l","cwd":"/home/kyle"}"#,
"naïve café 日本語 🙂",
"a backslash \\ and a quote \" stay as they are",
] {
assert_eq!(escape_json_text(text), text);
assert_eq!(escape_model_text(text), text);
}
}
#[test]
fn json_text_escapes_newline_and_tab_but_model_text_keeps_them() {
let text = "one\n\ttwo\r\n";
assert_eq!(
escape_json_text(text),
format!("one{}{}two{}{}", esc(0x0a), esc(0x09), esc(0x0d), esc(0x0a))
);
assert_eq!(
escape_model_text(text),
format!("one\n\ttwo{}\n", esc(0x0d)),
"only newline and tab pass; a carriage return could overwrite the line"
);
}
#[test]
fn model_text_escapes_everything_else_the_same_way() {
for (lo, hi) in HIDDEN {
for code in lo..=hi {
if code == 0x0a || code == 0x09 {
continue;
}
assert_eq!(
escape_model_text(&ch(code).to_string()),
esc(code),
"U+{code:04X}"
);
}
}
}
+186
View File
@@ -0,0 +1,186 @@
//! 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 }
}