agent_tui/daemon/usecases/
wait.rs

1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use crate::common::mutex_lock_or_recover;
5
6use crate::daemon::domain::{WaitInput, WaitOutput};
7use crate::daemon::error::SessionError;
8use crate::daemon::repository::SessionRepository;
9use crate::daemon::sleeper::{RealSleeper, Sleeper};
10use crate::daemon::wait::{StableTracker, WaitCondition, check_condition};
11
12pub trait WaitUseCase: Send + Sync {
13    fn execute(&self, input: WaitInput) -> Result<WaitOutput, SessionError>;
14}
15
16pub struct WaitUseCaseImpl<R: SessionRepository, S: Sleeper = RealSleeper> {
17    repository: Arc<R>,
18    sleeper: S,
19}
20
21impl<R: SessionRepository> WaitUseCaseImpl<R, RealSleeper> {
22    /// Create a new WaitUseCaseImpl with the default RealSleeper.
23    pub fn new(repository: Arc<R>) -> Self {
24        Self {
25            repository,
26            sleeper: RealSleeper,
27        }
28    }
29}
30
31impl<R: SessionRepository, S: Sleeper> WaitUseCaseImpl<R, S> {
32    /// Create a new WaitUseCaseImpl with a custom sleeper.
33    /// Use this for testing with MockSleeper.
34    pub fn with_sleeper(repository: Arc<R>, sleeper: S) -> Self {
35        Self {
36            repository,
37            sleeper,
38        }
39    }
40}
41
42impl<R: SessionRepository, S: Sleeper> WaitUseCase for WaitUseCaseImpl<R, S> {
43    fn execute(&self, input: WaitInput) -> Result<WaitOutput, SessionError> {
44        let session = self.repository.resolve(input.session_id.as_deref())?;
45        let timeout = Duration::from_millis(input.timeout_ms);
46        let start = Instant::now();
47
48        let condition = match (input.condition.as_deref(), input.text.as_ref()) {
49            (Some("stable"), _) => WaitCondition::Stable,
50            (Some("element"), Some(target)) => WaitCondition::Element(target.clone()),
51            (Some("focused"), Some(target)) => WaitCondition::Focused(target.clone()),
52            (Some("not_visible"), Some(target)) => WaitCondition::NotVisible(target.clone()),
53            (Some("text_gone"), Some(target)) => WaitCondition::TextGone(target.clone()),
54            (Some("value"), Some(target)) => {
55                if let Some((element_ref, expected)) = target.split_once('=') {
56                    WaitCondition::Value {
57                        element: element_ref.to_string(),
58                        expected: expected.to_string(),
59                    }
60                } else {
61                    WaitCondition::Text(target.clone())
62                }
63            }
64            (_, Some(text)) => WaitCondition::Text(text.clone()),
65            (None, None) => WaitCondition::Stable,
66            _ => WaitCondition::Stable,
67        };
68
69        let mut stable_tracker = StableTracker::new(3);
70        let poll_interval = Duration::from_millis(50);
71
72        loop {
73            {
74                let mut session_guard = mutex_lock_or_recover(&session);
75                session_guard.update()?;
76
77                if check_condition(&mut *session_guard, &condition, &mut stable_tracker) {
78                    return Ok(WaitOutput {
79                        found: true,
80                        elapsed_ms: start.elapsed().as_millis() as u64,
81                    });
82                }
83            }
84
85            if start.elapsed() >= timeout {
86                return Ok(WaitOutput {
87                    found: false,
88                    elapsed_ms: start.elapsed().as_millis() as u64,
89                });
90            }
91
92            self.sleeper.sleep(poll_interval);
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::daemon::domain::SessionId;
101    use crate::daemon::sleeper::MockSleeper;
102    use crate::daemon::test_support::{MockError, MockSessionRepository};
103
104    // ========================================================================
105    // WaitUseCase Tests (Error paths)
106    // ========================================================================
107
108    // ========================================================================
109    // MockSleeper Integration Tests
110    // ========================================================================
111
112    #[test]
113    fn test_wait_usecase_can_be_constructed_with_mock_sleeper() {
114        // This test demonstrates that WaitUseCaseImpl can be constructed
115        // with a MockSleeper, enabling deterministic tests without sleeping.
116        let repo = Arc::new(MockSessionRepository::new());
117        let mock_sleeper = MockSleeper::new();
118        let _usecase = WaitUseCaseImpl::with_sleeper(repo, mock_sleeper);
119        // Construction succeeds - the mock sleeper is injectable
120    }
121
122    #[test]
123    fn test_wait_usecase_default_uses_real_sleeper() {
124        // This test demonstrates that WaitUseCaseImpl::new() uses RealSleeper by default
125        let repo = Arc::new(MockSessionRepository::new());
126        let _usecase: WaitUseCaseImpl<_, RealSleeper> = WaitUseCaseImpl::new(repo);
127        // Type annotation confirms RealSleeper is the default
128    }
129
130    #[test]
131    fn test_wait_usecase_returns_error_when_no_active_session() {
132        let repo = Arc::new(MockSessionRepository::new());
133        let usecase = WaitUseCaseImpl::new(repo);
134
135        let input = WaitInput {
136            session_id: None,
137            text: Some("loading".to_string()),
138            timeout_ms: 5000,
139            condition: None,
140            target: None,
141        };
142
143        let result = usecase.execute(input);
144        assert!(matches!(result, Err(SessionError::NoActiveSession)));
145    }
146
147    #[test]
148    fn test_wait_usecase_returns_error_when_session_not_found() {
149        let repo = Arc::new(
150            MockSessionRepository::builder()
151                .with_resolve_error(MockError::NotFound("missing".to_string()))
152                .build(),
153        );
154        let usecase = WaitUseCaseImpl::new(repo);
155
156        let input = WaitInput {
157            session_id: Some(SessionId::new("missing")),
158            text: Some("ready".to_string()),
159            timeout_ms: 1000,
160            condition: None,
161            target: None,
162        };
163
164        let result = usecase.execute(input);
165        assert!(matches!(result, Err(SessionError::NotFound(_))));
166    }
167
168    #[test]
169    fn test_wait_usecase_returns_error_with_stable_condition() {
170        let repo = Arc::new(MockSessionRepository::new());
171        let usecase = WaitUseCaseImpl::new(repo);
172
173        let input = WaitInput {
174            session_id: None,
175            text: None,
176            timeout_ms: 5000,
177            condition: Some("stable".to_string()),
178            target: None,
179        };
180
181        let result = usecase.execute(input);
182        assert!(matches!(result, Err(SessionError::NoActiveSession)));
183    }
184
185    #[test]
186    fn test_wait_usecase_returns_error_with_element_condition() {
187        let repo = Arc::new(MockSessionRepository::new());
188        let usecase = WaitUseCaseImpl::new(repo);
189
190        let input = WaitInput {
191            session_id: None,
192            text: Some("@btn1".to_string()),
193            timeout_ms: 5000,
194            condition: Some("element".to_string()),
195            target: None,
196        };
197
198        let result = usecase.execute(input);
199        assert!(matches!(result, Err(SessionError::NoActiveSession)));
200    }
201}