1use super::event::PipelineEvent;
21use super::state::{CommitState, ContinuationState, PipelineState, RebaseState};
22
23pub fn reduce(state: PipelineState, event: PipelineEvent) -> PipelineState {
30 match &event {
31 PipelineEvent::PipelineStarted
33 | PipelineEvent::PipelineResumed { .. }
34 | PipelineEvent::PipelineCompleted
35 | PipelineEvent::PipelineAborted { .. } => reduce_pipeline_lifecycle(state, event),
36
37 PipelineEvent::PlanningPhaseStarted
39 | PipelineEvent::PlanningPhaseCompleted
40 | PipelineEvent::PlanGenerationStarted { .. }
41 | PipelineEvent::PlanGenerationCompleted { .. } => reduce_planning_event(state, event),
42
43 PipelineEvent::DevelopmentPhaseStarted
45 | PipelineEvent::DevelopmentIterationStarted { .. }
46 | PipelineEvent::DevelopmentIterationCompleted { .. }
47 | PipelineEvent::DevelopmentPhaseCompleted
48 | PipelineEvent::DevelopmentIterationContinuationTriggered { .. }
49 | PipelineEvent::DevelopmentIterationContinuationSucceeded { .. } => {
50 reduce_development_event(state, event)
51 }
52
53 PipelineEvent::ReviewPhaseStarted
55 | PipelineEvent::ReviewPassStarted { .. }
56 | PipelineEvent::ReviewCompleted { .. }
57 | PipelineEvent::FixAttemptStarted { .. }
58 | PipelineEvent::FixAttemptCompleted { .. }
59 | PipelineEvent::ReviewPhaseCompleted { .. } => reduce_review_event(state, event),
60
61 PipelineEvent::AgentInvocationStarted { .. }
63 | PipelineEvent::AgentInvocationSucceeded { .. }
64 | PipelineEvent::AgentInvocationFailed { .. }
65 | PipelineEvent::AgentFallbackTriggered { .. }
66 | PipelineEvent::AgentChainExhausted { .. }
67 | PipelineEvent::AgentModelFallbackTriggered { .. }
68 | PipelineEvent::AgentRetryCycleStarted { .. }
69 | PipelineEvent::AgentChainInitialized { .. }
70 | PipelineEvent::AgentRateLimitFallback { .. } => reduce_agent_event(state, event),
71
72 PipelineEvent::RebaseStarted { .. }
74 | PipelineEvent::RebaseConflictDetected { .. }
75 | PipelineEvent::RebaseConflictResolved { .. }
76 | PipelineEvent::RebaseSucceeded { .. }
77 | PipelineEvent::RebaseFailed { .. }
78 | PipelineEvent::RebaseSkipped { .. }
79 | PipelineEvent::RebaseAborted { .. } => reduce_rebase_event(state, event),
80
81 PipelineEvent::CommitGenerationStarted
83 | PipelineEvent::CommitMessageGenerated { .. }
84 | PipelineEvent::CommitCreated { .. }
85 | PipelineEvent::CommitGenerationFailed { .. }
86 | PipelineEvent::CommitSkipped { .. }
87 | PipelineEvent::CommitMessageValidationFailed { .. } => reduce_commit_event(state, event),
88
89 PipelineEvent::ContextCleaned => PipelineState {
91 context_cleaned: true,
92 ..state
93 },
94 PipelineEvent::CheckpointSaved { .. } => state,
95 PipelineEvent::FinalizingStarted => PipelineState {
96 phase: super::event::PipelinePhase::Finalizing,
97 ..state
98 },
99 PipelineEvent::PromptPermissionsRestored => PipelineState {
100 phase: super::event::PipelinePhase::Complete,
101 ..state
102 },
103 }
104}
105
106fn reduce_pipeline_lifecycle(state: PipelineState, event: PipelineEvent) -> PipelineState {
112 match event {
113 PipelineEvent::PipelineStarted => state,
114 PipelineEvent::PipelineResumed { .. } => state,
115 PipelineEvent::PipelineCompleted => PipelineState {
116 phase: super::event::PipelinePhase::Complete,
117 ..state
118 },
119 PipelineEvent::PipelineAborted { .. } => PipelineState {
120 phase: super::event::PipelinePhase::Interrupted,
121 ..state
122 },
123 _ => state,
124 }
125}
126
127fn reduce_planning_event(state: PipelineState, event: PipelineEvent) -> PipelineState {
129 match event {
130 PipelineEvent::PlanningPhaseStarted => PipelineState {
131 phase: super::event::PipelinePhase::Planning,
132 ..state
133 },
134 PipelineEvent::PlanningPhaseCompleted => PipelineState {
135 phase: super::event::PipelinePhase::Development,
136 ..state
137 },
138 PipelineEvent::PlanGenerationStarted { .. } => state,
139 PipelineEvent::PlanGenerationCompleted { valid, .. } => {
140 if valid {
141 PipelineState {
142 phase: super::event::PipelinePhase::Development,
143 ..state
144 }
145 } else {
146 PipelineState {
148 phase: super::event::PipelinePhase::Planning,
149 ..state
150 }
151 }
152 }
153 _ => state,
154 }
155}
156
157fn reduce_development_event(state: PipelineState, event: PipelineEvent) -> PipelineState {
159 match event {
160 PipelineEvent::DevelopmentPhaseStarted => PipelineState {
161 phase: super::event::PipelinePhase::Development,
162 ..state
163 },
164 PipelineEvent::DevelopmentIterationStarted { iteration } => PipelineState {
165 iteration,
166 agent_chain: state.agent_chain.reset(),
167 continuation: state.continuation.reset(),
169 ..state
170 },
171 PipelineEvent::DevelopmentIterationCompleted {
172 iteration,
173 output_valid,
174 } => {
175 if output_valid {
176 PipelineState {
178 phase: super::event::PipelinePhase::CommitMessage,
179 previous_phase: Some(super::event::PipelinePhase::Development),
180 iteration,
181 commit: super::state::CommitState::NotStarted,
182 context_cleaned: false,
183 continuation: ContinuationState::new(),
185 ..state
186 }
187 } else {
188 PipelineState {
190 phase: super::event::PipelinePhase::Development,
191 iteration,
192 ..state
193 }
194 }
195 }
196 PipelineEvent::DevelopmentPhaseCompleted => PipelineState {
197 phase: super::event::PipelinePhase::Review,
198 continuation: ContinuationState::new(),
200 ..state
201 },
202 PipelineEvent::DevelopmentIterationContinuationTriggered {
203 iteration,
204 status,
205 summary,
206 files_changed,
207 next_steps,
208 } => {
209 PipelineState {
211 iteration,
212 continuation: state.continuation.trigger_continuation(
213 status,
214 summary,
215 files_changed,
216 next_steps,
217 ),
218 ..state
219 }
220 }
221 PipelineEvent::DevelopmentIterationContinuationSucceeded {
222 iteration,
223 total_continuation_attempts: _,
224 } => {
225 PipelineState {
227 phase: super::event::PipelinePhase::CommitMessage,
228 previous_phase: Some(super::event::PipelinePhase::Development),
229 iteration,
230 commit: super::state::CommitState::NotStarted,
231 context_cleaned: false,
232 continuation: ContinuationState::new(),
233 ..state
234 }
235 }
236 _ => state,
237 }
238}
239
240fn reduce_review_event(state: PipelineState, event: PipelineEvent) -> PipelineState {
242 match event {
243 PipelineEvent::ReviewPhaseStarted => PipelineState {
244 phase: super::event::PipelinePhase::Review,
245 reviewer_pass: 0,
246 review_issues_found: false,
247 ..state
248 },
249 PipelineEvent::ReviewPassStarted { pass } => PipelineState {
250 reviewer_pass: pass,
251 review_issues_found: false,
252 agent_chain: state.agent_chain.reset(),
253 ..state
254 },
255 PipelineEvent::ReviewCompleted { pass, issues_found } => {
256 let next_pass = if issues_found { pass } else { pass + 1 };
257 let next_phase = if !issues_found && next_pass >= state.total_reviewer_passes {
258 super::event::PipelinePhase::CommitMessage
259 } else {
260 state.phase
261 };
262 PipelineState {
263 phase: next_phase,
264 reviewer_pass: next_pass,
265 review_issues_found: issues_found,
266 ..state
267 }
268 }
269 PipelineEvent::FixAttemptStarted { .. } => PipelineState {
270 agent_chain: state.agent_chain.reset(),
271 ..state
272 },
273 PipelineEvent::FixAttemptCompleted { pass, .. } => PipelineState {
274 phase: super::event::PipelinePhase::CommitMessage,
275 previous_phase: Some(super::event::PipelinePhase::Review),
276 reviewer_pass: pass,
277 review_issues_found: false,
278 commit: super::state::CommitState::NotStarted,
279 ..state
280 },
281 PipelineEvent::ReviewPhaseCompleted { .. } => PipelineState {
282 phase: super::event::PipelinePhase::CommitMessage,
283 ..state
284 },
285 _ => state,
286 }
287}
288
289fn reduce_agent_event(state: PipelineState, event: PipelineEvent) -> PipelineState {
291 match event {
292 PipelineEvent::AgentInvocationStarted { .. } => state,
293 PipelineEvent::AgentInvocationSucceeded { .. } => PipelineState {
295 agent_chain: state.agent_chain.clear_continuation_prompt(),
296 ..state
297 },
298 PipelineEvent::AgentRateLimitFallback { prompt_context, .. } => PipelineState {
303 agent_chain: state
304 .agent_chain
305 .switch_to_next_agent_with_prompt(prompt_context),
306 ..state
307 },
308 PipelineEvent::AgentInvocationFailed {
310 retriable: true, ..
311 } => PipelineState {
312 agent_chain: state.agent_chain.advance_to_next_model(),
313 ..state
314 },
315 PipelineEvent::AgentInvocationFailed {
317 retriable: false, ..
318 } => PipelineState {
319 agent_chain: state.agent_chain.switch_to_next_agent(),
320 ..state
321 },
322 PipelineEvent::AgentFallbackTriggered { .. } => PipelineState {
323 agent_chain: state.agent_chain.switch_to_next_agent(),
324 ..state
325 },
326 PipelineEvent::AgentChainExhausted { .. } => PipelineState {
327 agent_chain: state.agent_chain.start_retry_cycle(),
328 ..state
329 },
330 PipelineEvent::AgentModelFallbackTriggered { .. } => PipelineState {
331 agent_chain: state.agent_chain.advance_to_next_model(),
332 ..state
333 },
334 PipelineEvent::AgentRetryCycleStarted { .. } => state,
335 PipelineEvent::AgentChainInitialized { role, agents } => {
336 let models_per_agent = agents.iter().map(|_| vec![]).collect();
337 PipelineState {
338 agent_chain: state
339 .agent_chain
340 .with_agents(agents, models_per_agent, role)
341 .reset_for_role(role),
342 ..state
343 }
344 }
345 _ => state,
346 }
347}
348
349fn reduce_rebase_event(state: PipelineState, event: PipelineEvent) -> PipelineState {
351 match event {
352 PipelineEvent::RebaseStarted {
353 target_branch,
354 phase: _,
355 } => PipelineState {
356 rebase: RebaseState::InProgress {
357 original_head: state.current_head(),
358 target_branch,
359 },
360 ..state
361 },
362 PipelineEvent::RebaseConflictDetected { files } => PipelineState {
363 rebase: match &state.rebase {
364 RebaseState::InProgress {
365 original_head,
366 target_branch,
367 } => RebaseState::Conflicted {
368 original_head: original_head.clone(),
369 target_branch: target_branch.clone(),
370 files,
371 resolution_attempts: 0,
372 },
373 _ => state.rebase.clone(),
374 },
375 ..state
376 },
377 PipelineEvent::RebaseConflictResolved { .. } => PipelineState {
378 rebase: match &state.rebase {
379 RebaseState::Conflicted {
380 original_head,
381 target_branch,
382 ..
383 } => RebaseState::InProgress {
384 original_head: original_head.clone(),
385 target_branch: target_branch.clone(),
386 },
387 _ => state.rebase.clone(),
388 },
389 ..state
390 },
391 PipelineEvent::RebaseSucceeded { new_head, .. } => PipelineState {
392 rebase: RebaseState::Completed { new_head },
393 ..state
394 },
395 PipelineEvent::RebaseFailed { .. } => PipelineState {
396 rebase: RebaseState::NotStarted,
397 ..state
398 },
399 PipelineEvent::RebaseSkipped { .. } => PipelineState {
400 rebase: RebaseState::Skipped,
401 ..state
402 },
403 PipelineEvent::RebaseAborted { .. } => state,
404 _ => state,
405 }
406}
407
408fn reduce_commit_event(state: PipelineState, event: PipelineEvent) -> PipelineState {
410 match event {
411 PipelineEvent::CommitGenerationStarted => PipelineState {
412 commit: CommitState::Generating {
413 attempt: 1,
414 max_attempts: super::state::MAX_VALIDATION_RETRY_ATTEMPTS,
415 },
416 ..state
417 },
418 PipelineEvent::CommitMessageGenerated { message, .. } => PipelineState {
419 commit: CommitState::Generated { message },
420 ..state
421 },
422 PipelineEvent::CommitCreated { hash, .. } => {
423 let (next_phase, next_iter, next_reviewer_pass) =
424 compute_post_commit_transition(&state);
425 PipelineState {
426 commit: CommitState::Committed { hash },
427 phase: next_phase,
428 previous_phase: None,
429 iteration: next_iter,
430 reviewer_pass: next_reviewer_pass,
431 context_cleaned: false,
432 ..state
433 }
434 }
435 PipelineEvent::CommitGenerationFailed { .. } => PipelineState {
436 commit: CommitState::NotStarted,
437 ..state
438 },
439 PipelineEvent::CommitSkipped { .. } => {
440 let (next_phase, next_iter, next_reviewer_pass) =
441 compute_post_commit_transition(&state);
442 PipelineState {
443 commit: CommitState::Skipped,
444 phase: next_phase,
445 previous_phase: None,
446 iteration: next_iter,
447 reviewer_pass: next_reviewer_pass,
448 context_cleaned: false,
449 ..state
450 }
451 }
452 PipelineEvent::CommitMessageValidationFailed { attempt, .. } => {
453 reduce_commit_validation_failed(state, attempt)
454 }
455 _ => state,
456 }
457}
458
459fn compute_post_commit_transition(
461 state: &PipelineState,
462) -> (super::event::PipelinePhase, u32, u32) {
463 match state.previous_phase {
464 Some(super::event::PipelinePhase::Development) => {
465 let next_iter = state.iteration + 1;
466 if next_iter >= state.total_iterations {
467 (
468 super::event::PipelinePhase::Review,
469 next_iter,
470 state.reviewer_pass,
471 )
472 } else {
473 (
474 super::event::PipelinePhase::Planning,
475 next_iter,
476 state.reviewer_pass,
477 )
478 }
479 }
480 Some(super::event::PipelinePhase::Review) => {
481 let next_pass = state.reviewer_pass + 1;
482 if next_pass >= state.total_reviewer_passes {
483 (
484 super::event::PipelinePhase::FinalValidation,
485 state.iteration,
486 next_pass,
487 )
488 } else {
489 (
490 super::event::PipelinePhase::Review,
491 state.iteration,
492 next_pass,
493 )
494 }
495 }
496 _ => (
497 super::event::PipelinePhase::FinalValidation,
498 state.iteration,
499 state.reviewer_pass,
500 ),
501 }
502}
503
504fn reduce_commit_validation_failed(state: PipelineState, attempt: u32) -> PipelineState {
506 let next_attempt = attempt + 1;
507 let max_attempts = super::state::MAX_VALIDATION_RETRY_ATTEMPTS;
508
509 if next_attempt <= max_attempts {
510 PipelineState {
511 commit: CommitState::Generating {
512 attempt: next_attempt,
513 max_attempts,
514 },
515 ..state
516 }
517 } else {
518 let old_agent_index = state.agent_chain.current_agent_index;
520 let old_retry_cycle = state.agent_chain.retry_cycle;
521 let new_agent_chain = state.agent_chain.switch_to_next_agent();
522
523 let wrapped_around = new_agent_chain.retry_cycle > old_retry_cycle;
524 let advanced_to_next =
525 new_agent_chain.current_agent_index != old_agent_index && !wrapped_around;
526
527 if advanced_to_next {
528 PipelineState {
529 agent_chain: new_agent_chain,
530 commit: CommitState::Generating {
531 attempt: 1,
532 max_attempts,
533 },
534 ..state
535 }
536 } else {
537 PipelineState {
539 agent_chain: new_agent_chain,
540 commit: CommitState::NotStarted,
541 ..state
542 }
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use crate::agents::AgentRole;
551 use crate::reducer::event::AgentErrorKind;
552 use crate::reducer::event::PipelinePhase;
553 use crate::reducer::event::RebasePhase;
554 use crate::reducer::state::AgentChainState;
555
556 fn create_test_state() -> PipelineState {
557 PipelineState {
558 agent_chain: AgentChainState::initial().with_agents(
559 vec!["agent1".to_string(), "agent2".to_string()],
560 vec![vec!["model1".to_string(), "model2".to_string()]],
561 AgentRole::Developer,
562 ),
563 ..PipelineState::initial(5, 2)
564 }
565 }
566
567 #[test]
568 fn test_reduce_pipeline_started() {
569 let state = create_test_state();
570 let new_state = reduce(state, PipelineEvent::PipelineStarted);
571 assert_eq!(new_state.phase, PipelinePhase::Planning);
572 }
573
574 #[test]
575 fn test_reduce_pipeline_completed() {
576 let state = create_test_state();
577 let new_state = reduce(state, PipelineEvent::PipelineCompleted);
578 assert_eq!(new_state.phase, PipelinePhase::Complete);
579 }
580
581 #[test]
582 fn test_reduce_development_iteration_completed() {
583 let state = PipelineState {
586 phase: PipelinePhase::Development,
587 iteration: 2,
588 total_iterations: 5,
589 ..create_test_state()
590 };
591 let new_state = reduce(
592 state,
593 PipelineEvent::DevelopmentIterationCompleted {
594 iteration: 2,
595 output_valid: true,
596 },
597 );
598 assert_eq!(new_state.iteration, 2);
600 assert_eq!(new_state.phase, PipelinePhase::CommitMessage);
602 assert_eq!(new_state.previous_phase, Some(PipelinePhase::Development));
604 }
605
606 #[test]
607 fn test_reduce_development_iteration_complete_goes_to_commit() {
608 let state = PipelineState {
611 phase: PipelinePhase::Development,
612 iteration: 5,
613 total_iterations: 5,
614 ..create_test_state()
615 };
616 let new_state = reduce(
617 state,
618 PipelineEvent::DevelopmentIterationCompleted {
619 iteration: 5,
620 output_valid: true,
621 },
622 );
623 assert_eq!(new_state.iteration, 5);
625 assert_eq!(new_state.phase, PipelinePhase::CommitMessage);
627 }
628
629 #[test]
630 fn test_plan_generation_completed_invalid_does_not_transition_to_development() {
631 let state = PipelineState {
632 phase: PipelinePhase::Planning,
633 ..create_test_state()
634 };
635
636 let new_state = reduce(
637 state,
638 PipelineEvent::PlanGenerationCompleted {
639 iteration: 1,
640 valid: false,
641 },
642 );
643
644 assert_eq!(
645 new_state.phase,
646 PipelinePhase::Planning,
647 "Invalid plan should keep pipeline in Planning phase"
648 );
649 }
650
651 #[test]
652 fn test_reduce_agent_fallback_to_next_model() {
653 let state = create_test_state();
654 let initial_agent = state.agent_chain.current_agent().unwrap().clone();
655 let initial_model_index = state.agent_chain.current_model_index;
656
657 let new_state = reduce(
658 state,
659 PipelineEvent::AgentInvocationFailed {
660 role: AgentRole::Developer,
661 agent: initial_agent.clone(),
662 exit_code: 1,
663 error_kind: AgentErrorKind::Network,
664 retriable: true,
665 },
666 );
667
668 assert_ne!(
669 new_state.agent_chain.current_model_index,
670 initial_model_index
671 );
672 }
673
674 #[test]
675 fn test_reduce_rebase_started() {
676 let state = create_test_state();
677 let new_state = reduce(
678 state,
679 PipelineEvent::RebaseStarted {
680 phase: RebasePhase::Initial,
681 target_branch: "main".to_string(),
682 },
683 );
684
685 assert!(matches!(new_state.rebase, RebaseState::InProgress { .. }));
686 }
687
688 #[test]
689 fn test_reduce_rebase_succeeded() {
690 let state = create_test_state();
691 let new_state = reduce(
692 state,
693 PipelineEvent::RebaseSucceeded {
694 phase: RebasePhase::Initial,
695 new_head: "abc123".to_string(),
696 },
697 );
698
699 assert!(matches!(new_state.rebase, RebaseState::Completed { .. }));
700 }
701
702 #[test]
703 fn test_reduce_commit_generation_started() {
704 let state = create_test_state();
705 let new_state = reduce(state, PipelineEvent::CommitGenerationStarted);
706
707 assert!(matches!(new_state.commit, CommitState::Generating { .. }));
708 }
709
710 #[test]
711 fn test_reduce_commit_created() {
712 let state = create_test_state();
713 let new_state = reduce(
714 state,
715 PipelineEvent::CommitCreated {
716 hash: "abc123".to_string(),
717 message: "test commit".to_string(),
718 },
719 );
720
721 assert!(matches!(new_state.commit, CommitState::Committed { .. }));
722 assert_eq!(new_state.phase, PipelinePhase::FinalValidation);
723 }
724
725 #[test]
726 fn test_reduce_all_agent_failure_scenarios() {
727 let state = create_test_state();
728 let initial_agent_index = state.agent_chain.current_agent_index;
729 let initial_model_index = state.agent_chain.current_model_index;
730 let agent_name = state.agent_chain.current_agent().unwrap().clone();
731
732 let network_error_state = reduce(
733 state.clone(),
734 PipelineEvent::AgentInvocationFailed {
735 role: AgentRole::Developer,
736 agent: agent_name.clone(),
737 exit_code: 1,
738 error_kind: AgentErrorKind::Network,
739 retriable: true,
740 },
741 );
742 assert_eq!(
743 network_error_state.agent_chain.current_agent_index,
744 initial_agent_index
745 );
746 assert!(network_error_state.agent_chain.current_model_index > initial_model_index);
747
748 let auth_error_state = reduce(
749 state.clone(),
750 PipelineEvent::AgentInvocationFailed {
751 role: AgentRole::Developer,
752 agent: agent_name.clone(),
753 exit_code: 1,
754 error_kind: AgentErrorKind::Authentication,
755 retriable: false,
756 },
757 );
758 assert!(auth_error_state.agent_chain.current_agent_index > initial_agent_index);
759 assert_eq!(
760 auth_error_state.agent_chain.current_model_index,
761 initial_model_index
762 );
763
764 let internal_error_state = reduce(
765 state,
766 PipelineEvent::AgentInvocationFailed {
767 role: AgentRole::Developer,
768 agent: agent_name,
769 exit_code: 139,
770 error_kind: AgentErrorKind::InternalError,
771 retriable: false,
772 },
773 );
774 assert!(internal_error_state.agent_chain.current_agent_index > initial_agent_index);
775 }
776
777 #[test]
778 fn test_reduce_rebase_full_state_machine() {
779 let mut state = create_test_state();
780
781 state = reduce(
782 state,
783 PipelineEvent::RebaseStarted {
784 phase: RebasePhase::Initial,
785 target_branch: "main".to_string(),
786 },
787 );
788 assert!(matches!(state.rebase, RebaseState::InProgress { .. }));
789
790 state = reduce(
791 state,
792 PipelineEvent::RebaseConflictDetected {
793 files: vec![std::path::PathBuf::from("file1.txt")],
794 },
795 );
796 assert!(matches!(state.rebase, RebaseState::Conflicted { .. }));
797
798 state = reduce(
799 state,
800 PipelineEvent::RebaseConflictResolved {
801 files: vec![std::path::PathBuf::from("file1.txt")],
802 },
803 );
804 assert!(matches!(state.rebase, RebaseState::InProgress { .. }));
805
806 state = reduce(
807 state,
808 PipelineEvent::RebaseSucceeded {
809 phase: RebasePhase::Initial,
810 new_head: "def456".to_string(),
811 },
812 );
813 assert!(matches!(state.rebase, RebaseState::Completed { .. }));
814 }
815
816 #[test]
817 fn test_reduce_commit_full_state_machine() {
818 let mut state = create_test_state();
819
820 state = reduce(state, PipelineEvent::CommitGenerationStarted);
821 assert!(matches!(state.commit, CommitState::Generating { .. }));
822
823 state = reduce(
824 state,
825 PipelineEvent::CommitCreated {
826 hash: "abc123".to_string(),
827 message: "test commit".to_string(),
828 },
829 );
830 assert!(matches!(state.commit, CommitState::Committed { .. }));
831 }
832
833 #[test]
834 fn test_reduce_phase_transitions() {
835 let mut state = create_test_state();
836
837 state = reduce(state, PipelineEvent::PlanningPhaseCompleted);
838 assert_eq!(state.phase, PipelinePhase::Development);
839
840 state = reduce(state, PipelineEvent::DevelopmentPhaseStarted);
841 assert_eq!(state.phase, PipelinePhase::Development);
842
843 state = reduce(state, PipelineEvent::DevelopmentPhaseCompleted);
844 assert_eq!(state.phase, PipelinePhase::Review);
845
846 state = reduce(state, PipelineEvent::ReviewPhaseStarted);
847 assert_eq!(state.phase, PipelinePhase::Review);
848
849 state = reduce(
850 state,
851 PipelineEvent::ReviewPhaseCompleted { early_exit: false },
852 );
853 assert_eq!(state.phase, PipelinePhase::CommitMessage);
854 }
855
856 #[test]
857 fn test_reduce_agent_chain_exhaustion() {
858 let state = PipelineState {
859 agent_chain: AgentChainState::initial()
860 .with_agents(
861 vec!["agent1".to_string()],
862 vec![vec!["model1".to_string()]],
863 AgentRole::Developer,
864 )
865 .with_max_cycles(3),
866 ..create_test_state()
867 };
868
869 let exhausted_state = reduce(
870 state,
871 PipelineEvent::AgentChainExhausted {
872 role: AgentRole::Developer,
873 },
874 );
875
876 assert_eq!(exhausted_state.agent_chain.current_agent_index, 0);
877 assert_eq!(exhausted_state.agent_chain.current_model_index, 0);
878 assert_eq!(exhausted_state.agent_chain.retry_cycle, 1);
879 }
880
881 #[test]
882 fn test_reduce_agent_fallback_triggers_fallback_event() {
883 let state = create_test_state();
884 let agent = state.agent_chain.current_agent().unwrap().clone();
885
886 let new_state = reduce(
887 state,
888 PipelineEvent::AgentInvocationFailed {
889 role: AgentRole::Developer,
890 agent: agent.clone(),
891 exit_code: 1,
892 error_kind: AgentErrorKind::Authentication,
893 retriable: false,
894 },
895 );
896
897 assert!(new_state.agent_chain.current_agent_index > 0);
898 }
899
900 #[test]
901 fn test_reduce_model_fallback_triggers_for_network_error() {
902 let state = create_test_state();
903 let initial_model_index = state.agent_chain.current_model_index;
904 let agent_name = state.agent_chain.current_agent().unwrap().clone();
905
906 let new_state = reduce(
907 state,
908 PipelineEvent::AgentInvocationFailed {
909 role: AgentRole::Developer,
910 agent: agent_name,
911 exit_code: 1,
912 error_kind: AgentErrorKind::Network,
913 retriable: true,
914 },
915 );
916
917 assert!(new_state.agent_chain.current_model_index > initial_model_index);
918 }
919
920 #[test]
921 fn test_rate_limit_fallback_switches_agent() {
922 let state = create_test_state();
923 let initial_agent_index = state.agent_chain.current_agent_index;
924
925 let new_state = reduce(
926 state,
927 PipelineEvent::AgentRateLimitFallback {
928 role: AgentRole::Developer,
929 agent: "agent1".to_string(),
930 prompt_context: Some("test prompt".to_string()),
931 },
932 );
933
934 assert!(
936 new_state.agent_chain.current_agent_index > initial_agent_index,
937 "Rate limit should trigger agent fallback, not model fallback"
938 );
939 assert_eq!(
941 new_state.agent_chain.rate_limit_continuation_prompt,
942 Some("test prompt".to_string())
943 );
944 }
945
946 #[test]
947 fn test_rate_limit_fallback_with_no_prompt_context() {
948 let state = create_test_state();
949 let initial_agent_index = state.agent_chain.current_agent_index;
950
951 let new_state = reduce(
952 state,
953 PipelineEvent::AgentRateLimitFallback {
954 role: AgentRole::Developer,
955 agent: "agent1".to_string(),
956 prompt_context: None,
957 },
958 );
959
960 assert!(new_state.agent_chain.current_agent_index > initial_agent_index);
962 assert!(new_state
964 .agent_chain
965 .rate_limit_continuation_prompt
966 .is_none());
967 }
968
969 #[test]
970 fn test_success_clears_rate_limit_continuation_prompt() {
971 let mut state = create_test_state();
972 state.agent_chain.rate_limit_continuation_prompt = Some("old prompt".to_string());
973
974 let new_state = reduce(
975 state,
976 PipelineEvent::AgentInvocationSucceeded {
977 role: AgentRole::Developer,
978 agent: "agent1".to_string(),
979 },
980 );
981
982 assert!(
983 new_state
984 .agent_chain
985 .rate_limit_continuation_prompt
986 .is_none(),
987 "Success should clear rate limit continuation prompt"
988 );
989 }
990
991 #[test]
992 fn test_reduce_finalizing_started() {
993 let state = PipelineState {
994 phase: PipelinePhase::FinalValidation,
995 ..create_test_state()
996 };
997 let new_state = reduce(state, PipelineEvent::FinalizingStarted);
998 assert_eq!(new_state.phase, PipelinePhase::Finalizing);
999 }
1000
1001 #[test]
1002 fn test_reduce_prompt_permissions_restored() {
1003 let state = PipelineState {
1004 phase: PipelinePhase::Finalizing,
1005 ..create_test_state()
1006 };
1007 let new_state = reduce(state, PipelineEvent::PromptPermissionsRestored);
1008 assert_eq!(new_state.phase, PipelinePhase::Complete);
1009 }
1010
1011 #[test]
1012 fn test_reduce_finalization_full_flow() {
1013 let mut state = PipelineState {
1014 phase: PipelinePhase::FinalValidation,
1015 ..create_test_state()
1016 };
1017
1018 state = reduce(state, PipelineEvent::FinalizingStarted);
1020 assert_eq!(state.phase, PipelinePhase::Finalizing);
1021
1022 state = reduce(state, PipelineEvent::PromptPermissionsRestored);
1024 assert_eq!(state.phase, PipelinePhase::Complete);
1025 }
1026
1027 #[test]
1037 fn test_finalization_orchestration_integration() {
1038 use crate::reducer::mock_effect_handler::MockEffectHandler;
1039 use crate::reducer::orchestration::determine_next_effect;
1040
1041 let initial_state = PipelineState {
1043 phase: PipelinePhase::FinalValidation,
1044 ..PipelineState::initial(5, 2)
1045 };
1046
1047 let mut handler = MockEffectHandler::new(initial_state.clone());
1048
1049 let effect1 = determine_next_effect(&initial_state);
1051 assert!(
1052 matches!(effect1, crate::reducer::effect::Effect::ValidateFinalState),
1053 "FinalValidation should emit ValidateFinalState effect"
1054 );
1055
1056 let result1 = handler.execute_mock(effect1);
1058 assert!(
1059 matches!(result1.event, PipelineEvent::FinalizingStarted),
1060 "ValidateFinalState should return FinalizingStarted"
1061 );
1062
1063 let state2 = reduce(initial_state, result1.event);
1065 assert_eq!(state2.phase, PipelinePhase::Finalizing);
1066 assert!(!state2.is_complete(), "Finalizing should not be complete");
1067
1068 let effect2 = determine_next_effect(&state2);
1070 assert!(
1071 matches!(
1072 effect2,
1073 crate::reducer::effect::Effect::RestorePromptPermissions
1074 ),
1075 "Finalizing should emit RestorePromptPermissions effect"
1076 );
1077
1078 let result2 = handler.execute_mock(effect2);
1080 assert!(
1081 matches!(result2.event, PipelineEvent::PromptPermissionsRestored),
1082 "RestorePromptPermissions should return PromptPermissionsRestored"
1083 );
1084
1085 let final_state = reduce(state2, result2.event);
1087 assert_eq!(final_state.phase, PipelinePhase::Complete);
1088 assert!(final_state.is_complete(), "Complete should be complete");
1089
1090 let effects = handler.captured_effects();
1092 assert_eq!(effects.len(), 2);
1093 assert!(matches!(
1094 effects[0],
1095 crate::reducer::effect::Effect::ValidateFinalState
1096 ));
1097 assert!(matches!(
1098 effects[1],
1099 crate::reducer::effect::Effect::RestorePromptPermissions
1100 ));
1101 }
1102
1103 #[test]
1108 fn test_continuation_triggered_updates_state() {
1109 use crate::reducer::state::DevelopmentStatus;
1110
1111 let state = create_test_state();
1112 let new_state = reduce(
1113 state,
1114 PipelineEvent::DevelopmentIterationContinuationTriggered {
1115 iteration: 1,
1116 status: DevelopmentStatus::Partial,
1117 summary: "Did work".to_string(),
1118 files_changed: Some(vec!["src/main.rs".to_string()]),
1119 next_steps: Some("Continue".to_string()),
1120 },
1121 );
1122
1123 assert!(new_state.continuation.is_continuation());
1124 assert_eq!(
1125 new_state.continuation.previous_status,
1126 Some(DevelopmentStatus::Partial)
1127 );
1128 assert_eq!(
1129 new_state.continuation.previous_summary,
1130 Some("Did work".to_string())
1131 );
1132 assert_eq!(
1133 new_state.continuation.previous_files_changed,
1134 Some(vec!["src/main.rs".to_string()])
1135 );
1136 assert_eq!(
1137 new_state.continuation.previous_next_steps,
1138 Some("Continue".to_string())
1139 );
1140 assert_eq!(new_state.continuation.continuation_attempt, 1);
1141 }
1142
1143 #[test]
1144 fn test_continuation_triggered_sets_iteration_from_event() {
1145 use crate::reducer::state::DevelopmentStatus;
1146
1147 let state = PipelineState {
1148 iteration: 99,
1149 ..create_test_state()
1150 };
1151
1152 let new_state = reduce(
1153 state,
1154 PipelineEvent::DevelopmentIterationContinuationTriggered {
1155 iteration: 2,
1156 status: DevelopmentStatus::Partial,
1157 summary: "Did work".to_string(),
1158 files_changed: None,
1159 next_steps: None,
1160 },
1161 );
1162
1163 assert_eq!(new_state.iteration, 2);
1164 }
1165
1166 #[test]
1167 fn test_continuation_triggered_with_failed_status() {
1168 use crate::reducer::state::DevelopmentStatus;
1169
1170 let state = create_test_state();
1171 let new_state = reduce(
1172 state,
1173 PipelineEvent::DevelopmentIterationContinuationTriggered {
1174 iteration: 1,
1175 status: DevelopmentStatus::Failed,
1176 summary: "Build failed".to_string(),
1177 files_changed: None,
1178 next_steps: Some("Fix errors".to_string()),
1179 },
1180 );
1181
1182 assert!(new_state.continuation.is_continuation());
1183 assert_eq!(
1184 new_state.continuation.previous_status,
1185 Some(DevelopmentStatus::Failed)
1186 );
1187 assert_eq!(
1188 new_state.continuation.previous_summary,
1189 Some("Build failed".to_string())
1190 );
1191 assert!(new_state.continuation.previous_files_changed.is_none());
1192 }
1193
1194 #[test]
1195 fn test_continuation_succeeded_resets_state() {
1196 use crate::reducer::state::{ContinuationState, DevelopmentStatus};
1197
1198 let mut state = create_test_state();
1199 state.continuation = ContinuationState::new().trigger_continuation(
1200 DevelopmentStatus::Partial,
1201 "Work".to_string(),
1202 None,
1203 None,
1204 );
1205 assert!(state.continuation.is_continuation());
1206
1207 let new_state = reduce(
1208 state,
1209 PipelineEvent::DevelopmentIterationContinuationSucceeded {
1210 iteration: 1,
1211 total_continuation_attempts: 2,
1212 },
1213 );
1214
1215 assert!(!new_state.continuation.is_continuation());
1216 assert_eq!(new_state.continuation.continuation_attempt, 0);
1217 assert!(new_state.continuation.previous_status.is_none());
1218 }
1219
1220 #[test]
1221 fn test_continuation_succeeded_sets_iteration_from_event() {
1222 use crate::reducer::state::{ContinuationState, DevelopmentStatus};
1223
1224 let mut state = PipelineState {
1225 phase: PipelinePhase::Development,
1226 iteration: 99,
1227 ..create_test_state()
1228 };
1229 state.continuation = ContinuationState::new().trigger_continuation(
1230 DevelopmentStatus::Partial,
1231 "Work".to_string(),
1232 None,
1233 None,
1234 );
1235
1236 let new_state = reduce(
1237 state,
1238 PipelineEvent::DevelopmentIterationContinuationSucceeded {
1239 iteration: 1,
1240 total_continuation_attempts: 1,
1241 },
1242 );
1243
1244 assert_eq!(new_state.iteration, 1);
1245 }
1246
1247 #[test]
1248 fn test_iteration_started_resets_continuation() {
1249 use crate::reducer::state::{ContinuationState, DevelopmentStatus};
1250
1251 let mut state = create_test_state();
1252 state.continuation = ContinuationState::new().trigger_continuation(
1253 DevelopmentStatus::Partial,
1254 "Work".to_string(),
1255 None,
1256 None,
1257 );
1258 assert!(state.continuation.is_continuation());
1259
1260 let new_state = reduce(
1261 state,
1262 PipelineEvent::DevelopmentIterationStarted { iteration: 2 },
1263 );
1264
1265 assert!(!new_state.continuation.is_continuation());
1266 assert_eq!(new_state.iteration, 2);
1267 }
1268
1269 #[test]
1270 fn test_iteration_completed_resets_continuation() {
1271 use crate::reducer::state::{ContinuationState, DevelopmentStatus};
1272
1273 let mut state = create_test_state();
1274 state.phase = PipelinePhase::Development;
1275 state.continuation = ContinuationState::new().trigger_continuation(
1276 DevelopmentStatus::Partial,
1277 "Work".to_string(),
1278 None,
1279 None,
1280 );
1281
1282 let new_state = reduce(
1283 state,
1284 PipelineEvent::DevelopmentIterationCompleted {
1285 iteration: 1,
1286 output_valid: true,
1287 },
1288 );
1289
1290 assert!(!new_state.continuation.is_continuation());
1291 assert_eq!(new_state.phase, PipelinePhase::CommitMessage);
1292 }
1293
1294 #[test]
1295 fn test_development_phase_completed_resets_continuation() {
1296 use crate::reducer::state::{ContinuationState, DevelopmentStatus};
1297
1298 let mut state = create_test_state();
1299 state.phase = PipelinePhase::Development;
1300 state.continuation = ContinuationState::new().trigger_continuation(
1301 DevelopmentStatus::Partial,
1302 "Work".to_string(),
1303 None,
1304 None,
1305 );
1306
1307 let new_state = reduce(state, PipelineEvent::DevelopmentPhaseCompleted);
1308
1309 assert!(!new_state.continuation.is_continuation());
1310 assert_eq!(new_state.phase, PipelinePhase::Review);
1311 }
1312
1313 #[test]
1314 fn test_multiple_continuation_triggers_accumulate() {
1315 use crate::reducer::state::DevelopmentStatus;
1316
1317 let state = create_test_state();
1318
1319 let state = reduce(
1321 state,
1322 PipelineEvent::DevelopmentIterationContinuationTriggered {
1323 iteration: 1,
1324 status: DevelopmentStatus::Partial,
1325 summary: "First attempt".to_string(),
1326 files_changed: None,
1327 next_steps: None,
1328 },
1329 );
1330 assert_eq!(state.continuation.continuation_attempt, 1);
1331
1332 let state = reduce(
1334 state,
1335 PipelineEvent::DevelopmentIterationContinuationTriggered {
1336 iteration: 1,
1337 status: DevelopmentStatus::Partial,
1338 summary: "Second attempt".to_string(),
1339 files_changed: None,
1340 next_steps: None,
1341 },
1342 );
1343 assert_eq!(state.continuation.continuation_attempt, 2);
1344 assert_eq!(
1345 state.continuation.previous_summary,
1346 Some("Second attempt".to_string())
1347 );
1348 }
1349}