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