1use super::event::PipelineEvent;
6use super::state::{CommitState, PipelineState, RebaseState};
7
8pub fn reduce(state: PipelineState, event: PipelineEvent) -> PipelineState {
13 match event {
14 PipelineEvent::PipelineStarted => state,
15
16 PipelineEvent::PipelineResumed { .. } => state,
17
18 PipelineEvent::PipelineCompleted => PipelineState {
19 phase: super::event::PipelinePhase::Complete,
20 ..state
21 },
22
23 PipelineEvent::PipelineAborted { .. } => PipelineState {
24 phase: super::event::PipelinePhase::Interrupted,
25 ..state
26 },
27
28 PipelineEvent::PlanningPhaseStarted => PipelineState {
29 phase: super::event::PipelinePhase::Planning,
30 ..state
31 },
32
33 PipelineEvent::PlanningPhaseCompleted => PipelineState {
34 phase: super::event::PipelinePhase::Development,
35 ..state
36 },
37
38 PipelineEvent::DevelopmentPhaseStarted => PipelineState {
39 phase: super::event::PipelinePhase::Development,
40 ..state
41 },
42
43 PipelineEvent::DevelopmentIterationStarted { iteration } => PipelineState {
44 iteration,
45 agent_chain: state.agent_chain.reset(),
46 ..state
47 },
48
49 PipelineEvent::PlanGenerationStarted { .. } => state,
50
51 PipelineEvent::PlanGenerationCompleted { .. } => PipelineState {
52 phase: super::event::PipelinePhase::Development,
53 ..state
54 },
55
56 PipelineEvent::DevelopmentIterationCompleted {
57 iteration,
58 output_valid: _output_valid,
59 } => {
60 PipelineState {
63 phase: super::event::PipelinePhase::CommitMessage,
64 previous_phase: Some(super::event::PipelinePhase::Development),
65 iteration,
66 commit: super::state::CommitState::NotStarted,
67 context_cleaned: false,
68 ..state
69 }
70 }
71
72 PipelineEvent::DevelopmentPhaseCompleted => PipelineState {
73 phase: super::event::PipelinePhase::Review,
74 ..state
75 },
76
77 PipelineEvent::ReviewPhaseStarted => PipelineState {
78 phase: super::event::PipelinePhase::Review,
79 reviewer_pass: 0,
80 review_issues_found: false,
81 ..state
82 },
83
84 PipelineEvent::ReviewPassStarted { pass } => PipelineState {
85 reviewer_pass: pass,
86 review_issues_found: false, agent_chain: state.agent_chain.reset(),
88 ..state
89 },
90
91 PipelineEvent::ReviewCompleted { pass, issues_found } => {
92 let next_pass = if issues_found { pass } else { pass + 1 };
95
96 let next_phase = if !issues_found && next_pass >= state.total_reviewer_passes {
98 super::event::PipelinePhase::CommitMessage
99 } else {
100 state.phase
101 };
102
103 PipelineState {
104 phase: next_phase,
105 reviewer_pass: next_pass,
106 review_issues_found: issues_found,
107 ..state
108 }
109 }
110
111 PipelineEvent::FixAttemptStarted { .. } => PipelineState {
112 agent_chain: state.agent_chain.reset(),
113 ..state
114 },
115
116 PipelineEvent::FixAttemptCompleted { pass, .. } => {
117 PipelineState {
120 phase: super::event::PipelinePhase::CommitMessage,
121 previous_phase: Some(super::event::PipelinePhase::Review),
122 reviewer_pass: pass,
123 review_issues_found: false, commit: super::state::CommitState::NotStarted,
125 ..state
126 }
127 }
128
129 PipelineEvent::ReviewPhaseCompleted { .. } => PipelineState {
130 phase: super::event::PipelinePhase::CommitMessage,
131 ..state
132 },
133
134 PipelineEvent::AgentInvocationFailed {
135 retriable: true, ..
136 } => PipelineState {
137 agent_chain: state.agent_chain.advance_to_next_model(),
138 ..state
139 },
140
141 PipelineEvent::AgentFallbackTriggered { to_agent: _, .. } => PipelineState {
142 agent_chain: state.agent_chain.switch_to_next_agent(),
143 ..state
144 },
145
146 PipelineEvent::AgentChainExhausted { .. } => PipelineState {
147 agent_chain: state.agent_chain.start_retry_cycle(),
148 ..state
149 },
150
151 PipelineEvent::RebaseStarted {
152 target_branch,
153 phase: _,
154 } => PipelineState {
155 rebase: RebaseState::InProgress {
156 original_head: state.current_head(),
157 target_branch,
158 },
159 ..state
160 },
161
162 PipelineEvent::RebaseConflictDetected { files } => PipelineState {
163 rebase: match &state.rebase {
164 RebaseState::InProgress {
165 original_head,
166 target_branch,
167 } => RebaseState::Conflicted {
168 original_head: original_head.clone(),
169 target_branch: target_branch.clone(),
170 files,
171 resolution_attempts: 0,
172 },
173 _ => state.rebase.clone(),
174 },
175 ..state
176 },
177
178 PipelineEvent::RebaseConflictResolved { files: _ } => PipelineState {
179 rebase: match &state.rebase {
180 RebaseState::Conflicted {
181 original_head,
182 target_branch,
183 ..
184 } => RebaseState::InProgress {
185 original_head: original_head.clone(),
186 target_branch: target_branch.clone(),
187 },
188 _ => state.rebase.clone(),
189 },
190 ..state
191 },
192
193 PipelineEvent::RebaseSucceeded { new_head, phase: _ } => PipelineState {
194 rebase: RebaseState::Completed { new_head },
195 ..state
196 },
197
198 PipelineEvent::RebaseFailed { phase: _, .. } => PipelineState {
199 rebase: RebaseState::NotStarted,
200 ..state
201 },
202
203 PipelineEvent::RebaseSkipped { phase: _, .. } => PipelineState {
204 rebase: RebaseState::Skipped,
205 ..state
206 },
207
208 PipelineEvent::CommitGenerationStarted => PipelineState {
209 commit: CommitState::Generating {
210 attempt: 1,
211 max_attempts: super::state::MAX_VALIDATION_RETRY_ATTEMPTS,
212 },
213 ..state
214 },
215
216 PipelineEvent::CommitMessageGenerated { message, .. } => PipelineState {
217 commit: CommitState::Generated { message },
218 ..state
219 },
220
221 PipelineEvent::CommitCreated { hash, .. } => {
222 let (next_phase, next_iter, next_reviewer_pass) = match state.previous_phase {
224 Some(super::event::PipelinePhase::Development) => {
225 let next_iter = state.iteration + 1;
226 if next_iter >= state.total_iterations {
227 (
229 super::event::PipelinePhase::Review,
230 next_iter,
231 state.reviewer_pass,
232 )
233 } else {
234 (
236 super::event::PipelinePhase::Planning,
237 next_iter,
238 state.reviewer_pass,
239 )
240 }
241 }
242 Some(super::event::PipelinePhase::Review) => {
243 let next_pass = state.reviewer_pass + 1;
244 if next_pass >= state.total_reviewer_passes {
245 (
247 super::event::PipelinePhase::FinalValidation,
248 state.iteration,
249 next_pass,
250 )
251 } else {
252 (
254 super::event::PipelinePhase::Review,
255 state.iteration,
256 next_pass,
257 )
258 }
259 }
260 _ => {
261 (
263 super::event::PipelinePhase::FinalValidation,
264 state.iteration,
265 state.reviewer_pass,
266 )
267 }
268 };
269
270 PipelineState {
271 commit: CommitState::Committed { hash },
272 phase: next_phase,
273 previous_phase: None,
274 iteration: next_iter,
275 reviewer_pass: next_reviewer_pass,
276 context_cleaned: false, ..state
278 }
279 }
280
281 PipelineEvent::CommitGenerationFailed { .. } => PipelineState {
282 commit: CommitState::NotStarted,
283 ..state
284 },
285
286 PipelineEvent::CommitSkipped { .. } => {
287 let (next_phase, next_iter, next_reviewer_pass) = match state.previous_phase {
289 Some(super::event::PipelinePhase::Development) => {
290 let next_iter = state.iteration + 1;
291 if next_iter >= state.total_iterations {
292 (
294 super::event::PipelinePhase::Review,
295 next_iter,
296 state.reviewer_pass,
297 )
298 } else {
299 (
301 super::event::PipelinePhase::Planning,
302 next_iter,
303 state.reviewer_pass,
304 )
305 }
306 }
307 Some(super::event::PipelinePhase::Review) => {
308 let next_pass = state.reviewer_pass + 1;
309 if next_pass >= state.total_reviewer_passes {
310 (
312 super::event::PipelinePhase::FinalValidation,
313 state.iteration,
314 next_pass,
315 )
316 } else {
317 (
319 super::event::PipelinePhase::Review,
320 state.iteration,
321 next_pass,
322 )
323 }
324 }
325 _ => {
326 (
328 super::event::PipelinePhase::FinalValidation,
329 state.iteration,
330 state.reviewer_pass,
331 )
332 }
333 };
334
335 PipelineState {
336 commit: CommitState::Skipped,
337 phase: next_phase,
338 previous_phase: None,
339 iteration: next_iter,
340 reviewer_pass: next_reviewer_pass,
341 context_cleaned: false, ..state
343 }
344 }
345
346 PipelineEvent::ContextCleaned => PipelineState {
347 context_cleaned: true,
348 ..state
349 },
350
351 PipelineEvent::CheckpointSaved { .. } => state,
352
353 PipelineEvent::AgentInvocationStarted { .. } => state,
354 PipelineEvent::AgentInvocationSucceeded { .. } => state,
355 PipelineEvent::AgentInvocationFailed {
356 retriable: false, ..
357 } => PipelineState {
358 agent_chain: state.agent_chain.switch_to_next_agent(),
359 ..state
360 },
361 PipelineEvent::AgentModelFallbackTriggered { .. } => PipelineState {
362 agent_chain: state.agent_chain.advance_to_next_model(),
363 ..state
364 },
365 PipelineEvent::AgentRetryCycleStarted { .. } => state,
366 PipelineEvent::AgentChainInitialized { role, agents } => {
367 let models_per_agent = agents.iter().map(|_| vec![]).collect();
368
369 PipelineState {
370 agent_chain: state
371 .agent_chain
372 .with_agents(agents, models_per_agent, role)
373 .reset_for_role(role),
374 ..state
375 }
376 }
377 PipelineEvent::RebaseAborted { .. } => state,
378
379 PipelineEvent::CommitMessageValidationFailed { attempt, .. } => {
380 let next_attempt = attempt + 1;
382 let max_attempts = super::state::MAX_VALIDATION_RETRY_ATTEMPTS;
383
384 if next_attempt <= max_attempts {
385 PipelineState {
386 commit: CommitState::Generating {
387 attempt: next_attempt,
388 max_attempts,
389 },
390 ..state
391 }
392 } else {
393 let old_agent_index = state.agent_chain.current_agent_index;
395 let old_retry_cycle = state.agent_chain.retry_cycle;
396 let new_agent_chain = state.agent_chain.switch_to_next_agent();
397
398 let wrapped_around = new_agent_chain.retry_cycle > old_retry_cycle;
400
401 let advanced_to_next =
403 new_agent_chain.current_agent_index != old_agent_index && !wrapped_around;
404
405 if advanced_to_next {
406 PipelineState {
408 agent_chain: new_agent_chain,
409 commit: CommitState::Generating {
410 attempt: 1,
411 max_attempts,
412 },
413 ..state
414 }
415 } else {
416 PipelineState {
419 agent_chain: new_agent_chain,
420 commit: CommitState::NotStarted,
421 ..state
422 }
423 }
424 }
425 }
426
427 PipelineEvent::FinalizingStarted => PipelineState {
428 phase: super::event::PipelinePhase::Finalizing,
429 ..state
430 },
431
432 PipelineEvent::PromptPermissionsRestored => PipelineState {
433 phase: super::event::PipelinePhase::Complete,
434 ..state
435 },
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::agents::AgentRole;
443 use crate::reducer::event::AgentErrorKind;
444 use crate::reducer::event::PipelinePhase;
445 use crate::reducer::event::RebasePhase;
446 use crate::reducer::state::AgentChainState;
447
448 fn create_test_state() -> PipelineState {
449 PipelineState {
450 agent_chain: AgentChainState::initial().with_agents(
451 vec!["agent1".to_string(), "agent2".to_string()],
452 vec![vec!["model1".to_string(), "model2".to_string()]],
453 AgentRole::Developer,
454 ),
455 ..PipelineState::initial(5, 2)
456 }
457 }
458
459 #[test]
460 fn test_reduce_pipeline_started() {
461 let state = create_test_state();
462 let new_state = reduce(state, PipelineEvent::PipelineStarted);
463 assert_eq!(new_state.phase, PipelinePhase::Planning);
464 }
465
466 #[test]
467 fn test_reduce_pipeline_completed() {
468 let state = create_test_state();
469 let new_state = reduce(state, PipelineEvent::PipelineCompleted);
470 assert_eq!(new_state.phase, PipelinePhase::Complete);
471 }
472
473 #[test]
474 fn test_reduce_development_iteration_completed() {
475 let state = PipelineState {
478 phase: PipelinePhase::Development,
479 iteration: 2,
480 total_iterations: 5,
481 ..create_test_state()
482 };
483 let new_state = reduce(
484 state,
485 PipelineEvent::DevelopmentIterationCompleted {
486 iteration: 2,
487 output_valid: true,
488 },
489 );
490 assert_eq!(new_state.iteration, 2);
492 assert_eq!(new_state.phase, PipelinePhase::CommitMessage);
494 assert_eq!(new_state.previous_phase, Some(PipelinePhase::Development));
496 }
497
498 #[test]
499 fn test_reduce_development_iteration_complete_goes_to_commit() {
500 let state = PipelineState {
503 phase: PipelinePhase::Development,
504 iteration: 5,
505 total_iterations: 5,
506 ..create_test_state()
507 };
508 let new_state = reduce(
509 state,
510 PipelineEvent::DevelopmentIterationCompleted {
511 iteration: 5,
512 output_valid: true,
513 },
514 );
515 assert_eq!(new_state.iteration, 5);
517 assert_eq!(new_state.phase, PipelinePhase::CommitMessage);
519 }
520
521 #[test]
522 fn test_reduce_agent_fallback_to_next_model() {
523 let state = create_test_state();
524 let initial_agent = state.agent_chain.current_agent().unwrap().clone();
525 let initial_model_index = state.agent_chain.current_model_index;
526
527 let new_state = reduce(
528 state,
529 PipelineEvent::AgentInvocationFailed {
530 role: AgentRole::Developer,
531 agent: initial_agent.clone(),
532 exit_code: 1,
533 error_kind: AgentErrorKind::Network,
534 retriable: true,
535 },
536 );
537
538 assert_ne!(
539 new_state.agent_chain.current_model_index,
540 initial_model_index
541 );
542 }
543
544 #[test]
545 fn test_reduce_rebase_started() {
546 let state = create_test_state();
547 let new_state = reduce(
548 state,
549 PipelineEvent::RebaseStarted {
550 phase: RebasePhase::Initial,
551 target_branch: "main".to_string(),
552 },
553 );
554
555 assert!(matches!(new_state.rebase, RebaseState::InProgress { .. }));
556 }
557
558 #[test]
559 fn test_reduce_rebase_succeeded() {
560 let state = create_test_state();
561 let new_state = reduce(
562 state,
563 PipelineEvent::RebaseSucceeded {
564 phase: RebasePhase::Initial,
565 new_head: "abc123".to_string(),
566 },
567 );
568
569 assert!(matches!(new_state.rebase, RebaseState::Completed { .. }));
570 }
571
572 #[test]
573 fn test_reduce_commit_generation_started() {
574 let state = create_test_state();
575 let new_state = reduce(state, PipelineEvent::CommitGenerationStarted);
576
577 assert!(matches!(new_state.commit, CommitState::Generating { .. }));
578 }
579
580 #[test]
581 fn test_reduce_commit_created() {
582 let state = create_test_state();
583 let new_state = reduce(
584 state,
585 PipelineEvent::CommitCreated {
586 hash: "abc123".to_string(),
587 message: "test commit".to_string(),
588 },
589 );
590
591 assert!(matches!(new_state.commit, CommitState::Committed { .. }));
592 assert_eq!(new_state.phase, PipelinePhase::FinalValidation);
593 }
594
595 #[test]
596 fn test_reduce_all_agent_failure_scenarios() {
597 let state = create_test_state();
598 let initial_agent_index = state.agent_chain.current_agent_index;
599 let initial_model_index = state.agent_chain.current_model_index;
600 let agent_name = state.agent_chain.current_agent().unwrap().clone();
601
602 let network_error_state = reduce(
603 state.clone(),
604 PipelineEvent::AgentInvocationFailed {
605 role: AgentRole::Developer,
606 agent: agent_name.clone(),
607 exit_code: 1,
608 error_kind: AgentErrorKind::Network,
609 retriable: true,
610 },
611 );
612 assert_eq!(
613 network_error_state.agent_chain.current_agent_index,
614 initial_agent_index
615 );
616 assert!(network_error_state.agent_chain.current_model_index > initial_model_index);
617
618 let auth_error_state = reduce(
619 state.clone(),
620 PipelineEvent::AgentInvocationFailed {
621 role: AgentRole::Developer,
622 agent: agent_name.clone(),
623 exit_code: 1,
624 error_kind: AgentErrorKind::Authentication,
625 retriable: false,
626 },
627 );
628 assert!(auth_error_state.agent_chain.current_agent_index > initial_agent_index);
629 assert_eq!(
630 auth_error_state.agent_chain.current_model_index,
631 initial_model_index
632 );
633
634 let internal_error_state = reduce(
635 state,
636 PipelineEvent::AgentInvocationFailed {
637 role: AgentRole::Developer,
638 agent: agent_name,
639 exit_code: 139,
640 error_kind: AgentErrorKind::InternalError,
641 retriable: false,
642 },
643 );
644 assert!(internal_error_state.agent_chain.current_agent_index > initial_agent_index);
645 }
646
647 #[test]
648 fn test_reduce_rebase_full_state_machine() {
649 let mut state = create_test_state();
650
651 state = reduce(
652 state,
653 PipelineEvent::RebaseStarted {
654 phase: RebasePhase::Initial,
655 target_branch: "main".to_string(),
656 },
657 );
658 assert!(matches!(state.rebase, RebaseState::InProgress { .. }));
659
660 state = reduce(
661 state,
662 PipelineEvent::RebaseConflictDetected {
663 files: vec![std::path::PathBuf::from("file1.txt")],
664 },
665 );
666 assert!(matches!(state.rebase, RebaseState::Conflicted { .. }));
667
668 state = reduce(
669 state,
670 PipelineEvent::RebaseConflictResolved {
671 files: vec![std::path::PathBuf::from("file1.txt")],
672 },
673 );
674 assert!(matches!(state.rebase, RebaseState::InProgress { .. }));
675
676 state = reduce(
677 state,
678 PipelineEvent::RebaseSucceeded {
679 phase: RebasePhase::Initial,
680 new_head: "def456".to_string(),
681 },
682 );
683 assert!(matches!(state.rebase, RebaseState::Completed { .. }));
684 }
685
686 #[test]
687 fn test_reduce_commit_full_state_machine() {
688 let mut state = create_test_state();
689
690 state = reduce(state, PipelineEvent::CommitGenerationStarted);
691 assert!(matches!(state.commit, CommitState::Generating { .. }));
692
693 state = reduce(
694 state,
695 PipelineEvent::CommitCreated {
696 hash: "abc123".to_string(),
697 message: "test commit".to_string(),
698 },
699 );
700 assert!(matches!(state.commit, CommitState::Committed { .. }));
701 }
702
703 #[test]
704 fn test_reduce_phase_transitions() {
705 let mut state = create_test_state();
706
707 state = reduce(state, PipelineEvent::PlanningPhaseCompleted);
708 assert_eq!(state.phase, PipelinePhase::Development);
709
710 state = reduce(state, PipelineEvent::DevelopmentPhaseStarted);
711 assert_eq!(state.phase, PipelinePhase::Development);
712
713 state = reduce(state, PipelineEvent::DevelopmentPhaseCompleted);
714 assert_eq!(state.phase, PipelinePhase::Review);
715
716 state = reduce(state, PipelineEvent::ReviewPhaseStarted);
717 assert_eq!(state.phase, PipelinePhase::Review);
718
719 state = reduce(
720 state,
721 PipelineEvent::ReviewPhaseCompleted { early_exit: false },
722 );
723 assert_eq!(state.phase, PipelinePhase::CommitMessage);
724 }
725
726 #[test]
727 fn test_reduce_agent_chain_exhaustion() {
728 let state = PipelineState {
729 agent_chain: AgentChainState::initial()
730 .with_agents(
731 vec!["agent1".to_string()],
732 vec![vec!["model1".to_string()]],
733 AgentRole::Developer,
734 )
735 .with_max_cycles(3),
736 ..create_test_state()
737 };
738
739 let exhausted_state = reduce(
740 state,
741 PipelineEvent::AgentChainExhausted {
742 role: AgentRole::Developer,
743 },
744 );
745
746 assert_eq!(exhausted_state.agent_chain.current_agent_index, 0);
747 assert_eq!(exhausted_state.agent_chain.current_model_index, 0);
748 assert_eq!(exhausted_state.agent_chain.retry_cycle, 1);
749 }
750
751 #[test]
752 fn test_reduce_agent_fallback_triggers_fallback_event() {
753 let state = create_test_state();
754 let agent = state.agent_chain.current_agent().unwrap().clone();
755
756 let new_state = reduce(
757 state,
758 PipelineEvent::AgentInvocationFailed {
759 role: AgentRole::Developer,
760 agent: agent.clone(),
761 exit_code: 1,
762 error_kind: AgentErrorKind::Authentication,
763 retriable: false,
764 },
765 );
766
767 assert!(new_state.agent_chain.current_agent_index > 0);
768 }
769
770 #[test]
771 fn test_reduce_model_fallback_triggers_fallback_event() {
772 let state = create_test_state();
773 let initial_model_index = state.agent_chain.current_model_index;
774 let agent_name = state.agent_chain.current_agent().unwrap().clone();
775
776 let new_state = reduce(
777 state,
778 PipelineEvent::AgentInvocationFailed {
779 role: AgentRole::Developer,
780 agent: agent_name,
781 exit_code: 1,
782 error_kind: AgentErrorKind::RateLimit,
783 retriable: true,
784 },
785 );
786
787 assert!(new_state.agent_chain.current_model_index > initial_model_index);
788 }
789
790 #[test]
791 fn test_reduce_finalizing_started() {
792 let state = PipelineState {
793 phase: PipelinePhase::FinalValidation,
794 ..create_test_state()
795 };
796 let new_state = reduce(state, PipelineEvent::FinalizingStarted);
797 assert_eq!(new_state.phase, PipelinePhase::Finalizing);
798 }
799
800 #[test]
801 fn test_reduce_prompt_permissions_restored() {
802 let state = PipelineState {
803 phase: PipelinePhase::Finalizing,
804 ..create_test_state()
805 };
806 let new_state = reduce(state, PipelineEvent::PromptPermissionsRestored);
807 assert_eq!(new_state.phase, PipelinePhase::Complete);
808 }
809
810 #[test]
811 fn test_reduce_finalization_full_flow() {
812 let mut state = PipelineState {
813 phase: PipelinePhase::FinalValidation,
814 ..create_test_state()
815 };
816
817 state = reduce(state, PipelineEvent::FinalizingStarted);
819 assert_eq!(state.phase, PipelinePhase::Finalizing);
820
821 state = reduce(state, PipelineEvent::PromptPermissionsRestored);
823 assert_eq!(state.phase, PipelinePhase::Complete);
824 }
825
826 #[test]
836 fn test_finalization_orchestration_integration() {
837 use crate::reducer::mock_effect_handler::MockEffectHandler;
838 use crate::reducer::orchestration::determine_next_effect;
839
840 let initial_state = PipelineState {
842 phase: PipelinePhase::FinalValidation,
843 ..PipelineState::initial(5, 2)
844 };
845
846 let mut handler = MockEffectHandler::new(initial_state.clone());
847
848 let effect1 = determine_next_effect(&initial_state);
850 assert!(
851 matches!(effect1, crate::reducer::effect::Effect::ValidateFinalState),
852 "FinalValidation should emit ValidateFinalState effect"
853 );
854
855 let event1 = handler.execute_mock(effect1);
857 assert!(
858 matches!(event1, PipelineEvent::FinalizingStarted),
859 "ValidateFinalState should return FinalizingStarted"
860 );
861
862 let state2 = reduce(initial_state, event1);
864 assert_eq!(state2.phase, PipelinePhase::Finalizing);
865 assert!(!state2.is_complete(), "Finalizing should not be complete");
866
867 let effect2 = determine_next_effect(&state2);
869 assert!(
870 matches!(
871 effect2,
872 crate::reducer::effect::Effect::RestorePromptPermissions
873 ),
874 "Finalizing should emit RestorePromptPermissions effect"
875 );
876
877 let event2 = handler.execute_mock(effect2);
879 assert!(
880 matches!(event2, PipelineEvent::PromptPermissionsRestored),
881 "RestorePromptPermissions should return PromptPermissionsRestored"
882 );
883
884 let final_state = reduce(state2, event2);
886 assert_eq!(final_state.phase, PipelinePhase::Complete);
887 assert!(final_state.is_complete(), "Complete should be complete");
888
889 let effects = handler.captured_effects();
891 assert_eq!(effects.len(), 2);
892 assert!(matches!(
893 effects[0],
894 crate::reducer::effect::Effect::ValidateFinalState
895 ));
896 assert!(matches!(
897 effects[1],
898 crate::reducer::effect::Effect::RestorePromptPermissions
899 ));
900 }
901}