//! 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 `." ] ); } #[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 `.")), "{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() ); }