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>
198 lines
6.7 KiB
Rust
198 lines
6.7 KiB
Rust
//! The text of an approval's posts and of its ending line (M4b spec, section 6). Do not edit.
|
|
|
|
use bxcore::{CallId, DataClass, DenyReason, PendingApproval, SessionId, Timestamp, sha256};
|
|
use gatewayd::approvals::text::{
|
|
Ended, MAX_PART_POST, MAX_PARTS, PART_CHARS, code, ended_line, fence, render, with_ending,
|
|
};
|
|
|
|
/// The escape for one code point, built here so this file holds no such character itself.
|
|
fn esc(code: u32) -> String {
|
|
format!("{}u{code:04x}", char::from(92))
|
|
}
|
|
|
|
fn info(arguments: &str) -> PendingApproval {
|
|
PendingApproval {
|
|
approval: 42,
|
|
session: SessionId::new("mm-r0000000000000000000000001").unwrap(),
|
|
call: CallId(3),
|
|
tool: "shell".to_string(),
|
|
arguments: arguments.to_string(),
|
|
grant: "ops-shell".to_string(),
|
|
taint: DataClass::Private,
|
|
created: Timestamp::parse("2026-09-24T14:32:10.000Z").unwrap(),
|
|
expires: Timestamp::parse("2026-09-24T14:47:10.000Z").unwrap(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn one_part_exactly() {
|
|
let shown = render(&info(
|
|
r#"{"command":"ls -la /srv/backups","timeout_ms":10000}"#,
|
|
));
|
|
assert!(shown.answerable);
|
|
assert_eq!(
|
|
shown.posts,
|
|
[
|
|
"**Approval 42** · tool `shell` · grant `ops-shell` · taint private · expires 2026-09-24 14:47 UTC\n```\n{\"command\":\"ls -la /srv/backups\",\"timeout_ms\":10000}\n```\nReply in this thread with `!approve 42` or `!deny 42 <reason>`."
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fences_and_code_outrun_backticks() {
|
|
assert_eq!(fence("a"), "```\na\n```");
|
|
assert_eq!(fence("a ``` b ```` c"), "`````\na ``` b ```` c\n`````");
|
|
assert_eq!(code("ops"), "`ops`");
|
|
assert_eq!(code("a`b"), "``a`b``");
|
|
assert_eq!(code("`x"), "`` `x ``");
|
|
let shown = render(&info(r#"{"command":"echo ```` done"}"#));
|
|
assert!(
|
|
shown.posts[0].contains("\n`````\n{\"command\":\"echo ```` done\"}\n`````\n"),
|
|
"{}",
|
|
shown.posts[0]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn what_must_not_reach_the_reader_raw_is_escaped() {
|
|
let raw = "a\u{7}b\u{202e}c\u{200b}d\u{2028}e\u{feff}f\nnewline";
|
|
let shown = render(&info(&format!("{{\"command\":\"{raw}\"}}")));
|
|
let post = &shown.posts[0];
|
|
for bad in ['\u{7}', '\u{202e}', '\u{200b}', '\u{2028}', '\u{feff}'] {
|
|
assert!(!post.contains(bad), "{bad:?} reached the post");
|
|
}
|
|
let want = format!(
|
|
"a{}b{}c{}d{}e{}f{}newline",
|
|
esc(0x7),
|
|
esc(0x202e),
|
|
esc(0x200b),
|
|
esc(0x2028),
|
|
esc(0xfeff),
|
|
esc(0xa)
|
|
);
|
|
assert!(post.contains(&want), "{post}");
|
|
let mut odd = info("{}");
|
|
odd.grant = "g`rant\u{202e}".to_string();
|
|
assert!(
|
|
render(&odd).posts[0].contains(&format!("grant ``g`rant{}``", esc(0x202e))),
|
|
"{}",
|
|
render(&odd).posts[0]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn long_arguments_come_in_parts_with_a_hash() {
|
|
let args = format!("{{\"content\":\"{}\"}}", "é".repeat(PART_CHARS * 2));
|
|
let shown = render(&info(&args));
|
|
assert!(shown.answerable);
|
|
assert_eq!(shown.posts.len(), 3);
|
|
let total = args.chars().count();
|
|
assert!(shown.posts[0].starts_with("**Approval 42** · tool `shell` · grant `ops-shell` · taint private · expires 2026-09-24 14:47 UTC\nPart 1 of 3 of the arguments:\n```\n{\"content\":\""));
|
|
assert!(shown.posts[1].starts_with("**Approval 42**, part 2 of 3 of the arguments:\n```\n"));
|
|
let hash = sha256(args.as_bytes()).unwrap().to_hex();
|
|
let last = &shown.posts[2];
|
|
assert!(last.ends_with(&format!("\nApproval 42: 3 parts, {total} characters, sha256 {hash} (of the arguments as shown).\nReply in this thread with `!approve 42` or `!deny 42 <reason>`.")), "{last}");
|
|
assert!(
|
|
!shown.posts[0].contains("Reply in this thread")
|
|
&& !shown.posts[1].contains("Reply in this thread")
|
|
);
|
|
let shown_again: String = shown
|
|
.posts
|
|
.iter()
|
|
.map(|p| {
|
|
let body = p.split_once("\n```\n").unwrap().1;
|
|
body.split_once("\n```").unwrap().0.to_string()
|
|
})
|
|
.collect();
|
|
assert_eq!(
|
|
shown_again, args,
|
|
"the parts, joined, are the arguments: nothing lost"
|
|
);
|
|
assert!(
|
|
shown
|
|
.posts
|
|
.iter()
|
|
.all(|p| p.chars().count() <= MAX_PART_POST)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn too_long_to_show_is_answerable_only_in_bxctl() {
|
|
let args = "x".repeat(PART_CHARS * MAX_PARTS + 1);
|
|
let shown = render(&info(&args));
|
|
assert!(!shown.answerable);
|
|
let hash = sha256(args.as_bytes()).unwrap().to_hex();
|
|
assert_eq!(
|
|
shown.posts,
|
|
[format!(
|
|
"**Approval 42** · tool `shell` · grant `ops-shell` · taint private · expires 2026-09-24 14:47 UTC\nThe arguments are {} characters (sha256 {hash}), too long to show here; answer it with `bxctl approvals`.",
|
|
args.len()
|
|
)]
|
|
);
|
|
let exactly = "x".repeat(PART_CHARS * MAX_PARTS);
|
|
assert!(
|
|
render(&info(&exactly)).answerable,
|
|
"four full parts still show"
|
|
);
|
|
let ticks = format!("{}{}", "`".repeat(PART_CHARS - 10), "x".repeat(20));
|
|
assert!(
|
|
!render(&info(&ticks)).answerable,
|
|
"a fence that would overflow the post is not shown"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ending_lines() {
|
|
let kyle = "@kyle".to_string();
|
|
assert_eq!(
|
|
ended_line(&Ended::Runs { by: kyle.clone() }),
|
|
"✅ Approved by @kyle: the call runs."
|
|
);
|
|
assert_eq!(
|
|
ended_line(&Ended::Denied {
|
|
by: kyle.clone(),
|
|
reason: DenyReason::GrantExpired
|
|
}),
|
|
"Approved by @kyle, but denied on the re-check: grant_expired."
|
|
);
|
|
assert_eq!(
|
|
ended_line(&Ended::Refused {
|
|
by: kyle.clone(),
|
|
reason: None
|
|
}),
|
|
"❌ Refused by @kyle: no reason given."
|
|
);
|
|
assert_eq!(
|
|
ended_line(&Ended::Refused {
|
|
by: kyle.clone(),
|
|
reason: Some("not @channel `now`".to_string())
|
|
}),
|
|
"❌ Refused by @kyle: `` not @channel `now` ``."
|
|
);
|
|
assert_eq!(
|
|
ended_line(&Ended::NoLonger),
|
|
"No longer pending (answered elsewhere, expired, or its turn ended)."
|
|
);
|
|
assert_eq!(
|
|
ended_line(&Ended::Restarted),
|
|
"No longer answerable here: gatewayd restarted. If it is still pending it is posted again."
|
|
);
|
|
assert_eq!(
|
|
with_ending("post", &Ended::NoLonger),
|
|
"post\n\nNo longer pending (answered elsewhere, expired, or its turn ended)."
|
|
);
|
|
let longest = render(&info(&"y".repeat(PART_CHARS))).posts.remove(0);
|
|
let ended = with_ending(
|
|
&longest,
|
|
&Ended::Refused {
|
|
by: format!("@{}", "u".repeat(64)),
|
|
reason: Some("r".repeat(500)),
|
|
},
|
|
);
|
|
assert!(
|
|
ended.chars().count() <= 16_383,
|
|
"an ended post still fits: {}",
|
|
ended.chars().count()
|
|
);
|
|
}
|