1use ralph_proto::{Event, HatId};
4use std::collections::HashMap;
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone, Default)]
14pub struct TaskSummary {
15 pub id: String,
17 pub title: String,
19 pub status: String,
21}
22
23impl TaskSummary {
24 pub fn new(id: impl Into<String>, title: impl Into<String>, status: impl Into<String>) -> Self {
26 Self {
27 id: id.into(),
28 title: title.into(),
29 status: status.into(),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Default)]
40pub struct TaskCounts {
41 pub total: usize,
43 pub open: usize,
45 pub closed: usize,
47 pub ready: usize,
49}
50
51impl TaskCounts {
52 pub fn new(total: usize, open: usize, closed: usize, ready: usize) -> Self {
54 Self {
55 total,
56 open,
57 closed,
58 ready,
59 }
60 }
61}
62
63#[derive(Debug, Default)]
70pub struct SearchState {
71 pub query: Option<String>,
73 pub matches: Vec<(usize, usize)>,
75 pub current_match: usize,
77 pub search_mode: bool,
79}
80
81impl SearchState {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn clear(&mut self) {
89 self.query = None;
90 self.matches.clear();
91 self.current_match = 0;
92 self.search_mode = false;
93 }
94}
95
96pub struct TuiState {
98 pub pending_hat: Option<(HatId, String)>,
100 pub pending_backend: Option<String>,
102 pub iteration: u32,
104 pub prev_iteration: u32,
106 pub loop_started: Option<Instant>,
108 pub iteration_started: Option<Instant>,
110 pub last_event: Option<String>,
112 pub last_event_at: Option<Instant>,
114 pub show_help: bool,
116 pub in_scroll_mode: bool,
118 pub search_query: String,
120 pub search_forward: bool,
122 pub max_iterations: Option<u32>,
124 pub idle_timeout_remaining: Option<Duration>,
126 hat_map: HashMap<String, (HatId, String)>,
130
131 pub iterations: Vec<IterationBuffer>,
136 pub current_view: usize,
138 pub following_latest: bool,
140 pub new_iteration_alert: Option<usize>,
143
144 pub search_state: SearchState,
149
150 pub loop_completed: bool,
155 pub final_iteration_elapsed: Option<Duration>,
157 pub final_loop_elapsed: Option<Duration>,
159
160 pub task_counts: TaskCounts,
165 pub active_task: Option<TaskSummary>,
167}
168
169impl TuiState {
170 pub fn new() -> Self {
172 Self {
173 pending_hat: None,
174 pending_backend: None,
175 iteration: 0,
176 prev_iteration: 0,
177 loop_started: Some(Instant::now()),
178 iteration_started: None,
179 last_event: None,
180 last_event_at: None,
181 show_help: false,
182 in_scroll_mode: false,
183 search_query: String::new(),
184 search_forward: true,
185 max_iterations: None,
186 idle_timeout_remaining: None,
187 hat_map: HashMap::new(),
188 iterations: Vec::new(),
190 current_view: 0,
191 following_latest: true,
192 new_iteration_alert: None,
193 search_state: SearchState::new(),
195 loop_completed: false,
197 final_iteration_elapsed: None,
198 final_loop_elapsed: None,
199 task_counts: TaskCounts::default(),
201 active_task: None,
202 }
203 }
204
205 pub fn with_hat_map(hat_map: HashMap<String, (HatId, String)>) -> Self {
208 Self {
209 pending_hat: None,
210 pending_backend: None,
211 iteration: 0,
212 prev_iteration: 0,
213 loop_started: Some(Instant::now()),
214 iteration_started: None,
215 last_event: None,
216 last_event_at: None,
217 show_help: false,
218 in_scroll_mode: false,
219 search_query: String::new(),
220 search_forward: true,
221 max_iterations: None,
222 idle_timeout_remaining: None,
223 hat_map,
224 iterations: Vec::new(),
226 current_view: 0,
227 following_latest: true,
228 new_iteration_alert: None,
229 search_state: SearchState::new(),
231 loop_completed: false,
233 final_iteration_elapsed: None,
234 final_loop_elapsed: None,
235 task_counts: TaskCounts::default(),
237 active_task: None,
238 }
239 }
240
241 pub fn update(&mut self, event: &Event) {
243 let now = Instant::now();
244 let topic = event.topic.as_str();
245
246 self.last_event = Some(topic.to_string());
247 self.last_event_at = Some(now);
248
249 let custom_hat = self.hat_map.get(topic).cloned();
250 if let Some((hat_id, hat_display)) = custom_hat.clone() {
251 self.pending_hat = Some((hat_id, hat_display));
252 if topic.starts_with("build.") {
254 self.iteration_started = Some(now);
255 }
256 }
257
258 match topic {
260 "task.start" => {
261 let saved_hat_map = std::mem::take(&mut self.hat_map);
263 let saved_loop_started = self.loop_started; let saved_max_iterations = self.max_iterations;
265 let saved_iterations = std::mem::take(&mut self.iterations);
267 let saved_current_view = self.current_view;
268 let saved_following_latest = self.following_latest;
269 let saved_new_iteration_alert = self.new_iteration_alert.take();
270 let saved_pending_backend = self.pending_backend.clone();
271 *self = Self::new();
272 self.hat_map = saved_hat_map;
273 self.loop_started = saved_loop_started; self.max_iterations = saved_max_iterations;
275 self.iterations = saved_iterations;
276 self.current_view = saved_current_view;
277 self.following_latest = saved_following_latest;
278 self.new_iteration_alert = saved_new_iteration_alert;
279 self.pending_backend = saved_pending_backend;
280 if let Some((hat_id, hat_display)) = custom_hat.clone() {
281 self.pending_hat = Some((hat_id, hat_display));
282 } else {
283 self.pending_hat = Some((HatId::new("planner"), "📋Planner".to_string()));
284 }
285 self.last_event = Some(topic.to_string());
286 self.last_event_at = Some(now);
287 }
288 "task.resume" => {
289 if custom_hat.is_none() {
291 self.pending_hat = Some((HatId::new("planner"), "📋Planner".to_string()));
292 }
293 }
294 "build.task" => {
295 if custom_hat.is_none() {
296 self.pending_hat = Some((HatId::new("builder"), "🔨Builder".to_string()));
297 }
298 self.final_loop_elapsed = None;
300 self.iteration_started = Some(now);
301 }
302 "build.done" => {
303 if custom_hat.is_none() {
304 self.pending_hat = Some((HatId::new("planner"), "📋Planner".to_string()));
305 }
306 self.prev_iteration = self.iteration;
307 self.iteration += 1;
308 self.finish_latest_iteration();
309 self.freeze_loop_elapsed();
310 }
311 "build.blocked" => {
312 if custom_hat.is_none() {
313 self.pending_hat = Some((HatId::new("planner"), "📋Planner".to_string()));
314 }
315 self.finish_latest_iteration();
316 self.freeze_loop_elapsed();
317 }
318 "loop.terminate" => {
319 self.pending_hat = None;
320 self.loop_completed = true;
321 self.final_iteration_elapsed = self.iteration_started.map(|start| start.elapsed());
323 self.freeze_loop_elapsed();
325 self.finish_latest_iteration();
326 }
327 _ => {
328 }
330 }
331 }
332
333 pub fn get_pending_hat_display(&self) -> String {
335 self.pending_hat
336 .as_ref()
337 .map_or_else(|| "—".to_string(), |(_, display)| display.clone())
338 }
339
340 pub fn get_loop_elapsed(&self) -> Option<Duration> {
342 if let Some(final_elapsed) = self.final_loop_elapsed {
343 return Some(final_elapsed);
344 }
345 self.loop_started.map(|start| start.elapsed())
346 }
347
348 pub fn get_iteration_elapsed(&self) -> Option<Duration> {
350 if let Some(buffer) = self.current_iteration() {
351 if let Some(elapsed) = buffer.elapsed {
352 return Some(elapsed);
353 }
354 if let Some(started_at) = buffer.started_at {
355 return Some(started_at.elapsed());
356 }
357 }
358 if let Some(final_elapsed) = self.final_iteration_elapsed {
359 return Some(final_elapsed);
360 }
361 self.iteration_started.map(|start| start.elapsed())
362 }
363
364 pub fn is_active(&self) -> bool {
366 self.last_event_at
367 .is_some_and(|t| t.elapsed() < Duration::from_secs(2))
368 }
369
370 pub fn iteration_changed(&self) -> bool {
372 self.iteration != self.prev_iteration
373 }
374
375 pub fn get_task_counts(&self) -> &TaskCounts {
381 &self.task_counts
382 }
383
384 pub fn get_active_task(&self) -> Option<&TaskSummary> {
386 self.active_task.as_ref()
387 }
388
389 pub fn set_task_counts(&mut self, counts: TaskCounts) {
391 self.task_counts = counts;
392 }
393
394 pub fn set_active_task(&mut self, task: Option<TaskSummary>) {
396 self.active_task = task;
397 }
398
399 pub fn has_open_tasks(&self) -> bool {
401 self.task_counts.open > 0
402 }
403
404 pub fn get_task_progress_display(&self) -> String {
406 if self.task_counts.total == 0 {
407 "No tasks".to_string()
408 } else {
409 format!(
410 "{}/{} tasks",
411 self.task_counts.closed, self.task_counts.total
412 )
413 }
414 }
415
416 pub fn start_new_iteration(&mut self) {
424 self.start_new_iteration_with_metadata(None, None);
425 }
426
427 pub fn start_new_iteration_with_metadata(
429 &mut self,
430 hat_display: Option<String>,
431 backend: Option<String>,
432 ) {
433 let hat_display = hat_display.or_else(|| {
434 self.pending_hat
435 .as_ref()
436 .map(|(_, display)| display.clone())
437 });
438 let backend = backend.or_else(|| self.pending_backend.clone());
439 let number = (self.iterations.len() + 1) as u32;
440 let mut buffer = IterationBuffer::new(number);
441 buffer.hat_display = hat_display;
442 buffer.backend = backend;
443 buffer.started_at = Some(Instant::now());
444 if buffer.backend.is_some() {
445 self.pending_backend = buffer.backend.clone();
446 }
447 self.iterations.push(buffer);
448
449 if self.following_latest {
451 self.current_view = self.iterations.len().saturating_sub(1);
452 } else {
453 self.new_iteration_alert = Some(number as usize);
455 }
456 }
457
458 pub fn finish_latest_iteration(&mut self) {
460 let Some(buffer) = self.iterations.last_mut() else {
461 return;
462 };
463 if buffer.elapsed.is_some() {
464 return;
465 }
466 if let Some(started_at) = buffer.started_at {
467 buffer.elapsed = Some(started_at.elapsed());
468 }
469 }
470
471 fn freeze_loop_elapsed(&mut self) {
473 if self.final_loop_elapsed.is_some() {
474 return;
475 }
476 self.final_loop_elapsed = self.loop_started.map(|start| start.elapsed());
477 }
478
479 pub fn current_iteration_hat_display(&self) -> Option<&str> {
481 self.current_iteration()
482 .and_then(|buffer| buffer.hat_display.as_deref())
483 }
484
485 pub fn current_iteration_backend(&self) -> Option<&str> {
487 self.current_iteration()
488 .and_then(|buffer| buffer.backend.as_deref())
489 }
490
491 pub fn current_iteration(&self) -> Option<&IterationBuffer> {
493 self.iterations.get(self.current_view)
494 }
495
496 pub fn current_iteration_mut(&mut self) -> Option<&mut IterationBuffer> {
498 self.iterations.get_mut(self.current_view)
499 }
500
501 pub fn current_iteration_lines_handle(
506 &self,
507 ) -> Option<std::sync::Arc<std::sync::Mutex<Vec<Line<'static>>>>> {
508 self.iterations
509 .get(self.current_view)
510 .map(|buffer| buffer.lines_handle())
511 }
512
513 pub fn latest_iteration_lines_handle(
520 &self,
521 ) -> Option<std::sync::Arc<std::sync::Mutex<Vec<Line<'static>>>>> {
522 self.iterations.last().map(|buffer| buffer.lines_handle())
523 }
524
525 pub fn navigate_next(&mut self) {
528 if self.iterations.is_empty() {
529 return;
530 }
531 let max_index = self.iterations.len().saturating_sub(1);
532 if self.current_view < max_index {
533 self.current_view += 1;
534 if self.current_view == max_index {
536 self.following_latest = true;
537 self.new_iteration_alert = None;
538 }
539 }
540 }
541
542 pub fn navigate_prev(&mut self) {
545 if self.current_view > 0 {
546 self.current_view -= 1;
547 self.following_latest = false;
548 }
549 }
550
551 pub fn total_iterations(&self) -> usize {
553 self.iterations.len()
554 }
555
556 pub fn search(&mut self, query: &str) {
564 self.search_state.query = Some(query.to_string());
565 self.search_state.matches.clear();
566 self.search_state.current_match = 0;
567
568 if self.iterations.get(self.current_view).is_none() {
570 return;
571 }
572
573 let query_lower = query.to_lowercase();
574
575 let matches: Vec<(usize, usize)> = self
577 .iterations
578 .get(self.current_view)
579 .and_then(|buffer| {
580 let lines = buffer.lines.lock().ok()?;
581 let mut found = Vec::new();
582 for (line_idx, line) in lines.iter().enumerate() {
583 let line_text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
585 let line_lower = line_text.to_lowercase();
586
587 let mut search_start = 0;
589 while let Some(pos) = line_lower[search_start..].find(&query_lower) {
590 let char_offset = search_start + pos;
591 found.push((line_idx, char_offset));
592 search_start = char_offset + query_lower.len();
593 }
594 }
595 Some(found)
596 })
597 .unwrap_or_default();
598
599 self.search_state.matches = matches;
600
601 if !self.search_state.matches.is_empty() {
603 self.jump_to_current_match();
604 }
605 }
606
607 pub fn next_match(&mut self) {
609 if self.search_state.matches.is_empty() {
610 return;
611 }
612
613 self.search_state.current_match =
614 (self.search_state.current_match + 1) % self.search_state.matches.len();
615 self.jump_to_current_match();
616 }
617
618 pub fn prev_match(&mut self) {
620 if self.search_state.matches.is_empty() {
621 return;
622 }
623
624 if self.search_state.current_match == 0 {
625 self.search_state.current_match = self.search_state.matches.len() - 1;
626 } else {
627 self.search_state.current_match -= 1;
628 }
629 self.jump_to_current_match();
630 }
631
632 pub fn clear_search(&mut self) {
634 self.search_state.clear();
635 }
636
637 fn jump_to_current_match(&mut self) {
639 if self.search_state.matches.is_empty() {
640 return;
641 }
642
643 let (line_idx, _) = self.search_state.matches[self.search_state.current_match];
644
645 let viewport_height = 20;
648 if let Some(buffer) = self.current_iteration_mut() {
649 if line_idx < buffer.scroll_offset {
651 buffer.scroll_offset = line_idx;
652 }
653 else if line_idx >= buffer.scroll_offset + viewport_height {
655 buffer.scroll_offset = line_idx.saturating_sub(viewport_height / 2);
656 }
657 }
658 }
659}
660
661impl Default for TuiState {
662 fn default() -> Self {
663 Self::new()
664 }
665}
666
667use ratatui::text::Line;
672use std::sync::{Arc, Mutex};
673
674pub struct IterationBuffer {
681 pub number: u32,
683 pub lines: Arc<Mutex<Vec<Line<'static>>>>,
685 pub scroll_offset: usize,
687 pub following_bottom: bool,
691 pub hat_display: Option<String>,
693 pub backend: Option<String>,
695 pub started_at: Option<Instant>,
697 pub elapsed: Option<Duration>,
699}
700
701impl IterationBuffer {
702 pub fn new(number: u32) -> Self {
704 Self {
705 number,
706 lines: Arc::new(Mutex::new(Vec::new())),
707 scroll_offset: 0,
708 following_bottom: true, hat_display: None,
710 backend: None,
711 started_at: None,
712 elapsed: None,
713 }
714 }
715
716 pub fn lines_handle(&self) -> Arc<Mutex<Vec<Line<'static>>>> {
721 Arc::clone(&self.lines)
722 }
723
724 pub fn append_line(&mut self, line: Line<'static>) {
726 if let Ok(mut lines) = self.lines.lock() {
727 lines.push(line);
728 }
729 }
730
731 pub fn line_count(&self) -> usize {
733 self.lines.lock().map(|l| l.len()).unwrap_or(0)
734 }
735
736 pub fn visible_lines(&self, viewport_height: usize) -> Vec<Line<'static>> {
740 let Ok(lines) = self.lines.lock() else {
741 return Vec::new();
742 };
743 if lines.is_empty() {
744 return Vec::new();
745 }
746 let start = self.scroll_offset.min(lines.len());
747 let end = (start + viewport_height).min(lines.len());
748 lines[start..end].to_vec()
749 }
750
751 pub fn scroll_up(&mut self) {
754 self.scroll_offset = self.scroll_offset.saturating_sub(1);
755 self.following_bottom = false;
756 }
757
758 pub fn scroll_down(&mut self, viewport_height: usize) {
761 let max_scroll = self.max_scroll_offset(viewport_height);
762 if self.scroll_offset < max_scroll {
763 self.scroll_offset += 1;
764 }
765 if self.scroll_offset >= max_scroll {
767 self.following_bottom = true;
768 }
769 }
770
771 pub fn scroll_top(&mut self) {
774 self.scroll_offset = 0;
775 self.following_bottom = false;
776 }
777
778 pub fn scroll_bottom(&mut self, viewport_height: usize) {
781 self.scroll_offset = self.max_scroll_offset(viewport_height);
782 self.following_bottom = true;
783 }
784
785 fn max_scroll_offset(&self, viewport_height: usize) -> usize {
787 self.lines
788 .lock()
789 .map(|l| l.len().saturating_sub(viewport_height))
790 .unwrap_or(0)
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797
798 mod iteration_buffer {
803 use super::*;
804 use ratatui::text::Line;
805
806 #[test]
807 fn new_creates_buffer_with_correct_initial_state() {
808 let buffer = IterationBuffer::new(1);
809 assert_eq!(buffer.number, 1);
810 assert_eq!(buffer.line_count(), 0);
811 assert_eq!(buffer.scroll_offset, 0);
812 }
813
814 #[test]
815 fn append_line_adds_lines_in_order() {
816 let mut buffer = IterationBuffer::new(1);
817 buffer.append_line(Line::from("first"));
818 buffer.append_line(Line::from("second"));
819 buffer.append_line(Line::from("third"));
820
821 assert_eq!(buffer.line_count(), 3);
822 let lines = buffer.lines.lock().unwrap();
824 assert_eq!(lines[0].spans[0].content, "first");
825 assert_eq!(lines[1].spans[0].content, "second");
826 assert_eq!(lines[2].spans[0].content, "third");
827 }
828
829 #[test]
830 fn line_count_returns_correct_count() {
831 let mut buffer = IterationBuffer::new(1);
832 assert_eq!(buffer.line_count(), 0);
833
834 for i in 0..10 {
835 buffer.append_line(Line::from(format!("line {}", i)));
836 }
837 assert_eq!(buffer.line_count(), 10);
838 }
839
840 #[test]
841 fn visible_lines_returns_correct_slice_without_scroll() {
842 let mut buffer = IterationBuffer::new(1);
843 for i in 0..10 {
844 buffer.append_line(Line::from(format!("line {}", i)));
845 }
846
847 let visible = buffer.visible_lines(5);
848 assert_eq!(visible.len(), 5);
849 assert_eq!(visible[0].spans[0].content, "line 0");
851 assert_eq!(visible[4].spans[0].content, "line 4");
852 }
853
854 #[test]
855 fn visible_lines_returns_correct_slice_with_scroll() {
856 let mut buffer = IterationBuffer::new(1);
857 for i in 0..10 {
858 buffer.append_line(Line::from(format!("line {}", i)));
859 }
860 buffer.scroll_offset = 3;
861
862 let visible = buffer.visible_lines(5);
863 assert_eq!(visible.len(), 5);
864 assert_eq!(visible[0].spans[0].content, "line 3");
866 assert_eq!(visible[4].spans[0].content, "line 7");
867 }
868
869 #[test]
870 fn visible_lines_handles_viewport_larger_than_content() {
871 let mut buffer = IterationBuffer::new(1);
872 for i in 0..3 {
873 buffer.append_line(Line::from(format!("line {}", i)));
874 }
875
876 let visible = buffer.visible_lines(10);
877 assert_eq!(visible.len(), 3); }
879
880 #[test]
881 fn visible_lines_handles_empty_buffer() {
882 let buffer = IterationBuffer::new(1);
883 let visible = buffer.visible_lines(5);
884 assert!(visible.is_empty());
885 }
886
887 #[test]
888 fn scroll_down_increases_offset() {
889 let mut buffer = IterationBuffer::new(1);
890 for i in 0..10 {
891 buffer.append_line(Line::from(format!("line {}", i)));
892 }
893
894 assert_eq!(buffer.scroll_offset, 0);
895 buffer.scroll_down(5); assert_eq!(buffer.scroll_offset, 1);
897 buffer.scroll_down(5);
898 assert_eq!(buffer.scroll_offset, 2);
899 }
900
901 #[test]
902 fn scroll_up_decreases_offset() {
903 let mut buffer = IterationBuffer::new(1);
904 for _ in 0..10 {
905 buffer.append_line(Line::from("line"));
906 }
907 buffer.scroll_offset = 5;
908
909 buffer.scroll_up();
910 assert_eq!(buffer.scroll_offset, 4);
911 buffer.scroll_up();
912 assert_eq!(buffer.scroll_offset, 3);
913 }
914
915 #[test]
916 fn scroll_up_does_not_underflow() {
917 let mut buffer = IterationBuffer::new(1);
918 buffer.append_line(Line::from("line"));
919 buffer.scroll_offset = 0;
920
921 buffer.scroll_up();
922 assert_eq!(buffer.scroll_offset, 0); }
924
925 #[test]
926 fn scroll_down_does_not_overflow() {
927 let mut buffer = IterationBuffer::new(1);
928 for _ in 0..10 {
929 buffer.append_line(Line::from("line"));
930 }
931 buffer.scroll_offset = 5;
933
934 buffer.scroll_down(5);
935 assert_eq!(buffer.scroll_offset, 5); }
937
938 #[test]
939 fn scroll_top_resets_to_zero() {
940 let mut buffer = IterationBuffer::new(1);
941 for _ in 0..10 {
942 buffer.append_line(Line::from("line"));
943 }
944 buffer.scroll_offset = 5;
945
946 buffer.scroll_top();
947 assert_eq!(buffer.scroll_offset, 0);
948 }
949
950 #[test]
951 fn scroll_bottom_sets_to_max() {
952 let mut buffer = IterationBuffer::new(1);
953 for _ in 0..10 {
954 buffer.append_line(Line::from("line"));
955 }
956
957 buffer.scroll_bottom(5); assert_eq!(buffer.scroll_offset, 5); }
960
961 #[test]
962 fn scroll_bottom_handles_small_content() {
963 let mut buffer = IterationBuffer::new(1);
964 for _ in 0..3 {
965 buffer.append_line(Line::from("line"));
966 }
967
968 buffer.scroll_bottom(5); assert_eq!(buffer.scroll_offset, 0); }
971
972 #[test]
973 fn scroll_down_handles_empty_buffer() {
974 let mut buffer = IterationBuffer::new(1);
975 buffer.scroll_down(5);
976 assert_eq!(buffer.scroll_offset, 0);
977 }
978
979 #[test]
984 fn following_bottom_is_true_initially() {
985 let buffer = IterationBuffer::new(1);
986 assert!(
987 buffer.following_bottom,
988 "New buffer should start with following_bottom = true"
989 );
990 }
991
992 #[test]
993 fn scroll_up_disables_following_bottom() {
994 let mut buffer = IterationBuffer::new(1);
995 for _ in 0..10 {
996 buffer.append_line(Line::from("line"));
997 }
998 buffer.scroll_offset = 5;
999 assert!(buffer.following_bottom);
1000
1001 buffer.scroll_up();
1002
1003 assert!(
1004 !buffer.following_bottom,
1005 "scroll_up should disable following_bottom"
1006 );
1007 }
1008
1009 #[test]
1010 fn scroll_top_disables_following_bottom() {
1011 let mut buffer = IterationBuffer::new(1);
1012 for _ in 0..10 {
1013 buffer.append_line(Line::from("line"));
1014 }
1015 assert!(buffer.following_bottom);
1016
1017 buffer.scroll_top();
1018
1019 assert!(
1020 !buffer.following_bottom,
1021 "scroll_top should disable following_bottom"
1022 );
1023 }
1024
1025 #[test]
1026 fn scroll_bottom_enables_following_bottom() {
1027 let mut buffer = IterationBuffer::new(1);
1028 for _ in 0..10 {
1029 buffer.append_line(Line::from("line"));
1030 }
1031 buffer.following_bottom = false;
1032
1033 buffer.scroll_bottom(5);
1034
1035 assert!(
1036 buffer.following_bottom,
1037 "scroll_bottom should enable following_bottom"
1038 );
1039 }
1040
1041 #[test]
1042 fn scroll_down_to_bottom_enables_following_bottom() {
1043 let mut buffer = IterationBuffer::new(1);
1044 for _ in 0..10 {
1045 buffer.append_line(Line::from("line"));
1046 }
1047 buffer.scroll_offset = 4; buffer.following_bottom = false;
1049
1050 buffer.scroll_down(5); assert!(
1053 buffer.following_bottom,
1054 "scroll_down to bottom should enable following_bottom"
1055 );
1056 }
1057
1058 #[test]
1059 fn scroll_down_not_at_bottom_keeps_following_false() {
1060 let mut buffer = IterationBuffer::new(1);
1061 for _ in 0..10 {
1062 buffer.append_line(Line::from("line"));
1063 }
1064 buffer.scroll_offset = 0;
1065 buffer.following_bottom = false;
1066
1067 buffer.scroll_down(5); assert!(
1070 !buffer.following_bottom,
1071 "scroll_down not reaching bottom should keep following_bottom false"
1072 );
1073 }
1074
1075 #[test]
1076 fn autoscroll_scenario_content_grows_past_viewport() {
1077 let mut buffer = IterationBuffer::new(1);
1079
1080 for _ in 0..5 {
1082 buffer.append_line(Line::from("line"));
1083 }
1084
1085 let viewport = 20;
1087 assert!(buffer.following_bottom);
1088 assert_eq!(buffer.scroll_offset, 0);
1089
1090 if buffer.following_bottom {
1092 let max_scroll = buffer.line_count().saturating_sub(viewport);
1093 buffer.scroll_offset = max_scroll;
1094 }
1095 assert_eq!(buffer.scroll_offset, 0); for _ in 0..25 {
1099 buffer.append_line(Line::from("more content"));
1100 }
1101 if buffer.following_bottom {
1106 let max_scroll = buffer.line_count().saturating_sub(viewport);
1107 buffer.scroll_offset = max_scroll;
1108 }
1109
1110 assert_eq!(
1112 buffer.scroll_offset, 10,
1113 "Auto-scroll should move to bottom when content grows past viewport"
1114 );
1115 }
1116 }
1117
1118 #[test]
1123 fn iteration_changed_detects_boundary() {
1124 let mut state = TuiState::new();
1125 assert!(!state.iteration_changed(), "no change at start");
1126
1127 let event = Event::new("build.done", "");
1129 state.update(&event);
1130
1131 assert_eq!(state.iteration, 1);
1132 assert_eq!(state.prev_iteration, 0);
1133 assert!(state.iteration_changed(), "should detect iteration change");
1134 }
1135
1136 #[test]
1137 fn iteration_changed_resets_after_check() {
1138 let mut state = TuiState::new();
1139 let event = Event::new("build.done", "");
1140 state.update(&event);
1141
1142 assert!(state.iteration_changed());
1143
1144 state.prev_iteration = state.iteration;
1146 assert!(!state.iteration_changed(), "flag should reset");
1147 }
1148
1149 #[test]
1150 fn multiple_iterations_tracked() {
1151 let mut state = TuiState::new();
1152
1153 for i in 1..=3 {
1154 let event = Event::new("build.done", "");
1155 state.update(&event);
1156 assert_eq!(state.iteration, i);
1157 assert!(state.iteration_changed());
1158 state.prev_iteration = state.iteration; }
1160 }
1161
1162 #[test]
1163 fn custom_hat_topics_update_pending_hat() {
1164 use std::collections::HashMap;
1166
1167 let mut hat_map = HashMap::new();
1169 hat_map.insert(
1170 "review.security".to_string(),
1171 (
1172 HatId::new("security_reviewer"),
1173 "🔒 Security Reviewer".to_string(),
1174 ),
1175 );
1176 hat_map.insert(
1177 "review.correctness".to_string(),
1178 (
1179 HatId::new("correctness_reviewer"),
1180 "🎯 Correctness Reviewer".to_string(),
1181 ),
1182 );
1183
1184 let mut state = TuiState::with_hat_map(hat_map);
1185
1186 let event = Event::new("review.security", "Review PR #123");
1188 state.update(&event);
1189
1190 assert_eq!(
1192 state.get_pending_hat_display(),
1193 "🔒 Security Reviewer",
1194 "Should display security reviewer hat for review.security topic"
1195 );
1196
1197 let event = Event::new("review.correctness", "Check logic");
1199 state.update(&event);
1200
1201 assert_eq!(
1203 state.get_pending_hat_display(),
1204 "🎯 Correctness Reviewer",
1205 "Should display correctness reviewer hat for review.correctness topic"
1206 );
1207 }
1208
1209 #[test]
1210 fn unknown_topics_keep_pending_hat_unchanged() {
1211 let mut state = TuiState::new();
1213
1214 state.pending_hat = Some((HatId::new("planner"), "📋Planner".to_string()));
1216
1217 let event = Event::new("unknown.topic", "Some payload");
1219 state.update(&event);
1220
1221 assert_eq!(
1223 state.get_pending_hat_display(),
1224 "📋Planner",
1225 "Unknown topics should not clear pending_hat"
1226 );
1227 }
1228
1229 #[test]
1230 fn task_start_preserves_iterations_across_reset() {
1231 let mut state = TuiState::new();
1235
1236 state.start_new_iteration();
1238 state.start_new_iteration();
1239 state.start_new_iteration();
1240 assert_eq!(state.total_iterations(), 3);
1241 assert_eq!(state.current_view, 2); state.navigate_prev();
1245 assert_eq!(state.current_view, 1);
1246 assert!(!state.following_latest);
1247
1248 let event = Event::new("task.start", "New task");
1250 state.update(&event);
1251
1252 assert_eq!(
1254 state.total_iterations(),
1255 3,
1256 "task.start should not wipe iteration buffers"
1257 );
1258 assert_eq!(
1259 state.current_view, 1,
1260 "task.start should preserve current_view position"
1261 );
1262 assert!(
1263 !state.following_latest,
1264 "task.start should preserve following_latest state"
1265 );
1266 }
1267
1268 #[test]
1269 fn loop_terminate_freezes_iteration_timer() {
1270 let mut state = TuiState::new();
1272 let start_event = Event::new("build.task", "");
1273 state.update(&start_event);
1274
1275 assert!(state.iteration_started.is_some());
1277 let elapsed_before = state.get_iteration_elapsed().unwrap();
1278 assert!(elapsed_before.as_nanos() > 0);
1279
1280 let terminate_event = Event::new("loop.terminate", "");
1282 state.update(&terminate_event);
1283
1284 assert!(state.loop_completed);
1286 assert!(state.final_iteration_elapsed.is_some());
1287
1288 let frozen_elapsed = state.get_iteration_elapsed().unwrap();
1290 std::thread::sleep(std::time::Duration::from_millis(10));
1291 let elapsed_after_sleep = state.get_iteration_elapsed().unwrap();
1292
1293 assert_eq!(
1294 frozen_elapsed, elapsed_after_sleep,
1295 "Timer should be frozen after loop.terminate"
1296 );
1297 }
1298
1299 #[test]
1300 fn loop_terminate_freezes_total_timer() {
1301 let mut state = TuiState::new();
1302 state.loop_started = Some(
1303 std::time::Instant::now()
1304 .checked_sub(std::time::Duration::from_secs(90))
1305 .unwrap(),
1306 );
1307
1308 let before = state.get_loop_elapsed().unwrap();
1309 assert!(before.as_secs() >= 90);
1310
1311 let terminate_event = Event::new("loop.terminate", "");
1312 state.update(&terminate_event);
1313
1314 let frozen = state.get_loop_elapsed().unwrap();
1315 std::thread::sleep(std::time::Duration::from_millis(10));
1316 let after = state.get_loop_elapsed().unwrap();
1317
1318 assert_eq!(
1319 frozen, after,
1320 "Loop elapsed time should be frozen after termination"
1321 );
1322 }
1323
1324 #[test]
1325 fn build_done_freezes_total_timer() {
1326 let mut state = TuiState::new();
1327 state.loop_started = Some(
1328 std::time::Instant::now()
1329 .checked_sub(std::time::Duration::from_secs(42))
1330 .unwrap(),
1331 );
1332
1333 let before = state.get_loop_elapsed().unwrap();
1334 assert!(before.as_secs() >= 42);
1335
1336 let done_event = Event::new("build.done", "");
1337 state.update(&done_event);
1338
1339 let frozen = state.get_loop_elapsed().unwrap();
1340 std::thread::sleep(std::time::Duration::from_millis(10));
1341 let after = state.get_loop_elapsed().unwrap();
1342
1343 assert_eq!(
1344 frozen, after,
1345 "Loop elapsed time should be frozen after build.done"
1346 );
1347 }
1348
1349 #[test]
1350 fn build_blocked_freezes_total_timer() {
1351 let mut state = TuiState::new();
1352 state.loop_started = Some(
1353 std::time::Instant::now()
1354 .checked_sub(std::time::Duration::from_secs(7))
1355 .unwrap(),
1356 );
1357
1358 let before = state.get_loop_elapsed().unwrap();
1359 assert!(before.as_secs() >= 7);
1360
1361 let blocked_event = Event::new("build.blocked", "");
1362 state.update(&blocked_event);
1363
1364 let frozen = state.get_loop_elapsed().unwrap();
1365 std::thread::sleep(std::time::Duration::from_millis(10));
1366 let after = state.get_loop_elapsed().unwrap();
1367
1368 assert_eq!(
1369 frozen, after,
1370 "Loop elapsed time should be frozen after build.blocked"
1371 );
1372 }
1373
1374 mod tui_state_iterations {
1379 use super::*;
1380
1381 #[test]
1382 fn start_new_iteration_creates_first_buffer() {
1383 let mut state = TuiState::new();
1385 assert_eq!(state.total_iterations(), 0);
1386
1387 state.start_new_iteration();
1389
1390 assert_eq!(state.total_iterations(), 1);
1392 assert_eq!(state.iterations[0].number, 1);
1393 }
1394
1395 #[test]
1396 fn start_new_iteration_creates_subsequent_buffers() {
1397 let mut state = TuiState::new();
1398 state.start_new_iteration();
1399 state.start_new_iteration();
1400 state.start_new_iteration();
1401
1402 assert_eq!(state.total_iterations(), 3);
1403 assert_eq!(state.iterations[0].number, 1);
1404 assert_eq!(state.iterations[1].number, 2);
1405 assert_eq!(state.iterations[2].number, 3);
1406 }
1407
1408 #[test]
1409 fn current_iteration_returns_correct_buffer() {
1410 let mut state = TuiState::new();
1412 state.start_new_iteration();
1413 state.start_new_iteration();
1414 state.start_new_iteration();
1415 state.current_view = 1;
1416
1417 let current = state.current_iteration();
1419
1420 assert!(current.is_some());
1422 assert_eq!(current.unwrap().number, 2);
1423 }
1424
1425 #[test]
1426 fn current_iteration_returns_none_when_empty() {
1427 let state = TuiState::new();
1428 assert!(state.current_iteration().is_none());
1429 }
1430
1431 #[test]
1432 fn current_iteration_mut_allows_modification() {
1433 let mut state = TuiState::new();
1434 state.start_new_iteration();
1435
1436 if let Some(buffer) = state.current_iteration_mut() {
1438 buffer.append_line(Line::from("test line"));
1439 }
1440
1441 assert_eq!(state.current_iteration().unwrap().line_count(), 1);
1443 }
1444
1445 #[test]
1446 fn navigate_next_increases_current_view() {
1447 let mut state = TuiState::new();
1449 state.start_new_iteration();
1450 state.start_new_iteration();
1451 state.start_new_iteration();
1452 state.current_view = 1;
1453 state.following_latest = false;
1454
1455 state.navigate_next();
1457
1458 assert_eq!(state.current_view, 2);
1460 }
1461
1462 #[test]
1463 fn navigate_prev_decreases_current_view() {
1464 let mut state = TuiState::new();
1466 state.start_new_iteration();
1467 state.start_new_iteration();
1468 state.start_new_iteration();
1469 state.current_view = 2;
1470
1471 state.navigate_prev();
1473
1474 assert_eq!(state.current_view, 1);
1476 }
1477
1478 #[test]
1479 fn navigate_next_does_not_exceed_bounds() {
1480 let mut state = TuiState::new();
1482 state.start_new_iteration();
1483 state.start_new_iteration();
1484 state.start_new_iteration();
1485 state.current_view = 2;
1486
1487 state.navigate_next();
1489
1490 assert_eq!(state.current_view, 2);
1492 }
1493
1494 #[test]
1495 fn navigate_prev_does_not_go_below_zero() {
1496 let mut state = TuiState::new();
1498 state.start_new_iteration();
1499 state.current_view = 0;
1500
1501 state.navigate_prev();
1503
1504 assert_eq!(state.current_view, 0);
1506 }
1507
1508 #[test]
1509 fn following_latest_initially_true() {
1510 let state = TuiState::new();
1513
1514 assert!(state.following_latest);
1516 }
1517
1518 #[test]
1519 fn following_latest_becomes_false_on_back_navigation() {
1520 let mut state = TuiState::new();
1522 state.start_new_iteration();
1523 state.start_new_iteration();
1524 state.start_new_iteration();
1525 state.current_view = 2;
1526 state.following_latest = true;
1527
1528 state.navigate_prev();
1530
1531 assert!(!state.following_latest);
1533 }
1534
1535 #[test]
1536 fn following_latest_restored_at_latest() {
1537 let mut state = TuiState::new();
1539 state.start_new_iteration();
1540 state.start_new_iteration();
1541 state.start_new_iteration();
1542 state.current_view = 1;
1543 state.following_latest = false;
1544
1545 state.navigate_next(); assert!(state.following_latest);
1550 }
1551
1552 #[test]
1553 fn total_iterations_reports_count() {
1554 let mut state = TuiState::new();
1556 state.start_new_iteration();
1557 state.start_new_iteration();
1558 state.start_new_iteration();
1559
1560 assert_eq!(state.total_iterations(), 3);
1563 }
1564
1565 #[test]
1566 fn start_new_iteration_auto_follows_latest() {
1567 let mut state = TuiState::new();
1568 state.following_latest = true;
1569 state.start_new_iteration();
1570 state.start_new_iteration();
1571
1572 assert_eq!(state.current_view, 1); }
1575
1576 #[test]
1581 fn per_iteration_scroll_independence() {
1582 let mut state = TuiState::new();
1584 state.start_new_iteration();
1585 state.start_new_iteration();
1586
1587 state.iterations[0].scroll_offset = 5;
1589 state.iterations[1].scroll_offset = 0;
1590
1591 state.current_view = 0;
1593 assert_eq!(
1594 state.current_iteration().unwrap().scroll_offset,
1595 5,
1596 "iteration 1 should have scroll_offset 5"
1597 );
1598
1599 state.navigate_next();
1600 assert_eq!(
1601 state.current_iteration().unwrap().scroll_offset,
1602 0,
1603 "iteration 2 should have scroll_offset 0"
1604 );
1605
1606 state.navigate_prev();
1608 assert_eq!(
1609 state.current_iteration().unwrap().scroll_offset,
1610 5,
1611 "iteration 1 should still have scroll_offset 5 after switching back"
1612 );
1613 }
1614
1615 #[test]
1616 fn scroll_within_iteration_does_not_affect_others() {
1617 let mut state = TuiState::new();
1619 state.start_new_iteration();
1620 state.start_new_iteration();
1621 state.start_new_iteration();
1622
1623 for i in 0..3 {
1625 for j in 0..20 {
1626 state.iterations[i].append_line(Line::from(format!(
1627 "iter {} line {}",
1628 i + 1,
1629 j
1630 )));
1631 }
1632 }
1633
1634 state.iterations[0].scroll_offset = 3;
1636 state.iterations[1].scroll_offset = 7;
1637 state.iterations[2].scroll_offset = 10;
1638
1639 state.current_view = 1;
1641 state.current_iteration_mut().unwrap().scroll_down(10);
1642
1643 assert_eq!(
1645 state.iterations[0].scroll_offset, 3,
1646 "iteration 1 unchanged"
1647 );
1648 assert_eq!(
1649 state.iterations[1].scroll_offset, 8,
1650 "iteration 2 scrolled down"
1651 );
1652 assert_eq!(
1653 state.iterations[2].scroll_offset, 10,
1654 "iteration 3 unchanged"
1655 );
1656 }
1657
1658 #[test]
1663 fn new_iteration_alert_set_when_not_following() {
1664 let mut state = TuiState::new();
1666 state.start_new_iteration(); state.start_new_iteration(); state.navigate_prev(); state.start_new_iteration(); assert_eq!(state.new_iteration_alert, Some(3));
1675 }
1676
1677 #[test]
1678 fn new_iteration_alert_not_set_when_following() {
1679 let mut state = TuiState::new();
1681 state.following_latest = true;
1682 state.start_new_iteration();
1683
1684 state.start_new_iteration();
1686
1687 assert_eq!(state.new_iteration_alert, None);
1689 }
1690
1691 #[test]
1692 fn alert_cleared_when_following_restored() {
1693 let mut state = TuiState::new();
1695 state.start_new_iteration();
1696 state.start_new_iteration();
1697 state.start_new_iteration();
1698 state.current_view = 0;
1699 state.following_latest = false;
1700 state.new_iteration_alert = Some(3);
1701
1702 state.navigate_next(); state.navigate_next(); assert_eq!(state.new_iteration_alert, None);
1708 }
1709
1710 #[test]
1711 fn alert_not_cleared_on_partial_navigation() {
1712 let mut state = TuiState::new();
1714 state.start_new_iteration();
1715 state.start_new_iteration();
1716 state.start_new_iteration();
1717 state.current_view = 0;
1718 state.following_latest = false;
1719 state.new_iteration_alert = Some(3);
1720
1721 state.navigate_next(); assert_eq!(state.new_iteration_alert, Some(3));
1726 assert!(!state.following_latest);
1727 }
1728
1729 #[test]
1730 fn alert_updates_for_multiple_new_iterations() {
1731 let mut state = TuiState::new();
1733 state.start_new_iteration(); state.start_new_iteration(); state.navigate_prev(); state.start_new_iteration(); assert_eq!(state.new_iteration_alert, Some(3));
1739
1740 state.start_new_iteration(); assert_eq!(state.new_iteration_alert, Some(4));
1745 }
1746 }
1747
1748 mod search_state {
1753 use super::*;
1754
1755 #[test]
1756 fn search_finds_matches_in_lines() {
1757 let mut state = TuiState::new();
1759 state.start_new_iteration();
1760 let buffer = state.current_iteration_mut().unwrap();
1761 buffer.append_line(Line::from("First error occurred"));
1762 buffer.append_line(Line::from("Normal line"));
1763 buffer.append_line(Line::from("Another error here"));
1764 buffer.append_line(Line::from("Final error message"));
1765
1766 state.search("error");
1768
1769 assert!(
1771 state.search_state.matches.len() >= 3,
1772 "expected at least 3 matches, got {}",
1773 state.search_state.matches.len()
1774 );
1775 assert_eq!(state.search_state.query, Some("error".to_string()));
1776 }
1777
1778 #[test]
1779 fn search_is_case_insensitive() {
1780 let mut state = TuiState::new();
1782 state.start_new_iteration();
1783 let buffer = state.current_iteration_mut().unwrap();
1784 buffer.append_line(Line::from("Error in uppercase"));
1785 buffer.append_line(Line::from("error in lowercase"));
1786 buffer.append_line(Line::from("ERROR all caps"));
1787
1788 state.search("error");
1790
1791 assert_eq!(
1793 state.search_state.matches.len(),
1794 3,
1795 "expected 3 case-insensitive matches"
1796 );
1797 }
1798
1799 #[test]
1800 fn next_match_cycles_forward() {
1801 let mut state = TuiState::new();
1803 state.start_new_iteration();
1804 let buffer = state.current_iteration_mut().unwrap();
1805 buffer.append_line(Line::from("match one"));
1806 buffer.append_line(Line::from("match two"));
1807 buffer.append_line(Line::from("match three"));
1808 state.search("match");
1809 state.search_state.current_match = 2;
1810
1811 state.next_match();
1813
1814 assert_eq!(state.search_state.current_match, 0);
1816 }
1817
1818 #[test]
1819 fn prev_match_cycles_backward() {
1820 let mut state = TuiState::new();
1822 state.start_new_iteration();
1823 let buffer = state.current_iteration_mut().unwrap();
1824 buffer.append_line(Line::from("match one"));
1825 buffer.append_line(Line::from("match two"));
1826 buffer.append_line(Line::from("match three"));
1827 state.search("match");
1828 state.search_state.current_match = 0;
1829
1830 state.prev_match();
1832
1833 assert_eq!(state.search_state.current_match, 2);
1835 }
1836
1837 #[test]
1838 fn search_jumps_to_match_line() {
1839 let mut state = TuiState::new();
1841 state.start_new_iteration();
1842 let buffer = state.current_iteration_mut().unwrap();
1843 for i in 0..60 {
1844 if i == 50 {
1845 buffer.append_line(Line::from("target match here"));
1846 } else {
1847 buffer.append_line(Line::from(format!("line {}", i)));
1848 }
1849 }
1850
1851 state.search("target");
1853
1854 let buffer = state.current_iteration().unwrap();
1856 assert!(
1858 buffer.scroll_offset <= 50,
1859 "scroll_offset {} should position line 50 in view",
1860 buffer.scroll_offset
1861 );
1862 }
1863
1864 #[test]
1865 fn clear_search_resets_state() {
1866 let mut state = TuiState::new();
1868 state.start_new_iteration();
1869 let buffer = state.current_iteration_mut().unwrap();
1870 buffer.append_line(Line::from("search term here"));
1871 state.search("term");
1872 assert!(state.search_state.query.is_some());
1873
1874 state.clear_search();
1876
1877 assert!(state.search_state.query.is_none());
1879 assert!(state.search_state.matches.is_empty());
1880 assert!(!state.search_state.search_mode);
1881 }
1882
1883 #[test]
1884 fn search_with_no_matches_sets_empty() {
1885 let mut state = TuiState::new();
1887 state.start_new_iteration();
1888 let buffer = state.current_iteration_mut().unwrap();
1889 buffer.append_line(Line::from("hello world"));
1890
1891 state.search("xyz");
1893
1894 assert_eq!(state.search_state.query, Some("xyz".to_string()));
1896 assert!(state.search_state.matches.is_empty());
1897 assert_eq!(state.search_state.current_match, 0);
1898 }
1899
1900 #[test]
1901 fn search_on_empty_iteration_handles_gracefully() {
1902 let mut state = TuiState::new();
1904 state.start_new_iteration();
1905
1906 state.search("anything");
1908
1909 assert!(state.search_state.matches.is_empty());
1911 }
1912
1913 #[test]
1914 fn next_match_with_no_matches_does_nothing() {
1915 let mut state = TuiState::new();
1917 state.start_new_iteration();
1918
1919 state.next_match();
1921
1922 assert_eq!(state.search_state.current_match, 0);
1924 }
1925
1926 #[test]
1927 fn multiple_matches_on_same_line() {
1928 let mut state = TuiState::new();
1930 state.start_new_iteration();
1931 let buffer = state.current_iteration_mut().unwrap();
1932 buffer.append_line(Line::from("error error error"));
1933
1934 state.search("error");
1936
1937 assert_eq!(
1939 state.search_state.matches.len(),
1940 3,
1941 "should find 3 matches on same line"
1942 );
1943 }
1944
1945 #[test]
1946 fn next_match_updates_scroll_to_show_match() {
1947 let mut state = TuiState::new();
1949 state.start_new_iteration();
1950 let buffer = state.current_iteration_mut().unwrap();
1951 for i in 0..100 {
1952 if i % 30 == 0 {
1953 buffer.append_line(Line::from("findme"));
1954 } else {
1955 buffer.append_line(Line::from(format!("line {}", i)));
1956 }
1957 }
1958 state.search("findme");
1959
1960 state.next_match();
1962
1963 let buffer = state.current_iteration().unwrap();
1965 assert!(buffer.scroll_offset <= 30, "scroll should show line 30");
1967 }
1968
1969 #[test]
1970 fn latest_iteration_lines_handle_returns_newest_iteration() {
1971 let mut state = TuiState::new();
1973 state.start_new_iteration(); state.start_new_iteration(); state.start_new_iteration(); state.current_view = 0;
1979 state.following_latest = false;
1980
1981 let current_handle = state.current_iteration_lines_handle();
1983 let latest_handle = state.latest_iteration_lines_handle();
1984
1985 assert!(current_handle.is_some());
1987 assert!(latest_handle.is_some());
1989
1990 {
1992 let latest = latest_handle.unwrap();
1993 latest
1994 .lock()
1995 .unwrap()
1996 .push(Line::from("output from iteration 3"));
1997 }
1998
1999 let current = state.current_iteration().unwrap();
2001 assert_eq!(
2002 current.lines.lock().unwrap().len(),
2003 0,
2004 "iteration 1 should have no lines"
2005 );
2006
2007 let latest_buffer = state.iterations.last().unwrap();
2009 assert_eq!(
2010 latest_buffer.lines.lock().unwrap().len(),
2011 1,
2012 "iteration 3 should have the output"
2013 );
2014 }
2015
2016 #[test]
2017 fn output_goes_to_correct_iteration_when_user_reviewing_history() {
2018 let mut state = TuiState::new();
2020
2021 for _ in 0..6 {
2023 state.start_new_iteration();
2024 }
2025
2026 state.current_view = 2;
2028 state.following_latest = false;
2029
2030 state.start_new_iteration();
2032
2033 let lines_handle = state.latest_iteration_lines_handle();
2035
2036 {
2038 let handle = lines_handle.unwrap();
2039 handle
2040 .lock()
2041 .unwrap()
2042 .push(Line::from("iteration 7 output"));
2043 }
2044
2045 let iteration_3 = &state.iterations[2];
2047 assert_eq!(
2048 iteration_3.lines.lock().unwrap().len(),
2049 0,
2050 "iteration 3 (being viewed) should have no output"
2051 );
2052
2053 let iteration_7 = state.iterations.last().unwrap();
2055 assert_eq!(
2056 iteration_7.lines.lock().unwrap().len(),
2057 1,
2058 "iteration 7 (latest) should have the output"
2059 );
2060 }
2061 }
2062}