agent_tui/daemon/usecases/
wait.rs1use 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 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 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 #[test]
113 fn test_wait_usecase_can_be_constructed_with_mock_sleeper() {
114 let repo = Arc::new(MockSessionRepository::new());
117 let mock_sleeper = MockSleeper::new();
118 let _usecase = WaitUseCaseImpl::with_sleeper(repo, mock_sleeper);
119 }
121
122 #[test]
123 fn test_wait_usecase_default_uses_real_sleeper() {
124 let repo = Arc::new(MockSessionRepository::new());
126 let _usecase: WaitUseCaseImpl<_, RealSleeper> = WaitUseCaseImpl::new(repo);
127 }
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}