Files
kyleandClaude Opus 5 e3f37da232 Hand over the M3a plan: 22 tasks, their files, and the check record
Task files, the files they copy in (byte-identical to the reference on
m3a-ref), each area's check record, and a README with the per-task
table of what each check exposed. The handoff note is done with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 23:45:43 -07:00

284 lines
7.3 KiB
Rust

//! 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}");
}