@@ -333,6 +333,246 @@ describe('diff', () => {
333333 expect ( diff . modified [ 0 ] . changes ) . toContainEqual ( expect . stringContaining ( 'step "s1" data' ) ) ;
334334 } ) ;
335335
336+ it ( 'detects retry_override added to step' , ( ) => {
337+ const withRetry : SequenceToml = {
338+ ...intervalSequence ,
339+ steps : {
340+ poll : {
341+ path : '/webhooks/poll' ,
342+ retry_override : {
343+ min_retries : 3 ,
344+ delay_seconds : 10 ,
345+ strategy : 'fixed' ,
346+ jitter : false ,
347+ } ,
348+ } ,
349+ } ,
350+ } ;
351+ const diff = computeSequenceDiff ( [ withRetry ] , [ intervalSequence ] ) ;
352+ expect ( diff . modified ) . toHaveLength ( 1 ) ;
353+ expect ( diff . modified [ 0 ] . changes ) . toContainEqual (
354+ expect . stringContaining ( 'step "poll" retry_override' )
355+ ) ;
356+ } ) ;
357+
358+ it ( 'detects retry_override removed from step' , ( ) => {
359+ const withRetry : SequenceToml = {
360+ ...intervalSequence ,
361+ steps : {
362+ poll : {
363+ path : '/webhooks/poll' ,
364+ retry_override : {
365+ min_retries : 3 ,
366+ delay_seconds : 10 ,
367+ strategy : 'fixed' ,
368+ jitter : false ,
369+ } ,
370+ } ,
371+ } ,
372+ } ;
373+ const diff = computeSequenceDiff ( [ intervalSequence ] , [ withRetry ] ) ;
374+ expect ( diff . modified ) . toHaveLength ( 1 ) ;
375+ expect ( diff . modified [ 0 ] . changes ) . toContainEqual (
376+ expect . stringContaining ( 'step "poll" retry_override: fixed, 3 retries, 10s delay → (project defaults)' )
377+ ) ;
378+ } ) ;
379+
380+ it ( 'detects retry_override strategy change' , ( ) => {
381+ const fixed : SequenceToml = {
382+ ...intervalSequence ,
383+ steps : {
384+ poll : {
385+ path : '/webhooks/poll' ,
386+ retry_override : {
387+ min_retries : 3 ,
388+ delay_seconds : 10 ,
389+ strategy : 'fixed' ,
390+ jitter : false ,
391+ } ,
392+ } ,
393+ } ,
394+ } ;
395+ const exponential : SequenceToml = {
396+ ...intervalSequence ,
397+ steps : {
398+ poll : {
399+ path : '/webhooks/poll' ,
400+ retry_override : {
401+ min_retries : 3 ,
402+ delay_seconds : 10 ,
403+ strategy : 'exponential' ,
404+ backoff_factor : 2.0 ,
405+ max_delay_seconds : 300 ,
406+ jitter : true ,
407+ } ,
408+ } ,
409+ } ,
410+ } ;
411+ const diff = computeSequenceDiff ( [ exponential ] , [ fixed ] ) ;
412+ expect ( diff . modified ) . toHaveLength ( 1 ) ;
413+ expect ( diff . modified [ 0 ] . changes ) . toContainEqual (
414+ expect . stringContaining ( 'step "poll" retry_override: fixed, 3 retries, 10s delay → exponential, 3 retries, 10s delay, 2x backoff, 300s max, jitter' )
415+ ) ;
416+ } ) ;
417+
418+ it ( 'no diff when retry_override is identical' , ( ) => {
419+ const withRetry : SequenceToml = {
420+ ...intervalSequence ,
421+ steps : {
422+ poll : {
423+ path : '/webhooks/poll' ,
424+ retry_override : {
425+ min_retries : 3 ,
426+ delay_seconds : 10 ,
427+ strategy : 'fixed' ,
428+ jitter : false ,
429+ } ,
430+ } ,
431+ } ,
432+ } ;
433+ const diff = computeSequenceDiff ( [ withRetry ] , [ { ...withRetry } ] ) ;
434+ expect ( diff . modified ) . toHaveLength ( 0 ) ;
435+ } ) ;
436+
437+ it ( 'no diff when exponential backoff_factor defaults to 2.0' , ( ) => {
438+ const localWithoutBackoff : SequenceToml = {
439+ ...intervalSequence ,
440+ steps : {
441+ poll : {
442+ path : '/webhooks/poll' ,
443+ retry_override : {
444+ min_retries : 3 ,
445+ delay_seconds : 10 ,
446+ strategy : 'exponential' ,
447+ max_delay_seconds : 300 ,
448+ jitter : false ,
449+ } ,
450+ } ,
451+ } ,
452+ } ;
453+ const remoteWithBackoff : SequenceToml = {
454+ ...intervalSequence ,
455+ steps : {
456+ poll : {
457+ path : '/webhooks/poll' ,
458+ retry_override : {
459+ min_retries : 3 ,
460+ delay_seconds : 10 ,
461+ strategy : 'exponential' ,
462+ backoff_factor : 2.0 ,
463+ max_delay_seconds : 300 ,
464+ jitter : false ,
465+ } ,
466+ } ,
467+ } ,
468+ } ;
469+ const diff = computeSequenceDiff ( [ localWithoutBackoff ] , [ remoteWithBackoff ] ) ;
470+ expect ( diff . modified ) . toHaveLength ( 0 ) ;
471+ } ) ;
472+
473+ it ( 'no diff when jitter defaults to false' , ( ) => {
474+ const localNoJitter : SequenceToml = {
475+ ...intervalSequence ,
476+ steps : {
477+ poll : {
478+ path : '/webhooks/poll' ,
479+ retry_override : {
480+ min_retries : 3 ,
481+ delay_seconds : 10 ,
482+ strategy : 'fixed' ,
483+ } as SequenceStepToml [ 'retry_override' ] & object ,
484+ } ,
485+ } ,
486+ } ;
487+ const remoteWithJitter : SequenceToml = {
488+ ...intervalSequence ,
489+ steps : {
490+ poll : {
491+ path : '/webhooks/poll' ,
492+ retry_override : {
493+ min_retries : 3 ,
494+ delay_seconds : 10 ,
495+ strategy : 'fixed' ,
496+ jitter : false ,
497+ } ,
498+ } ,
499+ } ,
500+ } ;
501+ const diff = computeSequenceDiff ( [ localNoJitter ] , [ remoteWithJitter ] ) ;
502+ expect ( diff . modified ) . toHaveLength ( 0 ) ;
503+ } ) ;
504+
505+ it ( 'no diff when fixed strategy strips backoff fields' , ( ) => {
506+ const localWithExtra : SequenceToml = {
507+ ...intervalSequence ,
508+ steps : {
509+ poll : {
510+ path : '/webhooks/poll' ,
511+ retry_override : {
512+ min_retries : 3 ,
513+ delay_seconds : 10 ,
514+ strategy : 'fixed' ,
515+ backoff_factor : 2.0 ,
516+ max_delay_seconds : 300 ,
517+ jitter : false ,
518+ } ,
519+ } ,
520+ } ,
521+ } ;
522+ const remoteClean : SequenceToml = {
523+ ...intervalSequence ,
524+ steps : {
525+ poll : {
526+ path : '/webhooks/poll' ,
527+ retry_override : {
528+ min_retries : 3 ,
529+ delay_seconds : 10 ,
530+ strategy : 'fixed' ,
531+ jitter : false ,
532+ } ,
533+ } ,
534+ } ,
535+ } ;
536+ const diff = computeSequenceDiff ( [ localWithExtra ] , [ remoteClean ] ) ;
537+ expect ( diff . modified ) . toHaveLength ( 0 ) ;
538+ } ) ;
539+
540+ it ( 'still detects actual retry_override changes after normalization' , ( ) => {
541+ const local : SequenceToml = {
542+ ...intervalSequence ,
543+ steps : {
544+ poll : {
545+ path : '/webhooks/poll' ,
546+ retry_override : {
547+ min_retries : 5 ,
548+ delay_seconds : 10 ,
549+ strategy : 'fixed' ,
550+ jitter : false ,
551+ } ,
552+ } ,
553+ } ,
554+ } ;
555+ const remote : SequenceToml = {
556+ ...intervalSequence ,
557+ steps : {
558+ poll : {
559+ path : '/webhooks/poll' ,
560+ retry_override : {
561+ min_retries : 3 ,
562+ delay_seconds : 10 ,
563+ strategy : 'fixed' ,
564+ jitter : false ,
565+ } ,
566+ } ,
567+ } ,
568+ } ;
569+ const diff = computeSequenceDiff ( [ local ] , [ remote ] ) ;
570+ expect ( diff . modified ) . toHaveLength ( 1 ) ;
571+ expect ( diff . modified [ 0 ] . changes ) . toContainEqual (
572+ expect . stringContaining ( 'step "poll" retry_override' )
573+ ) ;
574+ } ) ;
575+
336576 it ( 'handles empty local and remote sequences' , ( ) => {
337577 const diff = computeSequenceDiff ( [ ] , [ ] ) ;
338578 expect ( diff . added ) . toEqual ( [ ] ) ;
0 commit comments