diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/steps.go b/modules/mesh-delivery-pipeline/internal/pipeline/steps.go index 4781a04d..e239c575 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/steps.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/steps.go @@ -272,8 +272,8 @@ func stepOfAsk(p seat.Pipeline, id string) int { } // endPipeline ends a pipeline whose every step ended: passed when every required step passed or was carried, else -// failed, naming each required step that held it (ADR 0666 §7, §9; design 47 G §24, §26). A failed optional step holds -// nothing. An approval step still waiting when a required step did not pass is `not asked` (§26). It says whether the +// failed, naming each required step that held it (ADR 0666 §7, §9; design 47 G §24, §26). A failed optional step, and an optional step +// still waiting, hold nothing. An approval step still waiting when a required step did not pass is `not asked` (§26). It says whether the // pipeline ended now, its end count one higher. func endPipeline(p *seat.Pipeline, now time.Time) bool { if p.Outcome != seat.PipelineOutcomeRunning { @@ -286,6 +286,10 @@ func endPipeline(p *seat.Pipeline, now time.Time) bool { case s.Step == seat.StepResultStepApproval && s.Outcome == seat.StepResultOutcomeWaiting: notAsked = i continue + case s.Outcome == seat.StepResultOutcomeWaiting && s.Status != seat.StepResultStatusRequired: + // An optional step still waiting (a code review nobody has given) holds nothing: only a required one holds the + // pipeline's end (A-104). + continue case s.Outcome == seat.StepResultOutcomeWaiting, s.Outcome == seat.StepResultOutcomeRunning: return false } diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/steps_test.go b/modules/mesh-delivery-pipeline/internal/pipeline/steps_test.go index 3f6c18bc..331cb6b8 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/steps_test.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/steps_test.go @@ -276,3 +276,25 @@ func TestApprovalEnds(t *testing.T) { t.Errorf("asked %d; %+v", len(asker.asked), p) } } + +// An optional step still waiting (a code review nobody has given) does not hold a pipeline's end; a required one does +// (design 47 G A-104). +func TestAnOptionalStepStillWaitingHoldsNoEnd(t *testing.T) { + now := time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC) + for name, c := range map[string]struct { + review string + ends bool + }{"optional review waiting": {seat.StepResultStatusOptional, true}, "required review waiting": {seat.StepResultStatusRequired, false}} { + p := seat.Pipeline{Outcome: seat.PipelineOutcomeRunning, Steps: []seat.StepResult{ + {Step: seat.StepResultStepBuild, Status: seat.StepResultStatusRequired, Outcome: seat.StepResultOutcomePassed}, + {Step: seat.StepResultStepReview, Status: c.review, Outcome: seat.StepResultOutcomeWaiting}, + {Step: seat.StepResultStepApproval, Status: seat.StepResultStatusRequired, Outcome: seat.StepResultOutcomePassed}, + }} + if got := endPipeline(&p, now); got != c.ends { + t.Errorf("%s: ended = %v, want %v", name, got, c.ends) + } + if c.ends && p.Outcome != seat.PipelineOutcomePassed { + t.Errorf("%s: outcome %s", name, p.Outcome) + } + } +}