@@ -13,6 +13,7 @@ def make_state():
1313 return {
1414 "schema_version" : reviewer_bot .STATE_SCHEMA_VERSION ,
1515 "freshness_runtime_epoch" : reviewer_bot .FRESHNESS_RUNTIME_EPOCH_V18 ,
16+ "status_projection_epoch" : reviewer_bot .STATUS_PROJECTION_EPOCH ,
1617 "last_updated" : None ,
1718 "current_index" : 0 ,
1819 "queue" : [
@@ -360,6 +361,111 @@ def test_main_schedule_reviewer_review_repair_marks_item_for_label_sync(monkeypa
360361 assert synced_issue_numbers == [42 ]
361362
362363
364+ def test_main_schedule_status_projection_epoch_mismatch_triggers_label_repair_sweep (monkeypatch ):
365+ monkeypatch .setenv ("EVENT_NAME" , "schedule" )
366+ monkeypatch .setenv ("EVENT_ACTION" , "" )
367+ state = make_state ()
368+ state ["status_projection_epoch" ] = "status_projection_v1"
369+ review = reviewer_bot .ensure_review_entry (state , 42 , create = True )
370+ assert review is not None
371+ review ["current_reviewer" ] = "alice"
372+ synced_issue_numbers = []
373+ saved_epochs = []
374+
375+ monkeypatch .setattr (reviewer_bot , "acquire_state_issue_lease_lock" , lambda : None )
376+ monkeypatch .setattr (reviewer_bot , "release_state_issue_lease_lock" , lambda : True )
377+ monkeypatch .setattr (reviewer_bot , "load_state" , lambda * args , ** kwargs : state )
378+ monkeypatch .setattr (reviewer_bot , "process_pass_until_expirations" , lambda current : (current , []))
379+ monkeypatch .setattr (reviewer_bot , "sync_members_with_queue" , lambda current : (current , []))
380+ monkeypatch .setattr (reviewer_bot , "handle_scheduled_check" , lambda current : False )
381+ monkeypatch .setattr (reviewer_bot , "list_open_items_with_status_labels" , lambda : [99 ])
382+ monkeypatch .setattr (
383+ reviewer_bot ,
384+ "sync_status_labels_for_items" ,
385+ lambda current , issue_numbers : synced_issue_numbers .extend (issue_numbers ) or True ,
386+ )
387+ monkeypatch .setattr (
388+ reviewer_bot ,
389+ "save_state" ,
390+ lambda current : saved_epochs .append (current .get ("status_projection_epoch" )) or True ,
391+ )
392+
393+ reviewer_bot .main ()
394+
395+ assert synced_issue_numbers == [42 , 99 ]
396+ assert saved_epochs [- 1 ] == reviewer_bot .STATUS_PROJECTION_EPOCH
397+
398+
399+ def test_main_schedule_status_projection_epoch_not_advanced_on_label_sync_failure (monkeypatch ):
400+ monkeypatch .setenv ("EVENT_NAME" , "schedule" )
401+ monkeypatch .setenv ("EVENT_ACTION" , "" )
402+ state = make_state ()
403+ state ["status_projection_epoch" ] = "status_projection_v1"
404+ review = reviewer_bot .ensure_review_entry (state , 42 , create = True )
405+ assert review is not None
406+ review ["current_reviewer" ] = "alice"
407+ saved_epochs = []
408+
409+ monkeypatch .setattr (reviewer_bot , "acquire_state_issue_lease_lock" , lambda : None )
410+ monkeypatch .setattr (reviewer_bot , "release_state_issue_lease_lock" , lambda : True )
411+ monkeypatch .setattr (reviewer_bot , "load_state" , lambda * args , ** kwargs : state )
412+ monkeypatch .setattr (reviewer_bot , "process_pass_until_expirations" , lambda current : (current , []))
413+ monkeypatch .setattr (reviewer_bot , "sync_members_with_queue" , lambda current : (current , []))
414+ monkeypatch .setattr (reviewer_bot , "handle_scheduled_check" , lambda current : False )
415+ monkeypatch .setattr (reviewer_bot , "list_open_items_with_status_labels" , lambda : [42 ])
416+ monkeypatch .setattr (
417+ reviewer_bot ,
418+ "sync_status_labels_for_items" ,
419+ lambda current , issue_numbers : (_ for _ in ()).throw (RuntimeError ("projection exploded" )),
420+ )
421+ monkeypatch .setattr (
422+ reviewer_bot ,
423+ "save_state" ,
424+ lambda current : saved_epochs .append (current .get ("status_projection_epoch" )) or True ,
425+ )
426+
427+ reviewer_bot .main ()
428+ assert all (epoch != reviewer_bot .STATUS_PROJECTION_EPOCH for epoch in saved_epochs )
429+
430+
431+ def test_main_schedule_projection_epoch_repair_relabels_previously_repaired_pr (monkeypatch ):
432+ monkeypatch .setenv ("EVENT_NAME" , "schedule" )
433+ monkeypatch .setenv ("EVENT_ACTION" , "" )
434+ state = make_state ()
435+ state ["status_projection_epoch" ] = "status_projection_v1"
436+ review = reviewer_bot .ensure_review_entry (state , 256 , create = True )
437+ assert review is not None
438+ review ["current_reviewer" ] = "vccjgust"
439+ review ["reviewer_review" ]["accepted" ] = {
440+ "semantic_key" : "pull_request_review:3821749029" ,
441+ "timestamp" : "2026-02-18T20:28:12Z" ,
442+ "actor" : "vccjgust" ,
443+ "reviewed_head_sha" : "head-1" ,
444+ "source_precedence" : 1 ,
445+ "payload" : {},
446+ }
447+ review ["active_head_sha" ] = "head-1"
448+ synced_issue_numbers = []
449+
450+ monkeypatch .setattr (reviewer_bot , "acquire_state_issue_lease_lock" , lambda : None )
451+ monkeypatch .setattr (reviewer_bot , "release_state_issue_lease_lock" , lambda : True )
452+ monkeypatch .setattr (reviewer_bot , "load_state" , lambda * args , ** kwargs : state )
453+ monkeypatch .setattr (reviewer_bot , "process_pass_until_expirations" , lambda current : (current , []))
454+ monkeypatch .setattr (reviewer_bot , "sync_members_with_queue" , lambda current : (current , []))
455+ monkeypatch .setattr (reviewer_bot , "handle_scheduled_check" , lambda current : False )
456+ monkeypatch .setattr (reviewer_bot , "list_open_items_with_status_labels" , lambda : [256 ])
457+ monkeypatch .setattr (
458+ reviewer_bot ,
459+ "sync_status_labels_for_items" ,
460+ lambda current , issue_numbers : synced_issue_numbers .extend (issue_numbers ) or True ,
461+ )
462+ monkeypatch .setattr (reviewer_bot , "save_state" , lambda current : True )
463+
464+ reviewer_bot .main ()
465+
466+ assert synced_issue_numbers == [256 ]
467+
468+
363469def test_main_mutating_event_fails_closed_when_state_unavailable (monkeypatch ):
364470 monkeypatch .setenv ("EVENT_NAME" , "issue_comment" )
365471 monkeypatch .setenv ("EVENT_ACTION" , "created" )
0 commit comments