Done the way the owner asked and the spike mapped: the crate and the manifests renamed, then what the build reported fixed. Layers: 138 "proto unresolved" errors across 169 files; cargo fmt reordering imports; six fixture paths built as strings in brokerd's and bxctl's tests, which the compiler cannot see, and comments naming crates/proto/; the crate-dependency check and its self-test. One more the spike did not reach: a `\nproto =` key inside a printf format in the self-test, which a word-boundary replacement skips because `n` and `p` are both word characters. make gate: gate: ok, 944 tests; no new entry in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
167 lines
4.8 KiB
Rust
167 lines
4.8 KiB
Rust
//! Which commands answer an approval, in the order of the M4b spec, section 7; everything else is
|
|
//! answered with its reason and changes nothing. Do not edit.
|
|
|
|
use std::sync::mpsc::{self, Receiver};
|
|
|
|
use bxcore::ApprovalReply;
|
|
use gatewayd::approvals::{Judged, NOT_APPROVER, Posted, Registry, USAGE};
|
|
use gatewayd::sessions::ApprovalCommand::{self, Approve, Deny, Malformed};
|
|
|
|
const KYLE: &str = "k0000000000000000000000000";
|
|
const HERMES: &str = "h0000000000000000000000000";
|
|
const DM: &str = "d0000000000000000000000000";
|
|
const ROOT: &str = "r0000000000000000000000000";
|
|
const ELSEWHERE: &str = "e0000000000000000000000000";
|
|
const CMD: &str = "c0000000000000000000000001";
|
|
|
|
fn approvers() -> Vec<String> {
|
|
vec![KYLE.to_string()]
|
|
}
|
|
|
|
/// A registry with approval 42 posted in ROOT, shown at 100.
|
|
fn posted(answerable: bool) -> (Registry, Receiver<ApprovalReply>) {
|
|
let registry = Registry::new();
|
|
let (tx, rx) = mpsc::channel();
|
|
registry.add(Posted {
|
|
approval: 42,
|
|
session: format!("mm-{ROOT}"),
|
|
channel: DM.to_string(),
|
|
root: ROOT.to_string(),
|
|
shown_at: 100,
|
|
answerable,
|
|
answer: tx,
|
|
});
|
|
(registry, rx)
|
|
}
|
|
|
|
fn judge(r: &Registry, command: &ApprovalCommand, user: &str, root: &str, at: i64) -> Judged {
|
|
r.judge(command, user, root, CMD, at, &approvers())
|
|
}
|
|
|
|
#[test]
|
|
fn an_approver_in_the_thread_after_the_post_answers_it() {
|
|
let (r, rx) = posted(true);
|
|
assert_eq!(judge(&r, &Approve(42), KYLE, ROOT, 101), Judged::Sent);
|
|
assert_eq!(
|
|
rx.try_recv().unwrap(),
|
|
ApprovalReply {
|
|
approval: 42,
|
|
approved: true,
|
|
by: KYLE.to_string(),
|
|
post: CMD.to_string(),
|
|
reason: None
|
|
}
|
|
);
|
|
assert!(!r.holds(42), "answered once");
|
|
assert_eq!(
|
|
judge(&r, &Approve(42), KYLE, ROOT, 102),
|
|
Judged::Reply("approval 42 is not pending here".to_string())
|
|
);
|
|
assert!(rx.try_recv().is_err(), "a second answer goes nowhere");
|
|
}
|
|
|
|
#[test]
|
|
fn a_refusal_carries_its_reason() {
|
|
let (r, rx) = posted(true);
|
|
assert_eq!(
|
|
judge(
|
|
&r,
|
|
&Deny(42, Some("not on a Friday".to_string())),
|
|
KYLE,
|
|
ROOT,
|
|
101
|
|
),
|
|
Judged::Sent
|
|
);
|
|
let got = rx.try_recv().unwrap();
|
|
assert_eq!(
|
|
(got.approved, got.reason.as_deref()),
|
|
(false, Some("not on a Friday"))
|
|
);
|
|
let (r, rx) = posted(true);
|
|
assert_eq!(judge(&r, &Deny(42, None), KYLE, ROOT, 101), Judged::Sent);
|
|
assert_eq!(rx.try_recv().unwrap().reason, None);
|
|
}
|
|
|
|
#[test]
|
|
fn everything_else_is_refused_with_its_reason_in_order() {
|
|
let cases: &[(&ApprovalCommand, &str, &str, i64, &str)] = &[
|
|
(&Approve(42), HERMES, ROOT, 101, NOT_APPROVER),
|
|
(&Malformed, HERMES, ROOT, 101, NOT_APPROVER),
|
|
(&Malformed, KYLE, ROOT, 101, USAGE),
|
|
(
|
|
&Approve(7),
|
|
KYLE,
|
|
ROOT,
|
|
101,
|
|
"approval 7 is not pending here",
|
|
),
|
|
(
|
|
&Approve(42),
|
|
KYLE,
|
|
ELSEWHERE,
|
|
101,
|
|
"approval 42 is answered in its own thread",
|
|
),
|
|
(
|
|
&Approve(42),
|
|
KYLE,
|
|
ROOT,
|
|
100,
|
|
"approval 42 was answered before it was shown in full",
|
|
),
|
|
(
|
|
&Approve(42),
|
|
KYLE,
|
|
ROOT,
|
|
50,
|
|
"approval 42 was answered before it was shown in full",
|
|
),
|
|
(
|
|
&Approve(42),
|
|
KYLE,
|
|
ELSEWHERE,
|
|
50,
|
|
"approval 42 is answered in its own thread",
|
|
),
|
|
];
|
|
for (command, user, root, at, want) in cases {
|
|
let (r, rx) = posted(true);
|
|
assert_eq!(
|
|
judge(&r, command, user, root, *at),
|
|
Judged::Reply(want.to_string()),
|
|
"{command:?} {user} {root} {at}"
|
|
);
|
|
assert!(r.holds(42), "still pending after {command:?}");
|
|
assert!(rx.try_recv().is_err(), "nothing sent for {command:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn too_long_to_show_is_answered_only_in_bxctl() {
|
|
let (r, rx) = posted(false);
|
|
assert_eq!(
|
|
judge(&r, &Approve(42), KYLE, ROOT, 101),
|
|
Judged::Reply("approval 42 can only be answered with bxctl approvals".to_string())
|
|
);
|
|
assert!(r.holds(42) && rx.try_recv().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn an_answer_whose_connection_is_gone_is_not_pending() {
|
|
let (r, rx) = posted(true);
|
|
drop(rx);
|
|
assert_eq!(
|
|
judge(&r, &Approve(42), KYLE, ROOT, 101),
|
|
Judged::Reply("approval 42 is not pending here".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_waiting_sessions_and_removal() {
|
|
let (r, _rx) = posted(true);
|
|
assert_eq!(r.waiting_sessions(), [format!("mm-{ROOT}")]);
|
|
assert_eq!(r.remove(42).map(|p| p.approval), Some(42));
|
|
assert!(r.waiting_sessions().is_empty() && !r.holds(42) && r.remove(42).is_none());
|
|
}
|