@@ -120,64 +120,83 @@ func TestValidateJobTypeChange(t *testing.T) {
120120 errorSubstr : "can only be set to 'adaptive'" ,
121121 },
122122
123- // Merge job type transitions - only merge allowed
123+ // Merge job type transitions - merge, scheduled, and other are all allowed in-place;
124+ // transitions to CI or Adaptive are blocked (those would require replacement at the
125+ // ModifyPlan level but validateJobTypeChange still guards them).
124126 {
125127 name : "merge to ci - not allowed" ,
126128 prevType : JobTypeMerge ,
127129 newType : JobTypeCI ,
128130 expectError : true ,
129- errorSubstr : "can only be set to 'merge '" ,
131+ errorSubstr : "cannot change job_type to 'ci '" ,
130132 },
131133 {
132- name : "merge to scheduled - not allowed" ,
134+ name : "merge to scheduled - allowed in-place " ,
133135 prevType : JobTypeMerge ,
134136 newType : JobTypeScheduled ,
135- expectError : true ,
136- errorSubstr : "can only be set to 'merge'" ,
137+ expectError : false ,
138+ },
139+ {
140+ name : "merge to other - allowed in-place" ,
141+ prevType : JobTypeMerge ,
142+ newType : JobTypeOther ,
143+ expectError : false ,
137144 },
138145
139- // Scheduled job type transitions - scheduled or other allowed
146+ // Scheduled job type transitions - scheduled, other, and merge are all allowed in-place
140147 {
141148 name : "scheduled to other - allowed" ,
142149 prevType : JobTypeScheduled ,
143150 newType : JobTypeOther ,
144151 expectError : false ,
145152 },
153+ {
154+ name : "scheduled to merge - allowed in-place" ,
155+ prevType : JobTypeScheduled ,
156+ newType : JobTypeMerge ,
157+ expectError : false ,
158+ },
146159 {
147160 name : "scheduled to ci - not allowed" ,
148161 prevType : JobTypeScheduled ,
149162 newType : JobTypeCI ,
150163 expectError : true ,
151- errorSubstr : "can only be set to 'scheduled' or 'other '" ,
164+ errorSubstr : "cannot change job_type to 'ci '" ,
152165 },
153166 {
154167 name : "scheduled to adaptive - not allowed" ,
155168 prevType : JobTypeScheduled ,
156169 newType : JobTypeAdaptive ,
157170 expectError : true ,
158- errorSubstr : "can only be set to 'scheduled' or 'other '" ,
171+ errorSubstr : "cannot change job_type to 'adaptive '" ,
159172 },
160173
161- // Other job type transitions - scheduled or other allowed
174+ // Other job type transitions - scheduled, other, and merge are all allowed in-place
162175 {
163176 name : "other to scheduled - allowed" ,
164177 prevType : JobTypeOther ,
165178 newType : JobTypeScheduled ,
166179 expectError : false ,
167180 },
181+ {
182+ name : "other to merge - allowed in-place" ,
183+ prevType : JobTypeOther ,
184+ newType : JobTypeMerge ,
185+ expectError : false ,
186+ },
168187 {
169188 name : "other to ci - not allowed" ,
170189 prevType : JobTypeOther ,
171190 newType : JobTypeCI ,
172191 expectError : true ,
173- errorSubstr : "can only be set to 'scheduled' or 'other '" ,
192+ errorSubstr : "cannot change job_type to 'ci '" ,
174193 },
175194 {
176195 name : "other to adaptive - not allowed" ,
177196 prevType : JobTypeOther ,
178197 newType : JobTypeAdaptive ,
179198 expectError : true ,
180- errorSubstr : "can only be set to 'scheduled' or 'other '" ,
199+ errorSubstr : "cannot change job_type to 'adaptive '" ,
181200 },
182201 }
183202
@@ -228,20 +247,20 @@ func TestValidateJobTypeChange_AllTransitions(t *testing.T) {
228247 JobTypeMerge : {
229248 JobTypeCI : false ,
230249 JobTypeMerge : true ,
231- JobTypeScheduled : false ,
232- JobTypeOther : false ,
250+ JobTypeScheduled : true , // in-place transition allowed
251+ JobTypeOther : true , // in-place transition allowed
233252 JobTypeAdaptive : false ,
234253 },
235254 JobTypeScheduled : {
236255 JobTypeCI : false ,
237- JobTypeMerge : true , // merge is allowed from scheduled (not in original server code but reasonable)
256+ JobTypeMerge : true , // in-place transition allowed
238257 JobTypeScheduled : true ,
239258 JobTypeOther : true ,
240259 JobTypeAdaptive : false ,
241260 },
242261 JobTypeOther : {
243262 JobTypeCI : false ,
244- JobTypeMerge : true , // merge is allowed from other (not in original server code but reasonable)
263+ JobTypeMerge : true , // in-place transition allowed
245264 JobTypeScheduled : true ,
246265 JobTypeOther : true ,
247266 JobTypeAdaptive : false ,
@@ -305,3 +324,57 @@ func TestJobTypeConstants(t *testing.T) {
305324 }
306325 }
307326}
327+
328+ func TestRequiresJobTypeReplacement (t * testing.T ) {
329+ t .Parallel ()
330+
331+ tests := []struct {
332+ name string
333+ oldType string
334+ newType string
335+ expected bool
336+ }{
337+ // Same type — never requires replacement
338+ {name : "ci to ci" , oldType : JobTypeCI , newType : JobTypeCI , expected : false },
339+ {name : "scheduled to scheduled" , oldType : JobTypeScheduled , newType : JobTypeScheduled , expected : false },
340+ {name : "other to other" , oldType : JobTypeOther , newType : JobTypeOther , expected : false },
341+ {name : "merge to merge" , oldType : JobTypeMerge , newType : JobTypeMerge , expected : false },
342+ {name : "adaptive to adaptive" , oldType : JobTypeAdaptive , newType : JobTypeAdaptive , expected : false },
343+
344+ // CI transitions — always require replacement
345+ {name : "ci to scheduled" , oldType : JobTypeCI , newType : JobTypeScheduled , expected : true },
346+ {name : "ci to other" , oldType : JobTypeCI , newType : JobTypeOther , expected : true },
347+ {name : "ci to merge" , oldType : JobTypeCI , newType : JobTypeMerge , expected : true },
348+ {name : "ci to adaptive" , oldType : JobTypeCI , newType : JobTypeAdaptive , expected : true },
349+ {name : "scheduled to ci" , oldType : JobTypeScheduled , newType : JobTypeCI , expected : true },
350+ {name : "other to ci" , oldType : JobTypeOther , newType : JobTypeCI , expected : true },
351+ {name : "merge to ci" , oldType : JobTypeMerge , newType : JobTypeCI , expected : true },
352+
353+ // Adaptive transitions — always require replacement
354+ {name : "adaptive to scheduled" , oldType : JobTypeAdaptive , newType : JobTypeScheduled , expected : true },
355+ {name : "adaptive to other" , oldType : JobTypeAdaptive , newType : JobTypeOther , expected : true },
356+ {name : "adaptive to merge" , oldType : JobTypeAdaptive , newType : JobTypeMerge , expected : true },
357+ {name : "scheduled to adaptive" , oldType : JobTypeScheduled , newType : JobTypeAdaptive , expected : true },
358+ {name : "other to adaptive" , oldType : JobTypeOther , newType : JobTypeAdaptive , expected : true },
359+ {name : "merge to adaptive" , oldType : JobTypeMerge , newType : JobTypeAdaptive , expected : true },
360+
361+ // In-place transitions (scheduled / other / merge)
362+ {name : "scheduled to other" , oldType : JobTypeScheduled , newType : JobTypeOther , expected : false },
363+ {name : "scheduled to merge" , oldType : JobTypeScheduled , newType : JobTypeMerge , expected : false },
364+ {name : "other to scheduled" , oldType : JobTypeOther , newType : JobTypeScheduled , expected : false },
365+ {name : "other to merge" , oldType : JobTypeOther , newType : JobTypeMerge , expected : false },
366+ {name : "merge to scheduled" , oldType : JobTypeMerge , newType : JobTypeScheduled , expected : false },
367+ {name : "merge to other" , oldType : JobTypeMerge , newType : JobTypeOther , expected : false },
368+ }
369+
370+ for _ , tc := range tests {
371+ tc := tc
372+ t .Run (tc .name , func (t * testing.T ) {
373+ t .Parallel ()
374+ got := requiresJobTypeReplacement (tc .oldType , tc .newType )
375+ if got != tc .expected {
376+ t .Errorf ("requiresJobTypeReplacement(%q, %q) = %v, want %v" , tc .oldType , tc .newType , got , tc .expected )
377+ }
378+ })
379+ }
380+ }
0 commit comments