batty_cli/team/delivery/
mod.rs1mod routing;
2mod telegram;
3mod verification;
4
5use std::time::{Duration, Instant};
6
7use crate::tmux;
8
9pub(super) const DELIVERY_VERIFICATION_CAPTURE_LINES: u32 = 50;
10#[allow(dead_code)]
13pub(super) const DELIVERY_VERIFICATION_CAPTURE_LINES_RECENTLY_READY: u32 = 100;
14pub(super) const FAILED_DELIVERY_RETRY_DELAY: Duration = Duration::from_secs(30);
15pub(super) const FAILED_DELIVERY_MAX_ATTEMPTS: u32 = 3;
16
17pub(super) fn is_agent_ready(pane_id: &str) -> bool {
20 match tmux::capture_pane_recent(pane_id, 20) {
21 Ok(capture) => super::watcher::is_at_agent_prompt(&capture),
22 Err(_) => false,
23 }
24}
25
26#[derive(Debug, Clone)]
27pub(super) struct PendingMessage {
28 pub(super) from: String,
29 pub(super) body: String,
30 #[allow(dead_code)] pub(super) queued_at: Instant,
32}
33
34#[derive(Debug, Clone)]
35pub(super) struct FailedDelivery {
36 pub(super) recipient: String,
37 pub(super) from: String,
38 pub(super) body: String,
39 pub(super) attempts: u32,
40 pub(super) last_attempt: Instant,
41}
42
43impl FailedDelivery {
44 pub(super) fn new(recipient: &str, from: &str, body: &str) -> Self {
45 Self {
46 recipient: recipient.to_string(),
47 from: from.to_string(),
48 body: body.to_string(),
49 attempts: 1,
50 last_attempt: Instant::now(),
51 }
52 }
53
54 pub(super) fn message_marker(&self) -> String {
55 message_delivery_marker(&self.from)
56 }
57
58 fn is_ready_for_retry(&self, now: Instant) -> bool {
59 now.duration_since(self.last_attempt) >= FAILED_DELIVERY_RETRY_DELAY
60 }
61
62 fn has_attempts_remaining(&self) -> bool {
63 self.attempts < FAILED_DELIVERY_MAX_ATTEMPTS
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub(super) enum MessageDelivery {
69 Channel,
70 LivePane,
71 InboxQueued,
72 DeferredPending,
73 SkippedUnknownRecipient,
74}
75
76pub(super) fn message_delivery_marker(sender: &str) -> String {
77 format!("--- Message from {sender} ---")
78}
79
80pub(super) fn capture_contains_message_marker(capture: &str, message_marker: &str) -> bool {
81 capture.contains(message_marker)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn delivery_confirm_marker_detection_matches_captured_text() {
90 let marker = message_delivery_marker("manager");
91 let capture = format!("prompt\n{marker}\nbody\n");
92 assert!(capture_contains_message_marker(&capture, &marker));
93 assert!(!capture_contains_message_marker("prompt only", &marker));
94 }
95
96 #[test]
97 fn delivery_confirm_marker_generation_uses_sender_header() {
98 assert_eq!(
99 message_delivery_marker("eng-1-4"),
100 "--- Message from eng-1-4 ---"
101 );
102 }
103
104 #[test]
105 fn failed_delivery_new_sets_expected_fields() {
106 let delivery = FailedDelivery::new("eng-1", "manager", "Please retry this.");
107 assert_eq!(delivery.recipient, "eng-1");
108 assert_eq!(delivery.from, "manager");
109 assert_eq!(delivery.body, "Please retry this.");
110 assert_eq!(delivery.attempts, 1);
111 assert_eq!(delivery.message_marker(), "--- Message from manager ---");
112 assert!(delivery.has_attempts_remaining());
113 }
114
115 #[test]
116 fn message_delivery_variants_are_distinct() {
117 assert_ne!(MessageDelivery::Channel, MessageDelivery::LivePane);
118 assert_ne!(MessageDelivery::LivePane, MessageDelivery::InboxQueued);
119 assert_ne!(
120 MessageDelivery::InboxQueued,
121 MessageDelivery::SkippedUnknownRecipient
122 );
123 assert_eq!(MessageDelivery::Channel, MessageDelivery::Channel);
124 }
125
126 #[test]
127 fn delivery_verification_constants_are_sane() {
128 const {
129 assert!(
130 DELIVERY_VERIFICATION_CAPTURE_LINES_RECENTLY_READY
131 > DELIVERY_VERIFICATION_CAPTURE_LINES
132 );
133 assert!(DELIVERY_VERIFICATION_CAPTURE_LINES > 0);
134 assert!(FAILED_DELIVERY_MAX_ATTEMPTS >= 2);
135 }
136 assert!(FAILED_DELIVERY_RETRY_DELAY >= Duration::from_secs(1));
137 }
138
139 #[test]
140 fn is_agent_ready_returns_false_for_nonexistent_pane() {
141 assert!(!is_agent_ready("%99999999"));
142 }
143
144 #[test]
147 fn failed_delivery_is_not_ready_for_retry_when_recent() {
148 let delivery = FailedDelivery::new("eng-1", "manager", "test");
149 assert!(!delivery.is_ready_for_retry(Instant::now()));
151 }
152
153 #[test]
154 fn failed_delivery_is_ready_for_retry_after_delay() {
155 let mut delivery = FailedDelivery::new("eng-1", "manager", "test");
156 delivery.last_attempt =
157 Instant::now() - FAILED_DELIVERY_RETRY_DELAY - Duration::from_secs(1);
158 assert!(delivery.is_ready_for_retry(Instant::now()));
159 }
160
161 #[test]
162 fn failed_delivery_has_attempts_remaining_at_boundary() {
163 let mut delivery = FailedDelivery::new("eng-1", "manager", "test");
164 delivery.attempts = FAILED_DELIVERY_MAX_ATTEMPTS - 1;
165 assert!(delivery.has_attempts_remaining());
166 delivery.attempts = FAILED_DELIVERY_MAX_ATTEMPTS;
167 assert!(!delivery.has_attempts_remaining());
168 }
169
170 #[test]
171 fn failed_delivery_message_marker_uses_from_field() {
172 let delivery = FailedDelivery::new("eng-1", "architect", "body");
173 assert_eq!(delivery.message_marker(), "--- Message from architect ---");
174 }
175
176 #[test]
179 fn capture_contains_marker_empty_capture() {
180 assert!(!capture_contains_message_marker(
181 "",
182 "--- Message from x ---"
183 ));
184 }
185
186 #[test]
187 fn capture_contains_marker_partial_match_fails() {
188 let marker = message_delivery_marker("manager");
189 assert!(!capture_contains_message_marker(
190 "--- Message from",
191 &marker
192 ));
193 }
194
195 #[test]
196 fn capture_contains_marker_multiline_capture() {
197 let marker = message_delivery_marker("eng-1");
198 let capture = "line1\nline2\n--- Message from eng-1 ---\nline4\n";
199 assert!(capture_contains_message_marker(capture, &marker));
200 }
201
202 #[test]
205 fn failed_delivery_not_ready_for_immediate_retry() {
206 let fd = FailedDelivery::new("eng-1", "manager", "test message");
207 assert!(!fd.is_ready_for_retry(Instant::now()));
209 }
210
211 #[test]
212 fn failed_delivery_ready_after_delay() {
213 let mut fd = FailedDelivery::new("eng-1", "manager", "test message");
214 fd.last_attempt = Instant::now() - FAILED_DELIVERY_RETRY_DELAY - Duration::from_secs(1);
216 assert!(fd.is_ready_for_retry(Instant::now()));
217 }
218
219 #[test]
220 fn failed_delivery_has_attempts_remaining_exhausted() {
221 let mut fd = FailedDelivery::new("eng-1", "manager", "test message");
222 assert!(fd.has_attempts_remaining()); fd.attempts = FAILED_DELIVERY_MAX_ATTEMPTS;
224 assert!(!fd.has_attempts_remaining());
225 }
226
227 #[test]
228 fn failed_delivery_message_marker_format() {
229 let fd = FailedDelivery::new("eng-1", "manager", "test message");
230 let marker = fd.message_marker();
231 assert!(marker.contains("manager"));
232 }
233
234 #[test]
235 fn failed_delivery_fields_preserved() {
236 let fd = FailedDelivery::new("eng-1", "manager", "hello world");
237 assert_eq!(fd.recipient, "eng-1");
238 assert_eq!(fd.from, "manager");
239 assert_eq!(fd.body, "hello world");
240 assert_eq!(fd.attempts, 1);
241 }
242}