1#[derive(Debug)]
25pub enum PeerInboundKind {
26 Message { id: String, from: String, org: String, body: String },
31 DeliveryFailure { target: String, org: String, reason: String },
39 WorkerSpawnFailed { label: String, reason: String },
48 Gotify { app: String, title: String, message: String, priority: u8 },
54 Cron { prompt: String },
60 Voice { note: String },
66 Slack {
73 workspace: String,
74 channel: String,
75 author: Option<String>,
82 body: String,
83 },
84}
85
86impl PeerInboundKind {
87 pub fn org(&self) -> &str {
94 match self {
95 Self::Message { org, .. } | Self::DeliveryFailure { org, .. } => org,
96 Self::WorkerSpawnFailed { .. }
97 | Self::Gotify { .. }
98 | Self::Cron { .. }
99 | Self::Voice { .. }
100 | Self::Slack { .. } => "",
101 }
102 }
103
104 pub fn peer_sender_identity(&self) -> Option<&str> {
109 match self {
110 Self::Message { from, .. } => Some(from),
111 Self::DeliveryFailure { target, .. } => Some(target),
112 Self::WorkerSpawnFailed { label, .. } => Some(label),
113 Self::Gotify { .. } | Self::Cron { .. } | Self::Voice { .. } | Self::Slack { .. } => {
114 None
115 }
116 }
117 }
118}
119
120pub fn inbound_envelope_id(text: &str) -> Option<&str> {
126 let (header, _) = text.strip_prefix('[')?.split_once(']')?;
129 let after = header.split_once(" id=")?.1;
133 let id = &after[..after.find([' ', ':']).unwrap_or(after.len())];
134 (!id.is_empty()).then_some(id)
135}
136
137pub fn detect_inbound(text: &str) -> Option<PeerInboundKind> {
142 let bracketed = text.strip_prefix('[')?;
143 let close_idx = bracketed.find(']')?;
144 let header = &bracketed[..close_idx];
145 let after_bracket = &bracketed[close_idx + 1..];
146 let body = after_bracket.strip_prefix("\n\n").unwrap_or("").to_owned();
151
152 if let Some(rest) = header.strip_prefix("Message id=") {
153 let (id, rest) = take_until(rest, " from agent ")?;
154 let (from, org) = extract_from_agent_after(rest)?;
155 return Some(PeerInboundKind::Message { id: id.to_owned(), from, org, body });
156 }
157
158 if let Some(rest) = header.strip_prefix("Question id=") {
162 let (id, rest) = take_until(rest, " from agent ")?;
163 let (from, org) = extract_from_agent_after(rest)?;
164 return Some(PeerInboundKind::Message { id: id.to_owned(), from, org, body });
165 }
166
167 if let Some(rest) = header.strip_prefix("Reply id=") {
168 let (id, rest) = take_until(rest, " from agent ")?;
169 let (from, org) = extract_from_agent_after(rest)?;
170 return Some(PeerInboundKind::Message { id: id.to_owned(), from, org, body });
171 }
172
173 if let Some(rest) = header.strip_prefix("Message to agent ")
174 && header.contains("failed to deliver:")
175 {
176 let (target, org, trailing) = extract_from_agent_after_with_trailer(rest)?;
177 let reason = trailing
178 .split_once("failed to deliver:")
179 .map(|(_, after)| after.trim().to_owned())
180 .unwrap_or_default();
181 return Some(PeerInboundKind::DeliveryFailure { target, org, reason });
182 }
183
184 if let Some(rest) = header.strip_prefix("Ask id=") {
187 if let Some(rest_to) = rest_after_id(rest, " to agent ")
189 && header.contains("failed to deliver:")
190 {
191 let (target, org, trailing) = extract_from_agent_after_with_trailer(rest_to)?;
192 let reason = trailing
193 .split_once("failed to deliver:")
194 .map(|(_, after)| after.trim().to_owned())
195 .unwrap_or_default();
196 return Some(PeerInboundKind::DeliveryFailure { target, org, reason });
197 }
198 }
199
200 if let Some(rest) = header.strip_prefix("Worker '") {
201 let (label, rest) = take_until(rest, "' spawn failed id=")?;
202 let (_id, reason) = take_until(rest, ": ")?;
203 return Some(PeerInboundKind::WorkerSpawnFailed {
204 label: label.to_owned(),
205 reason: reason.to_owned(),
206 });
207 }
208
209 if let Some(rest) = header.strip_prefix("Gotify - app '") {
210 let (app, priority_str) = take_until(rest, "', priority ")?;
211 let priority: u8 = priority_str.parse().ok()?;
212 let raw_body = after_bracket.strip_prefix('\n').unwrap_or(after_bracket);
215 let (title, message) = match raw_body.split_once('\n') {
216 Some((t, m)) => (t.to_owned(), m.to_owned()),
217 None => (raw_body.to_owned(), String::new()),
218 };
219 return Some(PeerInboundKind::Gotify { app: app.to_owned(), title, message, priority });
220 }
221
222 if let Some(rest) = header.strip_prefix("Slack - workspace '") {
223 let (workspace, channel) = take_until(rest, "', ")?;
224 let (tail, rest) = after_bracket.split_once('\n')?;
227 let (author, body) = rest.split_once(": ")?;
230 if bundle_member_count(tail).is_some() {
235 let body = rest
239 .lines()
240 .map(|line| {
241 line.split_once(": ").map_or(line, |(author, text)| {
242 if author == "unknown" || is_slack_id(author) { text } else { line }
243 })
244 })
245 .collect::<Vec<_>>()
246 .join("\n");
247 return Some(PeerInboundKind::Slack {
248 workspace: workspace.to_owned(),
249 channel: channel.to_owned(),
250 author: None,
251 body,
252 });
253 }
254 let author =
258 if author == "unknown" || is_slack_id(author) { None } else { Some(author.to_owned()) };
259 return Some(PeerInboundKind::Slack {
260 workspace: workspace.to_owned(),
261 channel: channel.to_owned(),
262 author,
263 body: body.to_owned(),
264 });
265 }
266
267 if header == "Cron" {
268 return Some(PeerInboundKind::Cron { prompt: body });
271 }
272
273 if header == "Voice" {
274 return Some(PeerInboundKind::Voice { note: body });
277 }
278
279 None
280}
281
282pub fn tidy_mrkdwn(text: &str) -> String {
285 let mut out = String::with_capacity(text.len());
286 let mut rest = text;
287 while let Some(open) = rest.find('<') {
288 out.push_str(&strip_emphasis(&rest[..open]));
289 let after = &rest[open + 1..];
290 let Some(close) = after.find('>') else {
291 out.push_str(&strip_emphasis(&rest[open..]));
292 return decode_entities(&out);
293 };
294 if let Some((url, label)) = after[..close].split_once('|') {
296 out.push_str(&strip_emphasis(label));
297 out.push_str(": ");
298 out.push_str(url);
299 } else {
300 out.push('<');
301 out.push_str(&after[..close]);
302 out.push('>');
303 }
304 rest = &after[close + 1..];
305 }
306 out.push_str(&strip_emphasis(rest));
307 decode_entities(&out)
308}
309
310fn strip_emphasis(text: &str) -> String {
314 let chars: Vec<char> = text.chars().collect();
315 let mut dropped = vec![false; chars.len()];
316 let mut i = 0;
317 while i < chars.len() {
318 if matches!(chars[i], '*' | '_')
319 && opens_a_span(&chars, i)
320 && let Some(close) = first_closer(&chars, i)
321 {
322 dropped[i] = true;
323 dropped[close] = true;
324 i = close + 1;
325 continue;
326 }
327 i += 1;
328 }
329 chars.iter().zip(&dropped).filter(|(_, dropped)| !**dropped).map(|(c, _)| *c).collect()
330}
331
332fn opens_a_span(chars: &[char], i: usize) -> bool {
336 let marker = chars[i];
337 let before = i.checked_sub(1).and_then(|p| chars.get(p));
338 let after = chars.get(i + 1).copied();
339 before.is_none_or(|p| !p.is_alphanumeric() && *p != marker)
340 && after.is_some_and(|n| !n.is_whitespace() && n != marker)
341}
342
343fn first_closer(chars: &[char], open: usize) -> Option<usize> {
348 let marker = chars[open];
349 ((open + 1)..chars.len()).find(|&j| {
350 chars[j] == marker
351 && chars.get(j - 1).is_some_and(|p| !p.is_whitespace())
352 && chars.get(j + 1).is_none_or(|n| !n.is_alphanumeric())
353 && !opens_a_span(chars, j)
354 })
355}
356
357fn decode_entities(text: &str) -> String {
360 text.replace("<", "<").replace(">", ">").replace("&", "&")
361}
362
363pub fn is_slack_id(text: &str) -> bool {
369 let mut chars = text.chars();
370 chars.next().is_some_and(|first| first.is_ascii_uppercase())
371 && chars.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
372}
373
374fn bundle_member_count(tail: &str) -> Option<usize> {
378 tail.rsplit_once('(')?.1.strip_suffix(" messages)")?.parse().ok()
379}
380
381fn take_until<'a>(s: &'a str, marker: &str) -> Option<(&'a str, &'a str)> {
385 let idx = s.find(marker)?;
386 Some((&s[..idx], &s[idx + marker.len()..]))
387}
388
389fn rest_after_id<'a>(after_id: &'a str, marker: &str) -> Option<&'a str> {
393 let idx = after_id.find(marker)?;
394 Some(&after_id[idx + marker.len()..])
395}
396
397fn extract_from_agent_after(rest: &str) -> Option<(String, String)> {
400 let after_open = rest.strip_prefix('\'')?;
401 let (name, after_name) = take_until(after_open, "' (org '")?;
402 let (org, _trailing) = take_until(after_name, "')")?;
403 Some((name.to_owned(), org.to_owned()))
404}
405
406fn extract_from_agent_after_with_trailer(rest: &str) -> Option<(String, String, String)> {
410 let after_open = rest.strip_prefix('\'')?;
411 let (name, after_name) = take_until(after_open, "' (org '")?;
412 let (org, trailing) = take_until(after_name, "')")?;
413 Some((name.to_owned(), org.to_owned(), trailing.to_owned()))
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn detect_message_inbound_carries_the_send_id() {
422 let text = "[Message id=m-c45a8f12 from agent 'gateway-backend' (org 'Gateway')]\n\nFYI rewriter cleanup just landed.";
423 let kind = detect_inbound(text).expect("message");
424 match kind {
425 PeerInboundKind::Message { id, from, org, body } => {
426 assert_eq!(id, "m-c45a8f12", "the send's id rides the envelope");
427 assert_eq!(from, "gateway-backend");
428 assert_eq!(org, "Gateway");
429 assert_eq!(body, "FYI rewriter cleanup just landed.");
430 }
431 other => panic!("expected Message, got {other:?}"),
432 }
433 }
434
435 #[test]
440 fn detect_question_and_reply_replay_as_the_one_message_kind() {
441 let question = concat!(
444 "[Question id=q-7f3a92e0 from agent 'forge' (org 'Personal') - reply with ",
445 "agents__tell",
447 " in_reply_to=q-7f3a92e0]\n\nWhat's the test setup?",
448 );
449 match detect_inbound(question).expect("a recorded question still parses") {
450 PeerInboundKind::Message { id, from, org, body } => {
451 assert_eq!(id, "q-7f3a92e0");
452 assert_eq!(from, "forge");
453 assert_eq!(org, "Personal");
454 assert_eq!(body, "What's the test setup?");
455 }
456 other => panic!("expected Message, got {other:?}"),
457 }
458
459 let reply = "[Reply id=q-7f3a92e0 from agent 'gateway-backend' (org 'Gateway') to your earlier ask]\n\nWe use pgtemp for ephemeral postgres in CI.";
460 match detect_inbound(reply).expect("a recorded reply still parses") {
461 PeerInboundKind::Message { from, org, body, .. } => {
462 assert_eq!(from, "gateway-backend");
463 assert_eq!(org, "Gateway");
464 assert_eq!(body, "We use pgtemp for ephemeral postgres in CI.");
465 }
466 other => panic!("expected Message, got {other:?}"),
467 }
468 }
469
470 #[test]
471 fn detect_delivery_failure_inbound() {
472 let text = "[Message to agent 'gateway-liq-bot' (org 'Gateway') failed to deliver: target spawn failed: all pinned accounts are rate-limited]";
473 let kind = detect_inbound(text).expect("delivery failure");
474 match kind {
475 PeerInboundKind::DeliveryFailure { target, org, reason } => {
476 assert_eq!(target, "gateway-liq-bot");
477 assert_eq!(org, "Gateway");
478 assert!(reason.contains("rate-limited"), "reason carries failure detail: {reason}");
479 }
480 other => panic!("expected DeliveryFailure, got {other:?}"),
481 }
482 }
483
484 #[test]
488 fn a_recorded_ask_failure_still_parses_as_a_delivery_failure() {
489 let text = "[Ask id=q-d31fa8a3 to agent 'gateway-liq-bot' (org 'Gateway') failed to deliver: target spawn failed]\n\n";
490 match detect_inbound(text).expect("a recorded failure still parses") {
491 PeerInboundKind::DeliveryFailure { target, org, reason } => {
492 assert_eq!(target, "gateway-liq-bot");
493 assert_eq!(org, "Gateway");
494 assert!(reason.contains("target spawn failed"), "reason: {reason}");
495 }
496 other => panic!("expected DeliveryFailure, got {other:?}"),
497 }
498 }
499
500 #[test]
501 fn detect_worker_spawn_failed_inbound() {
502 let text = "[Worker 'planner' spawn failed id=plan-7f3a: spawned with --worktree but git CLI not found]\n\n";
503 let kind = detect_inbound(text).expect("worker spawn failed");
504 match kind {
505 PeerInboundKind::WorkerSpawnFailed { label, reason } => {
506 assert_eq!(label, "planner");
507 assert!(reason.contains("git CLI not found"), "reason text: {reason}");
508 }
509 other => panic!("expected WorkerSpawnFailed, got {other:?}"),
510 }
511 }
512
513 #[test]
522 fn inbound_envelope_id_covers_every_header_shape() {
523 let cases: &[(&str, Option<&str>)] = &[
524 ("[Message id=m-1a2b from agent 'lead' (org 'forge')]\n\nbody", Some("m-1a2b")),
525 ("[Question id=q-1a2b from agent 'lead' (org 'forge')]\n\nbody", Some("q-1a2b")),
527 ("[Reply id=m-9f8e from agent 'lead' (org 'forge')]\n\nbody", Some("m-9f8e")),
528 ("[Ask id=q-77 to agent 'x' (org 'forge') failed to deliver: gone]\n\n", Some("q-77")),
530 ("[Message to agent 'x' (org 'forge') failed to deliver: gone]", None),
532 ("[Worker 'runner' spawn failed id=w-5: boom]", Some("w-5")),
533 ("[Worker 'grid=a' spawn failed id=w-5: boom]", Some("w-5")),
537 ("[Gotify - app 'ci', priority 5]\ntitle\nbody", None),
539 ("[Cron]\n\nprompt", None),
540 ("[Voice]\n\nnote", None),
541 ("plain user text", None),
543 ("[unterminated id=t-1", None),
544 ("[Message id= from agent 'lead' (org 'forge')]\n\nbody", None),
546 ];
547 for (text, want) in cases {
548 assert_eq!(inbound_envelope_id(text), *want, "for {text:?}");
549 }
550 }
551
552 #[test]
553 fn detect_inbound_rejects_non_peer_text() {
554 assert!(detect_inbound("plain user message").is_none());
555 assert!(detect_inbound("[not-a-peer-prefix]").is_none());
556 assert!(detect_inbound("[Question id=q-bad").is_none());
557 assert!(
560 detect_inbound("[Slack - workspace 'W', chan] id C1 ts 1.2").is_none(),
561 "a Slack header with no body line is not an envelope",
562 );
563 assert!(
564 detect_inbound("[Slack - workspace 'W', chan] id C1 ts 1.2\nno author prefix")
565 .is_none(),
566 "nor is a Slack body with no `<author>: ` separator",
567 );
568 }
569
570 #[test]
571 fn detect_gotify_inbound_parses_fields() {
572 let text = "[Gotify - app 'Backups', priority 3]\nNightly backup\nAll volumes done";
573 match detect_inbound(text).expect("gotify") {
574 PeerInboundKind::Gotify { app, title, message, priority } => {
575 assert_eq!(app, "Backups");
576 assert_eq!(title, "Nightly backup");
577 assert_eq!(message, "All volumes done");
578 assert_eq!(priority, 3);
579 }
580 other => panic!("expected Gotify, got {other:?}"),
581 }
582 }
583
584 #[test]
585 fn detect_gotify_inbound_preserves_multiline_message() {
586 let text = "[Gotify - app 'CI', priority 8]\nBuild failed\nstep 1 ok\nstep 2 failed";
587 match detect_inbound(text).expect("gotify") {
588 PeerInboundKind::Gotify { title, message, priority, .. } => {
589 assert_eq!(title, "Build failed");
590 assert_eq!(message, "step 1 ok\nstep 2 failed");
591 assert_eq!(priority, 8);
592 }
593 other => panic!("expected Gotify, got {other:?}"),
594 }
595 }
596
597 #[test]
598 fn detect_gotify_inbound_rejects_non_numeric_priority() {
599 assert!(detect_inbound("[Gotify - app 'X', priority high]\nt\nm").is_none());
602 }
603
604 #[test]
605 fn gotify_has_no_peer_sender_identity() {
606 let kind = PeerInboundKind::Gotify {
609 app: "Backups".into(),
610 title: "t".into(),
611 message: "m".into(),
612 priority: 5,
613 };
614 assert_eq!(kind.peer_sender_identity(), None);
615 }
616
617 #[test]
618 fn detect_cron_inbound_parses_prompt_body() {
619 let text = "[Cron]\n\nrun the morning summary";
620 match detect_inbound(text).expect("cron") {
621 PeerInboundKind::Cron { prompt } => assert_eq!(prompt, "run the morning summary"),
622 other => panic!("expected Cron, got {other:?}"),
623 }
624 }
625
626 #[test]
627 fn detect_cron_inbound_preserves_multiline_prompt() {
628 let text = "[Cron]\n\npost the EOD report\ninclude the realized PnL";
629 match detect_inbound(text).expect("cron") {
630 PeerInboundKind::Cron { prompt } => {
631 assert_eq!(prompt, "post the EOD report\ninclude the realized PnL");
632 }
633 other => panic!("expected Cron, got {other:?}"),
634 }
635 }
636
637 #[test]
638 fn detect_voice_inbound_parses_note_body() {
639 let listening = forge_workspace::voice::LISTENING_NOTE;
640 let text = format!("[Voice]\n\n{listening}");
641 match detect_inbound(&text).expect("voice") {
642 PeerInboundKind::Voice { note } => {
643 assert_eq!(note, listening, "the envelope carries the shipped note unchanged");
644 }
645 other => panic!("expected Voice, got {other:?}"),
646 }
647 }
648
649 #[test]
650 fn voice_has_no_peer_sender_identity() {
651 let kind = PeerInboundKind::Voice { note: "x".into() };
652 assert_eq!(kind.peer_sender_identity(), None);
653 }
654
655 #[test]
656 fn cron_has_no_peer_sender_identity() {
657 let kind = PeerInboundKind::Cron { prompt: "x".into() };
660 assert_eq!(kind.peer_sender_identity(), None);
661 }
662
663 #[test]
669 fn detect_slack_inbound_parses_header_and_drops_the_unknown_author() {
670 let text = "[Slack - workspace 'Trust Machines', granite-staging-alerts] id C0AE0CBJ77G ts 1789182982.499299\nunknown: _Large STX Transfer_\nAmount: 233468.293536 STX (~$60434.82 USD)";
671 match detect_inbound(text).expect("slack") {
672 PeerInboundKind::Slack { workspace, channel, author, body } => {
673 assert_eq!(workspace, "Trust Machines", "workspace off the header");
674 assert_eq!(channel, "granite-staging-alerts", "channel off the header");
675 assert_eq!(author, None, "`unknown` is not an author to print");
676 assert_eq!(
677 body,
678 "_Large STX Transfer_\nAmount: 233468.293536 STX (~$60434.82 USD)",
679 );
680 }
681 other => panic!("expected Slack, got {other:?}"),
682 }
683 }
684
685 #[test]
689 fn detect_slack_inbound_keeps_a_real_author() {
690 let text = "[Slack - workspace 'Subspace', mainnet-chain-alerts] id C0AE1 ts 1789.5\nalert-bot: Slow slot: 57296338 took 1s";
691 match detect_inbound(text).expect("slack") {
692 PeerInboundKind::Slack { workspace, channel, author, body } => {
693 assert_eq!(workspace, "Subspace");
694 assert_eq!(channel, "mainnet-chain-alerts");
695 assert_eq!(author.as_deref(), Some("alert-bot"));
696 assert_eq!(body, "Slow slot: 57296338 took 1s");
697 }
698 other => panic!("expected Slack, got {other:?}"),
699 }
700
701 let titled = "[Slack - workspace 'Subspace', mainnet-chain-alerts] id C0AE1 ts 1789.5\nGranite-Bot: Slow slot: 57296338 took 1s";
703 match detect_inbound(titled).expect("slack") {
704 PeerInboundKind::Slack { author, .. } => {
705 assert_eq!(
706 author.as_deref(),
707 Some("Granite-Bot"),
708 "a title-cased handle names someone"
709 );
710 }
711 other => panic!("expected Slack, got {other:?}"),
712 }
713 }
714
715 #[test]
719 fn detect_slack_inbound_drops_the_thread_suffix_with_the_header() {
720 let text = "[Slack - workspace 'W', chan] id C1 ts 1789.5 in thread 1789.4\nalert-bot: hi";
721 match detect_inbound(text).expect("slack") {
722 PeerInboundKind::Slack { channel, author, body, .. } => {
723 assert_eq!(channel, "chan");
724 assert_eq!(author.as_deref(), Some("alert-bot"));
725 assert_eq!(body, "hi");
726 }
727 other => panic!("expected Slack, got {other:?}"),
728 }
729 }
730
731 #[test]
735 fn slack_detection_leaves_gotify_and_peer_prose_alone() {
736 let gotify = "[Gotify - app 'Backups', priority 3]\nNightly backup\nAll volumes done";
737 assert!(
738 matches!(detect_inbound(gotify), Some(PeerInboundKind::Gotify { .. })),
739 "a Gotify notice still detects as Gotify",
740 );
741 let peer = "[Message id=t-1a2b from agent 'lead' (org 'forge')]\n\nbody";
742 assert!(
743 matches!(detect_inbound(peer), Some(PeerInboundKind::Message { .. })),
744 "a peer wrapper still detects as a peer envelope",
745 );
746 }
747
748 #[test]
749 fn slack_has_no_peer_sender_identity() {
750 let kind = PeerInboundKind::Slack {
753 workspace: "W".into(),
754 channel: "c".into(),
755 author: None,
756 body: "x".into(),
757 };
758 assert_eq!(kind.peer_sender_identity(), None);
759 }
760
761 #[test]
765 fn a_bundle_header_names_no_author() {
766 let prose = "[Slack - workspace 'acme', general] id C1 ts 2.2 (2 messages)\n\
767 alice: one [ts 1.1]\n\
768 bob: two [ts 2.2]";
769 match detect_inbound(prose).expect("slack") {
770 PeerInboundKind::Slack { author, body, .. } => {
771 assert_eq!(author, None, "a multi-member body names its own authors");
772 assert!(body.contains("alice: one"), "the body keeps every member: {body}");
773 }
774 other => panic!("expected Slack, got {other:?}"),
775 }
776 }
777
778 #[test]
781 fn a_stale_bundle_header_reads_as_the_same_block() {
782 let prose = "[Slack - workspace 'acme', general] id C1 ts 2.2, 8d old (2 messages)\n\
783 alice: one [ts 1.1]\n\
784 bob: two [ts 2.2]";
785 match detect_inbound(prose).expect("slack") {
786 PeerInboundKind::Slack { author, body, .. } => {
787 assert_eq!(author, None, "the age clause keeps the bundle shape");
788 assert!(body.contains("bob: two"), "the body keeps every member: {body}");
789 }
790 other => panic!("expected Slack, got {other:?}"),
791 }
792 }
793
794 #[test]
798 fn a_wrapped_single_message_is_not_read_as_a_bundle() {
799 let text = "[Slack - workspace 'acme', general] id C1 ts 1.1\n\
800 alice: line one\nline two [ts 1.1]";
801 match detect_inbound(text).expect("slack") {
802 PeerInboundKind::Slack { author, body, .. } => {
803 assert_eq!(author.as_deref(), Some("alice"), "one member keeps its header author");
804 assert_eq!(body, "line one\nline two [ts 1.1]");
805 }
806 other => panic!("expected Slack, got {other:?}"),
807 }
808 }
809
810 #[test]
813 fn detect_slack_inbound_drops_an_id_shaped_author() {
814 let text = "[Slack - workspace 'Trust Machines', granite-staging-alerts] id C0AE ts 1789.5\nU0AE0CBJ77G: Large STX Transfer";
815 match detect_inbound(text).expect("slack") {
816 PeerInboundKind::Slack { author, body, .. } => {
817 assert_eq!(author, None, "a `U…` id names nobody to print");
818 assert_eq!(body, "Large STX Transfer");
819 }
820 other => panic!("expected Slack, got {other:?}"),
821 }
822 }
823
824 #[test]
825 fn tidy_mrkdwn_strips_emphasis_markers() {
826 assert_eq!(tidy_mrkdwn("_Large STX Transfer_"), "Large STX Transfer");
827 assert_eq!(tidy_mrkdwn("*build failed*"), "build failed");
828 }
829
830 #[test]
831 fn tidy_mrkdwn_turns_a_labelled_link_into_label_colon_url() {
832 assert_eq!(
833 tidy_mrkdwn("View <https://explorer.hiro.so/txid/0x37599daf|Transaction>"),
834 "View Transaction: https://explorer.hiro.so/txid/0x37599daf",
835 );
836 }
837
838 #[test]
839 fn tidy_mrkdwn_leaves_a_plain_string_untouched() {
840 let plain = "Amount: 233468.293536 STX (~$60434.82 USD)";
841 assert_eq!(tidy_mrkdwn(plain), plain);
842 }
843
844 #[test]
845 fn tidy_mrkdwn_decodes_slack_entities() {
846 assert_eq!(tidy_mrkdwn("AT&T <ok>"), "AT&T <ok>");
847 assert_eq!(tidy_mrkdwn("&lt;"), "<");
850 }
851
852 #[test]
855 fn tidy_mrkdwn_leaves_a_link_url_whole() {
856 assert_eq!(
857 tidy_mrkdwn("<https://example.com/a_b|log_file>"),
858 "log_file: https://example.com/a_b",
859 );
860 }
861
862 #[test]
866 fn tidy_mrkdwn_leaves_a_marker_that_is_not_emphasis() {
867 assert_eq!(tidy_mrkdwn("set some_var_name here"), "set some_var_name here");
868 assert_eq!(tidy_mrkdwn("the _bold_suffix here"), "the _bold_suffix here");
869 assert_eq!(tidy_mrkdwn("**bold**"), "**bold**");
870 assert_eq!(tidy_mrkdwn("__init__"), "__init__");
871 assert_eq!(tidy_mrkdwn("src/**/*.rs"), "src/**/*.rs");
872 assert_eq!(tidy_mrkdwn("src/*.rs and tests/*.rs"), "src/*.rs and tests/*.rs");
873 }
874
875 #[test]
879 fn tidy_mrkdwn_leaves_an_angle_span_that_is_not_a_link() {
880 assert_eq!(tidy_mrkdwn("ping <@U123> at <https://x>"), "ping <@U123> at <https://x>");
881 assert_eq!(tidy_mrkdwn("a < b"), "a < b");
882 }
883}