agent_tui/daemon/usecases/
input.rs

1use std::sync::Arc;
2
3use crate::common::mutex_lock_or_recover;
4
5use crate::daemon::domain::{
6    KeydownInput, KeydownOutput, KeystrokeInput, KeystrokeOutput, KeyupInput, KeyupOutput,
7    TypeInput, TypeOutput,
8};
9use crate::daemon::error::SessionError;
10use crate::daemon::repository::SessionRepository;
11
12pub trait KeystrokeUseCase: Send + Sync {
13    fn execute(&self, input: KeystrokeInput) -> Result<KeystrokeOutput, SessionError>;
14}
15
16pub struct KeystrokeUseCaseImpl<R: SessionRepository> {
17    repository: Arc<R>,
18}
19
20impl<R: SessionRepository> KeystrokeUseCaseImpl<R> {
21    pub fn new(repository: Arc<R>) -> Self {
22        Self { repository }
23    }
24}
25
26impl<R: SessionRepository> KeystrokeUseCase for KeystrokeUseCaseImpl<R> {
27    fn execute(&self, input: KeystrokeInput) -> Result<KeystrokeOutput, SessionError> {
28        let session = self.repository.resolve(input.session_id.as_deref())?;
29        let session_guard = mutex_lock_or_recover(&session);
30
31        session_guard.keystroke(&input.key)?;
32
33        Ok(KeystrokeOutput { success: true })
34    }
35}
36
37pub trait TypeUseCase: Send + Sync {
38    fn execute(&self, input: TypeInput) -> Result<TypeOutput, SessionError>;
39}
40
41pub struct TypeUseCaseImpl<R: SessionRepository> {
42    repository: Arc<R>,
43}
44
45impl<R: SessionRepository> TypeUseCaseImpl<R> {
46    pub fn new(repository: Arc<R>) -> Self {
47        Self { repository }
48    }
49}
50
51impl<R: SessionRepository> TypeUseCase for TypeUseCaseImpl<R> {
52    fn execute(&self, input: TypeInput) -> Result<TypeOutput, SessionError> {
53        let session = self.repository.resolve(input.session_id.as_deref())?;
54        let session_guard = mutex_lock_or_recover(&session);
55
56        session_guard.type_text(&input.text)?;
57
58        Ok(TypeOutput { success: true })
59    }
60}
61
62pub trait KeydownUseCase: Send + Sync {
63    fn execute(&self, input: KeydownInput) -> Result<KeydownOutput, SessionError>;
64}
65
66pub struct KeydownUseCaseImpl<R: SessionRepository> {
67    repository: Arc<R>,
68}
69
70impl<R: SessionRepository> KeydownUseCaseImpl<R> {
71    pub fn new(repository: Arc<R>) -> Self {
72        Self { repository }
73    }
74}
75
76impl<R: SessionRepository> KeydownUseCase for KeydownUseCaseImpl<R> {
77    fn execute(&self, input: KeydownInput) -> Result<KeydownOutput, SessionError> {
78        let session = self.repository.resolve(input.session_id.as_deref())?;
79        let mut session_guard = mutex_lock_or_recover(&session);
80
81        session_guard.keydown(&input.key)?;
82
83        Ok(KeydownOutput { success: true })
84    }
85}
86
87pub trait KeyupUseCase: Send + Sync {
88    fn execute(&self, input: KeyupInput) -> Result<KeyupOutput, SessionError>;
89}
90
91pub struct KeyupUseCaseImpl<R: SessionRepository> {
92    repository: Arc<R>,
93}
94
95impl<R: SessionRepository> KeyupUseCaseImpl<R> {
96    pub fn new(repository: Arc<R>) -> Self {
97        Self { repository }
98    }
99}
100
101impl<R: SessionRepository> KeyupUseCase for KeyupUseCaseImpl<R> {
102    fn execute(&self, input: KeyupInput) -> Result<KeyupOutput, SessionError> {
103        let session = self.repository.resolve(input.session_id.as_deref())?;
104        let mut session_guard = mutex_lock_or_recover(&session);
105
106        session_guard.keyup(&input.key)?;
107
108        Ok(KeyupOutput { success: true })
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::daemon::domain::SessionId;
116    use crate::daemon::test_support::{MockError, MockSessionRepository};
117
118    // ========================================================================
119    // KeystrokeUseCase Tests (Error paths)
120    // ========================================================================
121
122    #[test]
123    fn test_keystroke_usecase_returns_error_when_no_active_session() {
124        let repo = Arc::new(MockSessionRepository::new());
125        let usecase = KeystrokeUseCaseImpl::new(repo);
126
127        let input = KeystrokeInput {
128            session_id: None,
129            key: "Enter".to_string(),
130        };
131
132        let result = usecase.execute(input);
133        assert!(matches!(result, Err(SessionError::NoActiveSession)));
134    }
135
136    #[test]
137    fn test_keystroke_usecase_returns_error_when_session_not_found() {
138        let repo = Arc::new(
139            MockSessionRepository::builder()
140                .with_resolve_error(MockError::NotFound("missing".to_string()))
141                .build(),
142        );
143        let usecase = KeystrokeUseCaseImpl::new(repo);
144
145        let input = KeystrokeInput {
146            session_id: Some(SessionId::new("missing")),
147            key: "Tab".to_string(),
148        };
149
150        let result = usecase.execute(input);
151        assert!(matches!(result, Err(SessionError::NotFound(_))));
152    }
153
154    // ========================================================================
155    // TypeUseCase Tests (Error paths)
156    // ========================================================================
157
158    #[test]
159    fn test_type_usecase_returns_error_when_no_active_session() {
160        let repo = Arc::new(MockSessionRepository::new());
161        let usecase = TypeUseCaseImpl::new(repo);
162
163        let input = TypeInput {
164            session_id: None,
165            text: "hello world".to_string(),
166        };
167
168        let result = usecase.execute(input);
169        assert!(matches!(result, Err(SessionError::NoActiveSession)));
170    }
171
172    #[test]
173    fn test_type_usecase_returns_error_when_session_not_found() {
174        let repo = Arc::new(
175            MockSessionRepository::builder()
176                .with_resolve_error(MockError::NotFound("missing".to_string()))
177                .build(),
178        );
179        let usecase = TypeUseCaseImpl::new(repo);
180
181        let input = TypeInput {
182            session_id: Some(SessionId::new("missing")),
183            text: "test text".to_string(),
184        };
185
186        let result = usecase.execute(input);
187        assert!(matches!(result, Err(SessionError::NotFound(_))));
188    }
189
190    // ========================================================================
191    // KeydownUseCase Tests (Error paths)
192    // ========================================================================
193
194    #[test]
195    fn test_keydown_usecase_returns_error_when_no_active_session() {
196        let repo = Arc::new(MockSessionRepository::new());
197        let usecase = KeydownUseCaseImpl::new(repo);
198
199        let input = KeydownInput {
200            session_id: None,
201            key: "Ctrl".to_string(),
202        };
203
204        let result = usecase.execute(input);
205        assert!(matches!(result, Err(SessionError::NoActiveSession)));
206    }
207
208    #[test]
209    fn test_keydown_usecase_returns_error_when_session_not_found() {
210        let repo = Arc::new(
211            MockSessionRepository::builder()
212                .with_resolve_error(MockError::NotFound("missing".to_string()))
213                .build(),
214        );
215        let usecase = KeydownUseCaseImpl::new(repo);
216
217        let input = KeydownInput {
218            session_id: Some(SessionId::new("missing")),
219            key: "Shift".to_string(),
220        };
221
222        let result = usecase.execute(input);
223        assert!(matches!(result, Err(SessionError::NotFound(_))));
224    }
225
226    // ========================================================================
227    // KeyupUseCase Tests (Error paths)
228    // ========================================================================
229
230    #[test]
231    fn test_keyup_usecase_returns_error_when_no_active_session() {
232        let repo = Arc::new(MockSessionRepository::new());
233        let usecase = KeyupUseCaseImpl::new(repo);
234
235        let input = KeyupInput {
236            session_id: None,
237            key: "Ctrl".to_string(),
238        };
239
240        let result = usecase.execute(input);
241        assert!(matches!(result, Err(SessionError::NoActiveSession)));
242    }
243
244    #[test]
245    fn test_keyup_usecase_returns_error_when_session_not_found() {
246        let repo = Arc::new(
247            MockSessionRepository::builder()
248                .with_resolve_error(MockError::NotFound("missing".to_string()))
249                .build(),
250        );
251        let usecase = KeyupUseCaseImpl::new(repo);
252
253        let input = KeyupInput {
254            session_id: Some(SessionId::new("missing")),
255            key: "Alt".to_string(),
256        };
257
258        let result = usecase.execute(input);
259        assert!(matches!(result, Err(SessionError::NotFound(_))));
260    }
261}