agent_tui/daemon/usecases/
session.rs

1use std::sync::Arc;
2
3use crate::daemon::domain::{
4    AttachInput, AttachOutput, CleanupFailure, CleanupInput, CleanupOutput, KillOutput,
5    ResizeInput, ResizeOutput, RestartOutput, SessionInput, SessionsOutput, SpawnInput,
6    SpawnOutput,
7};
8use crate::daemon::error::{DomainError, SessionError};
9use crate::daemon::repository::SessionRepository;
10use crate::daemon::session::SessionId;
11
12/// Use case for creating new terminal sessions.
13///
14/// Single responsibility: create a new session. The terminal dimensions (cols/rows)
15/// are essential parameters for PTY creation - you cannot create a terminal without
16/// specifying its initial size. This is distinct from ResizeUseCase which changes
17/// dimensions of an existing session.
18pub trait SpawnUseCase: Send + Sync {
19    fn execute(&self, input: SpawnInput) -> Result<SpawnOutput, DomainError>;
20}
21
22pub struct SpawnUseCaseImpl<R: SessionRepository> {
23    repository: Arc<R>,
24}
25
26impl<R: SessionRepository> SpawnUseCaseImpl<R> {
27    pub fn new(repository: Arc<R>) -> Self {
28        Self { repository }
29    }
30}
31
32impl<R: SessionRepository> SpawnUseCase for SpawnUseCaseImpl<R> {
33    fn execute(&self, input: SpawnInput) -> Result<SpawnOutput, DomainError> {
34        // Convert domain SessionId to infrastructure String at the boundary
35        let session_id_str = input.session_id.map(|id| id.to_string());
36        let command = input.command.clone();
37
38        match self.repository.spawn(
39            &input.command,
40            &input.args,
41            input.cwd.as_deref(),
42            input.env.as_ref(),
43            session_id_str,
44            input.cols,
45            input.rows,
46        ) {
47            Ok((session_id, pid)) => Ok(SpawnOutput { session_id, pid }),
48            Err(SessionError::LimitReached(max)) => Err(DomainError::SessionLimitReached { max }),
49            Err(e) => {
50                // Classify spawn errors into specific domain errors
51                let err_str = e.to_string();
52                if err_str.contains("No such file") || err_str.contains("not found") {
53                    Err(DomainError::CommandNotFound { command })
54                } else if err_str.contains("Permission denied") {
55                    Err(DomainError::PermissionDenied { command })
56                } else {
57                    Err(DomainError::PtyError {
58                        operation: "spawn".to_string(),
59                        reason: err_str,
60                    })
61                }
62            }
63        }
64    }
65}
66
67pub trait KillUseCase: Send + Sync {
68    fn execute(&self, input: SessionInput) -> Result<KillOutput, SessionError>;
69}
70
71pub struct KillUseCaseImpl<R: SessionRepository> {
72    repository: Arc<R>,
73}
74
75impl<R: SessionRepository> KillUseCaseImpl<R> {
76    pub fn new(repository: Arc<R>) -> Self {
77        Self { repository }
78    }
79}
80
81impl<R: SessionRepository> KillUseCase for KillUseCaseImpl<R> {
82    fn execute(&self, input: SessionInput) -> Result<KillOutput, SessionError> {
83        let session = self.repository.resolve(input.session_id.as_deref())?;
84        let id = {
85            let guard = session.lock().unwrap();
86            SessionId::from(guard.id.as_str())
87        };
88
89        self.repository.kill(id.as_str())?;
90
91        Ok(KillOutput {
92            session_id: id,
93            success: true,
94        })
95    }
96}
97
98pub trait SessionsUseCase: Send + Sync {
99    fn execute(&self) -> SessionsOutput;
100}
101
102pub struct SessionsUseCaseImpl<R: SessionRepository> {
103    repository: Arc<R>,
104}
105
106impl<R: SessionRepository> SessionsUseCaseImpl<R> {
107    pub fn new(repository: Arc<R>) -> Self {
108        Self { repository }
109    }
110}
111
112impl<R: SessionRepository> SessionsUseCase for SessionsUseCaseImpl<R> {
113    fn execute(&self) -> SessionsOutput {
114        let sessions = self.repository.list();
115        let active_session = self.repository.active_session_id();
116
117        SessionsOutput {
118            sessions,
119            active_session,
120        }
121    }
122}
123
124pub trait RestartUseCase: Send + Sync {
125    fn execute(&self, input: SessionInput) -> Result<RestartOutput, SessionError>;
126}
127
128pub struct RestartUseCaseImpl<R: SessionRepository> {
129    repository: Arc<R>,
130}
131
132impl<R: SessionRepository> RestartUseCaseImpl<R> {
133    pub fn new(repository: Arc<R>) -> Self {
134        Self { repository }
135    }
136}
137
138impl<R: SessionRepository> RestartUseCase for RestartUseCaseImpl<R> {
139    fn execute(&self, input: SessionInput) -> Result<RestartOutput, SessionError> {
140        let session = self.repository.resolve(input.session_id.as_deref())?;
141
142        let (old_id, command, cols, rows) = {
143            let guard = session.lock().unwrap();
144            let (c, r) = guard.size();
145            (
146                SessionId::from(guard.id.as_str()),
147                guard.command.clone(),
148                c,
149                r,
150            )
151        };
152
153        self.repository.kill(old_id.as_str())?;
154
155        let (new_session_id, pid) =
156            self.repository
157                .spawn(&command, &[], None, None, None, cols, rows)?;
158
159        Ok(RestartOutput {
160            old_session_id: old_id,
161            new_session_id,
162            command,
163            pid,
164        })
165    }
166}
167
168pub trait AttachUseCase: Send + Sync {
169    fn execute(&self, input: AttachInput) -> Result<AttachOutput, SessionError>;
170}
171
172pub struct AttachUseCaseImpl<R: SessionRepository> {
173    repository: Arc<R>,
174}
175
176impl<R: SessionRepository> AttachUseCaseImpl<R> {
177    pub fn new(repository: Arc<R>) -> Self {
178        Self { repository }
179    }
180}
181
182impl<R: SessionRepository> AttachUseCase for AttachUseCaseImpl<R> {
183    fn execute(&self, input: AttachInput) -> Result<AttachOutput, SessionError> {
184        let session_id_str = input.session_id.to_string();
185        let session = self.repository.resolve(Some(&session_id_str))?;
186
187        let is_running = {
188            let mut guard = session.lock().unwrap();
189            guard.is_running()
190        };
191
192        if !is_running {
193            return Err(SessionError::NotFound(format!(
194                "{} (session not running)",
195                session_id_str
196            )));
197        }
198
199        self.repository.set_active(&session_id_str)?;
200
201        Ok(AttachOutput {
202            session_id: input.session_id,
203            success: true,
204            message: format!("Now attached to session {}", session_id_str),
205        })
206    }
207}
208
209pub trait ResizeUseCase: Send + Sync {
210    fn execute(&self, input: ResizeInput) -> Result<ResizeOutput, SessionError>;
211}
212
213pub struct ResizeUseCaseImpl<R: SessionRepository> {
214    repository: Arc<R>,
215}
216
217impl<R: SessionRepository> ResizeUseCaseImpl<R> {
218    pub fn new(repository: Arc<R>) -> Self {
219        Self { repository }
220    }
221}
222
223impl<R: SessionRepository> ResizeUseCase for ResizeUseCaseImpl<R> {
224    fn execute(&self, input: ResizeInput) -> Result<ResizeOutput, SessionError> {
225        let session = self.repository.resolve(input.session_id.as_deref())?;
226        let mut guard = session.lock().unwrap();
227
228        guard.resize(input.cols, input.rows)?;
229
230        Ok(ResizeOutput {
231            session_id: SessionId::from(guard.id.as_str()),
232            success: true,
233            cols: input.cols,
234            rows: input.rows,
235        })
236    }
237}
238
239/// Use case for cleaning up sessions.
240///
241/// Cleans up sessions based on the `all` flag:
242/// - If `all` is true, terminates all sessions
243/// - If `all` is false, terminates only non-running sessions
244pub trait CleanupUseCase: Send + Sync {
245    fn execute(&self, input: CleanupInput) -> CleanupOutput;
246}
247
248pub struct CleanupUseCaseImpl<R: SessionRepository> {
249    repository: Arc<R>,
250}
251
252impl<R: SessionRepository> CleanupUseCaseImpl<R> {
253    pub fn new(repository: Arc<R>) -> Self {
254        Self { repository }
255    }
256}
257
258impl<R: SessionRepository> CleanupUseCase for CleanupUseCaseImpl<R> {
259    fn execute(&self, input: CleanupInput) -> CleanupOutput {
260        let sessions = self.repository.list();
261        let mut cleaned = 0;
262        let mut failures = Vec::new();
263
264        for info in sessions {
265            // If not cleaning all, skip running sessions
266            let should_cleanup = input.all || !info.is_active();
267
268            if should_cleanup {
269                match self.repository.kill(info.id.as_str()) {
270                    Ok(_) => cleaned += 1,
271                    Err(e) => failures.push(CleanupFailure {
272                        session_id: info.id.clone(),
273                        error: e.to_string(),
274                    }),
275                }
276            }
277        }
278
279        CleanupOutput { cleaned, failures }
280    }
281}
282
283use crate::daemon::domain::{AssertConditionType, AssertInput, AssertOutput};
284
285/// Use case for asserting conditions.
286///
287/// Performs condition checks based on condition type:
288/// - Text: checks if text is visible on screen
289/// - Element: checks if element exists and is visible
290/// - Session: checks if session exists and is running
291pub trait AssertUseCase: Send + Sync {
292    fn execute(&self, input: AssertInput) -> Result<AssertOutput, SessionError>;
293}
294
295pub struct AssertUseCaseImpl<R: SessionRepository> {
296    repository: Arc<R>,
297}
298
299impl<R: SessionRepository> AssertUseCaseImpl<R> {
300    pub fn new(repository: Arc<R>) -> Self {
301        Self { repository }
302    }
303}
304
305impl<R: SessionRepository> AssertUseCase for AssertUseCaseImpl<R> {
306    fn execute(&self, input: AssertInput) -> Result<AssertOutput, SessionError> {
307        let condition = format!("{}:{}", input.condition_type.as_str(), input.value);
308
309        let passed = match input.condition_type {
310            AssertConditionType::Text => {
311                // Resolve session and check if text is visible
312                let session = self.repository.resolve(input.session_id.as_deref())?;
313                let mut guard = session.lock().unwrap();
314                guard.update()?;
315                let screen = guard.screen_text();
316                screen.contains(&input.value)
317            }
318            AssertConditionType::Element => {
319                // Resolve session and check if element exists
320                let session = self.repository.resolve(input.session_id.as_deref())?;
321                let mut guard = session.lock().unwrap();
322                guard.update()?;
323                guard.detect_elements();
324                guard.find_element(&input.value).is_some()
325            }
326            AssertConditionType::Session => {
327                // Check if session exists and is running
328                let sessions = self.repository.list();
329                sessions
330                    .iter()
331                    .any(|s| s.id.as_str() == input.value && s.is_active())
332            }
333        };
334
335        Ok(AssertOutput { passed, condition })
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::daemon::domain::SessionId;
343    use crate::daemon::session::SessionInfo;
344    use crate::daemon::test_support::{MockError, MockSessionRepository};
345    use std::collections::HashMap;
346
347    // ========================================================================
348    // SpawnUseCase Tests
349    // ========================================================================
350
351    #[test]
352    fn test_spawn_usecase_forwards_all_parameters_to_repository() {
353        let repo = Arc::new(
354            MockSessionRepository::builder()
355                .with_spawn_result("new-session", 12345)
356                .build(),
357        );
358        let usecase = SpawnUseCaseImpl::new(repo.clone());
359
360        let mut env = HashMap::new();
361        env.insert("FOO".to_string(), "bar".to_string());
362
363        let input = SpawnInput {
364            command: "bash".to_string(),
365            args: vec!["-c".to_string(), "echo hello".to_string()],
366            cwd: Some("/tmp".to_string()),
367            env: Some(env.clone()),
368            session_id: Some(SessionId::new("custom-id")),
369            cols: 120,
370            rows: 40,
371        };
372
373        let result = usecase.execute(input);
374        assert!(result.is_ok());
375
376        let params = repo.spawn_params();
377        assert_eq!(params.len(), 1);
378        assert_eq!(params[0].command, "bash");
379        assert_eq!(params[0].args, vec!["-c", "echo hello"]);
380        assert_eq!(params[0].cwd, Some("/tmp".to_string()));
381        assert_eq!(params[0].env, Some(env));
382        assert_eq!(params[0].session_id, Some("custom-id".to_string()));
383        assert_eq!(params[0].cols, 120);
384        assert_eq!(params[0].rows, 40);
385    }
386
387    #[test]
388    fn test_spawn_usecase_returns_session_id_and_pid() {
389        let repo = Arc::new(
390            MockSessionRepository::builder()
391                .with_spawn_result("test-session-123", 54321)
392                .build(),
393        );
394        let usecase = SpawnUseCaseImpl::new(repo);
395
396        let input = SpawnInput {
397            command: "vim".to_string(),
398            args: vec![],
399            cwd: None,
400            env: None,
401            session_id: None,
402            cols: 80,
403            rows: 24,
404        };
405
406        let result = usecase.execute(input).unwrap();
407        assert_eq!(result.session_id.as_str(), "test-session-123");
408        assert_eq!(result.pid, 54321);
409    }
410
411    #[test]
412    fn test_spawn_usecase_uses_default_cols_and_rows() {
413        let repo = Arc::new(
414            MockSessionRepository::builder()
415                .with_spawn_result("session", 1000)
416                .build(),
417        );
418        let usecase = SpawnUseCaseImpl::new(repo.clone());
419
420        let input = SpawnInput {
421            command: "cat".to_string(),
422            args: vec![],
423            cwd: None,
424            env: None,
425            session_id: None,
426            cols: 80,
427            rows: 24,
428        };
429
430        let _ = usecase.execute(input);
431
432        let params = repo.spawn_params();
433        assert_eq!(params[0].cols, 80);
434        assert_eq!(params[0].rows, 24);
435    }
436
437    #[test]
438    fn test_spawn_usecase_propagates_limit_reached_error() {
439        let repo = Arc::new(
440            MockSessionRepository::builder()
441                .with_spawn_error(MockError::LimitReached(16))
442                .build(),
443        );
444        let usecase = SpawnUseCaseImpl::new(repo);
445
446        let input = SpawnInput {
447            command: "bash".to_string(),
448            args: vec![],
449            cwd: None,
450            env: None,
451            session_id: None,
452            cols: 80,
453            rows: 24,
454        };
455
456        let result = usecase.execute(input);
457        assert!(matches!(
458            result,
459            Err(DomainError::SessionLimitReached { max: 16 })
460        ));
461    }
462
463    #[test]
464    fn test_spawn_usecase_custom_session_id_respected() {
465        let repo = Arc::new(
466            MockSessionRepository::builder()
467                .with_spawn_result("my-custom-session", 1)
468                .build(),
469        );
470        let usecase = SpawnUseCaseImpl::new(repo.clone());
471
472        let input = SpawnInput {
473            command: "bash".to_string(),
474            args: vec![],
475            cwd: None,
476            env: None,
477            session_id: Some(SessionId::new("my-custom-session")),
478            cols: 80,
479            rows: 24,
480        };
481
482        let result = usecase.execute(input).unwrap();
483        assert_eq!(result.session_id.as_str(), "my-custom-session");
484
485        let params = repo.spawn_params();
486        assert_eq!(params[0].session_id, Some("my-custom-session".to_string()));
487    }
488
489    // ========================================================================
490    // SpawnUseCase Error Classification Tests
491    // ========================================================================
492
493    #[test]
494    fn test_spawn_usecase_classifies_command_not_found_error() {
495        let repo = Arc::new(
496            MockSessionRepository::builder()
497                .with_spawn_error(MockError::Pty("No such file or directory".to_string()))
498                .build(),
499        );
500        let usecase = SpawnUseCaseImpl::new(repo);
501
502        let input = SpawnInput {
503            command: "nonexistent-command".to_string(),
504            args: vec![],
505            cwd: None,
506            env: None,
507            session_id: None,
508            cols: 80,
509            rows: 24,
510        };
511
512        let result = usecase.execute(input);
513        assert!(matches!(
514            result,
515            Err(DomainError::CommandNotFound { command }) if command == "nonexistent-command"
516        ));
517    }
518
519    #[test]
520    fn test_spawn_usecase_classifies_not_found_variant_error() {
521        let repo = Arc::new(
522            MockSessionRepository::builder()
523                .with_spawn_error(MockError::Pty("command not found".to_string()))
524                .build(),
525        );
526        let usecase = SpawnUseCaseImpl::new(repo);
527
528        let input = SpawnInput {
529            command: "missing-cmd".to_string(),
530            args: vec![],
531            cwd: None,
532            env: None,
533            session_id: None,
534            cols: 80,
535            rows: 24,
536        };
537
538        let result = usecase.execute(input);
539        assert!(matches!(
540            result,
541            Err(DomainError::CommandNotFound { command }) if command == "missing-cmd"
542        ));
543    }
544
545    #[test]
546    fn test_spawn_usecase_classifies_permission_denied_error() {
547        let repo = Arc::new(
548            MockSessionRepository::builder()
549                .with_spawn_error(MockError::Pty("Permission denied".to_string()))
550                .build(),
551        );
552        let usecase = SpawnUseCaseImpl::new(repo);
553
554        let input = SpawnInput {
555            command: "/etc/shadow".to_string(),
556            args: vec![],
557            cwd: None,
558            env: None,
559            session_id: None,
560            cols: 80,
561            rows: 24,
562        };
563
564        let result = usecase.execute(input);
565        assert!(matches!(
566            result,
567            Err(DomainError::PermissionDenied { command }) if command == "/etc/shadow"
568        ));
569    }
570
571    #[test]
572    fn test_spawn_usecase_classifies_generic_pty_error() {
573        let repo = Arc::new(
574            MockSessionRepository::builder()
575                .with_spawn_error(MockError::Pty("unknown error occurred".to_string()))
576                .build(),
577        );
578        let usecase = SpawnUseCaseImpl::new(repo);
579
580        let input = SpawnInput {
581            command: "some-command".to_string(),
582            args: vec![],
583            cwd: None,
584            env: None,
585            session_id: None,
586            cols: 80,
587            rows: 24,
588        };
589
590        let result = usecase.execute(input);
591        match result {
592            Err(DomainError::PtyError { operation, reason }) => {
593                assert_eq!(operation, "spawn");
594                assert!(reason.contains("unknown error"));
595            }
596            _ => panic!("Expected PtyError but got {:?}", result),
597        }
598    }
599
600    // ========================================================================
601    // SessionsUseCase Tests
602    // ========================================================================
603
604    #[test]
605    fn test_sessions_usecase_returns_empty_list_when_no_sessions() {
606        let repo = Arc::new(MockSessionRepository::new());
607        let usecase = SessionsUseCaseImpl::new(repo);
608
609        let result = usecase.execute();
610        assert!(result.sessions.is_empty());
611        assert!(result.active_session.is_none());
612    }
613
614    #[test]
615    fn test_sessions_usecase_returns_configured_sessions() {
616        let sessions = vec![
617            SessionInfo {
618                id: SessionId::new("session1"),
619                command: "bash".to_string(),
620                pid: 1001,
621                running: true,
622                created_at: "2024-01-01T00:00:00Z".to_string(),
623                size: (80, 24),
624            },
625            SessionInfo {
626                id: SessionId::new("session2"),
627                command: "vim".to_string(),
628                pid: 1002,
629                running: true,
630                created_at: "2024-01-01T01:00:00Z".to_string(),
631                size: (120, 40),
632            },
633        ];
634
635        let repo = Arc::new(
636            MockSessionRepository::builder()
637                .with_sessions(sessions)
638                .with_active_session("session1")
639                .build(),
640        );
641        let usecase = SessionsUseCaseImpl::new(repo);
642
643        let result = usecase.execute();
644        assert_eq!(result.sessions.len(), 2);
645        assert_eq!(result.sessions[0].id.as_str(), "session1");
646        assert_eq!(result.sessions[0].command, "bash");
647        assert_eq!(result.sessions[1].id.as_str(), "session2");
648        assert_eq!(result.sessions[1].command, "vim");
649        assert_eq!(result.active_session.unwrap().as_str(), "session1");
650    }
651
652    #[test]
653    fn test_sessions_usecase_returns_active_session_none_when_not_set() {
654        let sessions = vec![SessionInfo {
655            id: SessionId::new("orphan"),
656            command: "sleep".to_string(),
657            pid: 999,
658            running: true,
659            created_at: "2024-01-01T00:00:00Z".to_string(),
660            size: (80, 24),
661        }];
662
663        let repo = Arc::new(
664            MockSessionRepository::builder()
665                .with_sessions(sessions)
666                .build(),
667        );
668        let usecase = SessionsUseCaseImpl::new(repo);
669
670        let result = usecase.execute();
671        assert_eq!(result.sessions.len(), 1);
672        assert!(result.active_session.is_none());
673    }
674
675    // ========================================================================
676    // KillUseCase Tests (Error paths only - happy path needs real Session)
677    // ========================================================================
678
679    #[test]
680    fn test_kill_usecase_returns_error_when_no_active_session() {
681        let repo = Arc::new(MockSessionRepository::new());
682        let usecase = KillUseCaseImpl::new(repo);
683
684        let input = SessionInput { session_id: None };
685        let result = usecase.execute(input);
686        assert!(matches!(result, Err(SessionError::NoActiveSession)));
687    }
688
689    #[test]
690    fn test_kill_usecase_returns_error_when_session_not_found() {
691        let repo = Arc::new(
692            MockSessionRepository::builder()
693                .with_resolve_error(MockError::NotFound("nonexistent".to_string()))
694                .build(),
695        );
696        let usecase = KillUseCaseImpl::new(repo);
697
698        let input = SessionInput {
699            session_id: Some(SessionId::new("nonexistent")),
700        };
701        let result = usecase.execute(input);
702        assert!(matches!(result, Err(SessionError::NotFound(_))));
703    }
704
705    // ========================================================================
706    // RestartUseCase Tests (Error paths only - happy path needs real Session)
707    // ========================================================================
708
709    #[test]
710    fn test_restart_usecase_returns_error_when_no_active_session() {
711        let repo = Arc::new(MockSessionRepository::new());
712        let usecase = RestartUseCaseImpl::new(repo);
713
714        let input = SessionInput { session_id: None };
715        let result = usecase.execute(input);
716        assert!(matches!(result, Err(SessionError::NoActiveSession)));
717    }
718
719    #[test]
720    fn test_restart_usecase_returns_error_when_session_not_found() {
721        let repo = Arc::new(
722            MockSessionRepository::builder()
723                .with_resolve_error(MockError::NotFound("missing".to_string()))
724                .build(),
725        );
726        let usecase = RestartUseCaseImpl::new(repo);
727
728        let input = SessionInput {
729            session_id: Some(SessionId::new("missing")),
730        };
731        let result = usecase.execute(input);
732        assert!(matches!(result, Err(SessionError::NotFound(id)) if id == "missing"));
733    }
734
735    // ========================================================================
736    // AttachUseCase Tests (Error paths only - happy path needs real Session)
737    // ========================================================================
738
739    #[test]
740    fn test_attach_usecase_returns_error_when_session_not_found() {
741        let repo = Arc::new(MockSessionRepository::new());
742        let usecase = AttachUseCaseImpl::new(repo);
743
744        let input = AttachInput {
745            session_id: SessionId::new("nonexistent"),
746        };
747        let result = usecase.execute(input);
748        assert!(matches!(result, Err(SessionError::NotFound(_))));
749    }
750
751    #[test]
752    fn test_attach_usecase_returns_error_with_configured_error() {
753        let repo = Arc::new(
754            MockSessionRepository::builder()
755                .with_resolve_error(MockError::NotFound("target-session".to_string()))
756                .build(),
757        );
758        let usecase = AttachUseCaseImpl::new(repo);
759
760        let input = AttachInput {
761            session_id: SessionId::new("target-session"),
762        };
763        let result = usecase.execute(input);
764        assert!(matches!(result, Err(SessionError::NotFound(id)) if id == "target-session"));
765    }
766
767    // ========================================================================
768    // ResizeUseCase Tests (Error paths only - happy path needs real Session)
769    // ========================================================================
770
771    #[test]
772    fn test_resize_usecase_returns_error_when_no_active_session() {
773        let repo = Arc::new(MockSessionRepository::new());
774        let usecase = ResizeUseCaseImpl::new(repo);
775
776        let input = ResizeInput {
777            session_id: None,
778            cols: 120,
779            rows: 40,
780        };
781
782        let result = usecase.execute(input);
783        assert!(matches!(result, Err(SessionError::NoActiveSession)));
784    }
785
786    #[test]
787    fn test_resize_usecase_returns_error_when_session_not_found() {
788        let repo = Arc::new(
789            MockSessionRepository::builder()
790                .with_resolve_error(MockError::NotFound("unknown".to_string()))
791                .build(),
792        );
793        let usecase = ResizeUseCaseImpl::new(repo);
794
795        let input = ResizeInput {
796            session_id: Some(SessionId::new("unknown")),
797            cols: 80,
798            rows: 24,
799        };
800
801        let result = usecase.execute(input);
802        assert!(matches!(result, Err(SessionError::NotFound(_))));
803    }
804}