agent_tui/daemon/usecases/
diagnostics.rs

1use std::sync::Arc;
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::time::Instant;
4
5use crate::common::mutex_lock_or_recover;
6use crate::terminal::PtyError;
7
8use crate::daemon::domain::{
9    ConsoleInput, ConsoleOutput, ErrorsInput, ErrorsOutput, HealthInput, HealthOutput,
10    MetricsInput, MetricsOutput, PtyReadInput, PtyReadOutput, PtyWriteInput, PtyWriteOutput,
11    TraceInput, TraceOutput,
12};
13use crate::daemon::error::SessionError;
14use crate::daemon::metrics::DaemonMetrics;
15use crate::daemon::repository::SessionRepository;
16
17pub trait TraceUseCase: Send + Sync {
18    fn execute(&self, input: TraceInput) -> Result<TraceOutput, SessionError>;
19}
20
21pub struct TraceUseCaseImpl<R: SessionRepository> {
22    repository: Arc<R>,
23}
24
25impl<R: SessionRepository> TraceUseCaseImpl<R> {
26    pub fn new(repository: Arc<R>) -> Self {
27        Self { repository }
28    }
29}
30
31impl<R: SessionRepository> TraceUseCase for TraceUseCaseImpl<R> {
32    fn execute(&self, input: TraceInput) -> Result<TraceOutput, SessionError> {
33        let session = self.repository.resolve(input.session_id.as_deref())?;
34        let session_guard = mutex_lock_or_recover(&session);
35
36        let count = if input.count == 0 { 1000 } else { input.count };
37        let entries = session_guard.get_trace_entries(count);
38
39        Ok(TraceOutput {
40            tracing: true,
41            entries,
42        })
43    }
44}
45
46pub trait ConsoleUseCase: Send + Sync {
47    fn execute(&self, input: ConsoleInput) -> Result<ConsoleOutput, SessionError>;
48}
49
50pub struct ConsoleUseCaseImpl<R: SessionRepository> {
51    repository: Arc<R>,
52}
53
54impl<R: SessionRepository> ConsoleUseCaseImpl<R> {
55    pub fn new(repository: Arc<R>) -> Self {
56        Self { repository }
57    }
58}
59
60impl<R: SessionRepository> ConsoleUseCase for ConsoleUseCaseImpl<R> {
61    fn execute(&self, input: ConsoleInput) -> Result<ConsoleOutput, SessionError> {
62        let session = self.repository.resolve(input.session_id.as_deref())?;
63        let mut session_guard = mutex_lock_or_recover(&session);
64
65        // Best-effort update - ignore errors since we still want to return console content
66        let _ = session_guard.update();
67
68        let screen_text = session_guard.screen_text();
69        let lines: Vec<String> = screen_text.lines().map(String::from).collect();
70
71        Ok(ConsoleOutput { lines })
72    }
73}
74
75pub trait ErrorsUseCase: Send + Sync {
76    fn execute(&self, input: ErrorsInput) -> Result<ErrorsOutput, SessionError>;
77}
78
79pub struct ErrorsUseCaseImpl<R: SessionRepository> {
80    repository: Arc<R>,
81}
82
83impl<R: SessionRepository> ErrorsUseCaseImpl<R> {
84    pub fn new(repository: Arc<R>) -> Self {
85        Self { repository }
86    }
87}
88
89impl<R: SessionRepository> ErrorsUseCase for ErrorsUseCaseImpl<R> {
90    fn execute(&self, input: ErrorsInput) -> Result<ErrorsOutput, SessionError> {
91        let session = self.repository.resolve(input.session_id.as_deref())?;
92        let session_guard = mutex_lock_or_recover(&session);
93
94        let count = if input.count == 0 { 1000 } else { input.count };
95        let errors = session_guard.get_errors(count);
96
97        Ok(ErrorsOutput {
98            total_count: errors.len(),
99            errors,
100        })
101    }
102}
103
104pub trait PtyReadUseCase: Send + Sync {
105    fn execute(&self, input: PtyReadInput) -> Result<PtyReadOutput, SessionError>;
106}
107
108pub struct PtyReadUseCaseImpl<R: SessionRepository> {
109    repository: Arc<R>,
110}
111
112impl<R: SessionRepository> PtyReadUseCaseImpl<R> {
113    pub fn new(repository: Arc<R>) -> Self {
114        Self { repository }
115    }
116}
117
118impl<R: SessionRepository> PtyReadUseCase for PtyReadUseCaseImpl<R> {
119    fn execute(&self, input: PtyReadInput) -> Result<PtyReadOutput, SessionError> {
120        let session = self.repository.resolve(input.session_id.as_deref())?;
121        let session_guard = mutex_lock_or_recover(&session);
122
123        let max_bytes = if input.max_bytes == 0 {
124            4096
125        } else {
126            input.max_bytes
127        };
128        let mut buf = vec![0u8; max_bytes];
129
130        match session_guard.pty_try_read(&mut buf, 100) {
131            Ok(bytes_read) => {
132                buf.truncate(bytes_read);
133                let data = String::from_utf8_lossy(&buf).to_string();
134                Ok(PtyReadOutput {
135                    session_id: session_guard.id.clone(),
136                    data,
137                    bytes_read,
138                })
139            }
140            Err(e) => Err(SessionError::Pty(PtyError::Read(e.to_string()))),
141        }
142    }
143}
144
145pub trait PtyWriteUseCase: Send + Sync {
146    fn execute(&self, input: PtyWriteInput) -> Result<PtyWriteOutput, SessionError>;
147}
148
149pub struct PtyWriteUseCaseImpl<R: SessionRepository> {
150    repository: Arc<R>,
151}
152
153impl<R: SessionRepository> PtyWriteUseCaseImpl<R> {
154    pub fn new(repository: Arc<R>) -> Self {
155        Self { repository }
156    }
157}
158
159impl<R: SessionRepository> PtyWriteUseCase for PtyWriteUseCaseImpl<R> {
160    fn execute(&self, input: PtyWriteInput) -> Result<PtyWriteOutput, SessionError> {
161        let session = self.repository.resolve(input.session_id.as_deref())?;
162        let session_guard = mutex_lock_or_recover(&session);
163
164        match session_guard.pty_write(input.data.as_bytes()) {
165            Ok(()) => Ok(PtyWriteOutput {
166                session_id: session_guard.id.clone(),
167                bytes_written: input.data.len(),
168                success: true,
169            }),
170            Err(e) => Err(SessionError::Pty(PtyError::Write(e.to_string()))),
171        }
172    }
173}
174
175pub trait HealthUseCase: Send + Sync {
176    fn execute(&self, input: HealthInput) -> Result<HealthOutput, SessionError>;
177}
178
179pub struct HealthUseCaseImpl<R: SessionRepository> {
180    repository: Arc<R>,
181    metrics: Arc<DaemonMetrics>,
182    start_time: Instant,
183    active_connections: Arc<AtomicUsize>,
184}
185
186impl<R: SessionRepository> HealthUseCaseImpl<R> {
187    pub fn new(
188        repository: Arc<R>,
189        metrics: Arc<DaemonMetrics>,
190        start_time: Instant,
191        active_connections: Arc<AtomicUsize>,
192    ) -> Self {
193        Self {
194            repository,
195            metrics,
196            start_time,
197            active_connections,
198        }
199    }
200}
201
202impl<R: SessionRepository> HealthUseCase for HealthUseCaseImpl<R> {
203    fn execute(&self, _input: HealthInput) -> Result<HealthOutput, SessionError> {
204        Ok(HealthOutput {
205            status: "healthy".to_string(),
206            pid: std::process::id(),
207            uptime_ms: self.start_time.elapsed().as_millis() as u64,
208            session_count: self.repository.session_count(),
209            version: env!("CARGO_PKG_VERSION").to_string(),
210            active_connections: self.active_connections.load(Ordering::Relaxed),
211            total_requests: self.metrics.requests(),
212            error_count: self.metrics.errors(),
213        })
214    }
215}
216
217pub trait MetricsUseCase: Send + Sync {
218    fn execute(&self, input: MetricsInput) -> Result<MetricsOutput, SessionError>;
219}
220
221pub struct MetricsUseCaseImpl<R: SessionRepository> {
222    repository: Arc<R>,
223    metrics: Arc<DaemonMetrics>,
224    start_time: Instant,
225    active_connections: Arc<AtomicUsize>,
226}
227
228impl<R: SessionRepository> MetricsUseCaseImpl<R> {
229    pub fn new(
230        repository: Arc<R>,
231        metrics: Arc<DaemonMetrics>,
232        start_time: Instant,
233        active_connections: Arc<AtomicUsize>,
234    ) -> Self {
235        Self {
236            repository,
237            metrics,
238            start_time,
239            active_connections,
240        }
241    }
242}
243
244impl<R: SessionRepository> MetricsUseCase for MetricsUseCaseImpl<R> {
245    fn execute(&self, _input: MetricsInput) -> Result<MetricsOutput, SessionError> {
246        Ok(MetricsOutput {
247            requests_total: self.metrics.requests(),
248            errors_total: self.metrics.errors(),
249            lock_timeouts: self.metrics.lock_timeouts(),
250            poison_recoveries: self.metrics.poison_recoveries(),
251            uptime_ms: self.start_time.elapsed().as_millis() as u64,
252            active_connections: self.active_connections.load(Ordering::Relaxed),
253            session_count: self.repository.session_count(),
254        })
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    use crate::daemon::domain::SessionId;
263    use crate::daemon::test_support::{MockError, MockSessionRepository};
264
265    #[test]
266    fn test_health_usecase_returns_correct_output() {
267        let repo = Arc::new(
268            MockSessionRepository::builder()
269                .with_session_count(5)
270                .build(),
271        );
272        let metrics = Arc::new(DaemonMetrics::new());
273        metrics.record_request();
274        metrics.record_request();
275        metrics.record_error();
276
277        let active_connections = Arc::new(AtomicUsize::new(3));
278        let start_time = Instant::now();
279
280        let usecase = HealthUseCaseImpl::new(repo, metrics, start_time, active_connections);
281
282        let output = usecase.execute(HealthInput).unwrap();
283
284        assert_eq!(output.status, "healthy");
285        assert_eq!(output.session_count, 5);
286        assert_eq!(output.active_connections, 3);
287        assert_eq!(output.total_requests, 2);
288        assert_eq!(output.error_count, 1);
289        assert!(!output.version.is_empty());
290    }
291
292    #[test]
293    fn test_metrics_usecase_returns_correct_output() {
294        let repo = Arc::new(
295            MockSessionRepository::builder()
296                .with_session_count(2)
297                .build(),
298        );
299        let metrics = Arc::new(DaemonMetrics::new());
300        metrics.record_request();
301        metrics.record_lock_timeout();
302        metrics.record_poison_recovery();
303
304        let active_connections = Arc::new(AtomicUsize::new(1));
305        let start_time = Instant::now();
306
307        let usecase = MetricsUseCaseImpl::new(repo, metrics, start_time, active_connections);
308
309        let output = usecase.execute(MetricsInput).unwrap();
310
311        assert_eq!(output.requests_total, 1);
312        assert_eq!(output.errors_total, 0);
313        assert_eq!(output.lock_timeouts, 1);
314        assert_eq!(output.poison_recoveries, 1);
315        assert_eq!(output.active_connections, 1);
316        assert_eq!(output.session_count, 2);
317    }
318
319    // ========================================================================
320    // TraceUseCase Tests (Error paths)
321    // ========================================================================
322
323    #[test]
324    fn test_trace_usecase_returns_error_when_no_active_session() {
325        let repo = Arc::new(MockSessionRepository::new());
326        let usecase = TraceUseCaseImpl::new(repo);
327
328        let input = TraceInput {
329            session_id: None,
330            start: false,
331            stop: false,
332            count: 100,
333        };
334
335        let result = usecase.execute(input);
336        assert!(matches!(result, Err(SessionError::NoActiveSession)));
337    }
338
339    #[test]
340    fn test_trace_usecase_returns_error_when_session_not_found() {
341        let repo = Arc::new(
342            MockSessionRepository::builder()
343                .with_resolve_error(MockError::NotFound("missing".to_string()))
344                .build(),
345        );
346        let usecase = TraceUseCaseImpl::new(repo);
347
348        let input = TraceInput {
349            session_id: Some(SessionId::new("missing")),
350            start: true,
351            stop: false,
352            count: 50,
353        };
354
355        let result = usecase.execute(input);
356        assert!(matches!(result, Err(SessionError::NotFound(_))));
357    }
358
359    // ========================================================================
360    // ConsoleUseCase Tests (Error paths)
361    // ========================================================================
362
363    #[test]
364    fn test_console_usecase_returns_error_when_no_active_session() {
365        let repo = Arc::new(MockSessionRepository::new());
366        let usecase = ConsoleUseCaseImpl::new(repo);
367
368        let input = ConsoleInput {
369            session_id: None,
370            count: 100,
371            clear: false,
372        };
373
374        let result = usecase.execute(input);
375        assert!(matches!(result, Err(SessionError::NoActiveSession)));
376    }
377
378    #[test]
379    fn test_console_usecase_returns_error_when_session_not_found() {
380        let repo = Arc::new(
381            MockSessionRepository::builder()
382                .with_resolve_error(MockError::NotFound("missing".to_string()))
383                .build(),
384        );
385        let usecase = ConsoleUseCaseImpl::new(repo);
386
387        let input = ConsoleInput {
388            session_id: Some(SessionId::new("missing")),
389            count: 50,
390            clear: true,
391        };
392
393        let result = usecase.execute(input);
394        assert!(matches!(result, Err(SessionError::NotFound(_))));
395    }
396
397    // ========================================================================
398    // ErrorsUseCase Tests (Error paths)
399    // ========================================================================
400
401    #[test]
402    fn test_errors_usecase_returns_error_when_no_active_session() {
403        let repo = Arc::new(MockSessionRepository::new());
404        let usecase = ErrorsUseCaseImpl::new(repo);
405
406        let input = ErrorsInput {
407            session_id: None,
408            count: 100,
409            clear: false,
410        };
411
412        let result = usecase.execute(input);
413        assert!(matches!(result, Err(SessionError::NoActiveSession)));
414    }
415
416    #[test]
417    fn test_errors_usecase_returns_error_when_session_not_found() {
418        let repo = Arc::new(
419            MockSessionRepository::builder()
420                .with_resolve_error(MockError::NotFound("missing".to_string()))
421                .build(),
422        );
423        let usecase = ErrorsUseCaseImpl::new(repo);
424
425        let input = ErrorsInput {
426            session_id: Some(SessionId::new("missing")),
427            count: 50,
428            clear: false,
429        };
430
431        let result = usecase.execute(input);
432        assert!(matches!(result, Err(SessionError::NotFound(_))));
433    }
434
435    // ========================================================================
436    // PtyReadUseCase Tests (Error paths)
437    // ========================================================================
438
439    #[test]
440    fn test_pty_read_usecase_returns_error_when_no_active_session() {
441        let repo = Arc::new(MockSessionRepository::new());
442        let usecase = PtyReadUseCaseImpl::new(repo);
443
444        let input = PtyReadInput {
445            session_id: None,
446            max_bytes: 4096,
447        };
448
449        let result = usecase.execute(input);
450        assert!(matches!(result, Err(SessionError::NoActiveSession)));
451    }
452
453    #[test]
454    fn test_pty_read_usecase_returns_error_when_session_not_found() {
455        let repo = Arc::new(
456            MockSessionRepository::builder()
457                .with_resolve_error(MockError::NotFound("missing".to_string()))
458                .build(),
459        );
460        let usecase = PtyReadUseCaseImpl::new(repo);
461
462        let input = PtyReadInput {
463            session_id: Some(SessionId::new("missing")),
464            max_bytes: 1024,
465        };
466
467        let result = usecase.execute(input);
468        assert!(matches!(result, Err(SessionError::NotFound(_))));
469    }
470
471    // ========================================================================
472    // PtyWriteUseCase Tests (Error paths)
473    // ========================================================================
474
475    #[test]
476    fn test_pty_write_usecase_returns_error_when_no_active_session() {
477        let repo = Arc::new(MockSessionRepository::new());
478        let usecase = PtyWriteUseCaseImpl::new(repo);
479
480        let input = PtyWriteInput {
481            session_id: None,
482            data: "hello".to_string(),
483        };
484
485        let result = usecase.execute(input);
486        assert!(matches!(result, Err(SessionError::NoActiveSession)));
487    }
488
489    #[test]
490    fn test_pty_write_usecase_returns_error_when_session_not_found() {
491        let repo = Arc::new(
492            MockSessionRepository::builder()
493                .with_resolve_error(MockError::NotFound("missing".to_string()))
494                .build(),
495        );
496        let usecase = PtyWriteUseCaseImpl::new(repo);
497
498        let input = PtyWriteInput {
499            session_id: Some(SessionId::new("missing")),
500            data: "test data".to_string(),
501        };
502
503        let result = usecase.execute(input);
504        assert!(matches!(result, Err(SessionError::NotFound(_))));
505    }
506}