@@ -467,6 +467,10 @@ pub fn has_maintainer_reaction_with_callback(
467467 } ;
468468
469469 let endorser_min_rank = integrity_level_rank ( endorser_min) ;
470+ let item_updated_at = item
471+ . get ( "updatedAt" )
472+ . or_else ( || item. get ( "updated_at" ) )
473+ . and_then ( |v| v. as_str ( ) ) ;
470474
471475 for node in nodes. iter ( ) . take ( MAX_REACTIONS_TO_CHECK ) {
472476 let content = match node. get ( "content" ) . and_then ( |v| v. as_str ( ) ) {
@@ -490,6 +494,26 @@ pub fn has_maintainer_reaction_with_callback(
490494 None => continue ,
491495 } ;
492496
497+ let reaction_created_at = node
498+ . get ( "createdAt" )
499+ . or_else ( || node. get ( "created_at" ) )
500+ . and_then ( |v| v. as_str ( ) ) ;
501+ if let ( Some ( item_updated) , Some ( reaction_created) ) = ( item_updated_at, reaction_created_at) {
502+ if item_updated > reaction_created {
503+ crate :: log_debug ( & format ! (
504+ "[integrity] {}: skipping stale {} reaction {} from @{} \
505+ (item updatedAt={} > reaction createdAt={})",
506+ repo_full_name,
507+ reaction_kind,
508+ content,
509+ login,
510+ item_updated,
511+ reaction_created
512+ ) ) ;
513+ continue ;
514+ }
515+ }
516+
493517 // Fetch reactor's collaborator permission to determine their integrity level.
494518 let perm = super :: backend:: get_collaborator_permission_with_callback (
495519 callback, owner, repo, login,
@@ -2008,6 +2032,101 @@ mod tests {
20082032 ) ) ;
20092033 }
20102034
2035+ #[ test]
2036+ fn test_has_maintainer_reaction_honors_unmodified_item_endorsement ( ) {
2037+ let ctx = ctx_with_endorsement_reactions ( vec ! [ "THUMBS_UP" ] ) ;
2038+ let item = serde_json:: json!( {
2039+ "number" : 42 ,
2040+ "updatedAt" : "2026-04-20T00:00:00Z" ,
2041+ "reactions" : { "nodes" : [ {
2042+ "user" : { "login" : "alice" } ,
2043+ "content" : "THUMBS_UP" ,
2044+ "createdAt" : "2026-04-20T00:00:00Z"
2045+ } ] }
2046+ } ) ;
2047+ assert ! ( has_maintainer_reaction_with_callback(
2048+ & item, "owner/repo" , & ctx. endorsement_reactions, "approved" , & ctx,
2049+ admin_permission_callback, "endorsement"
2050+ ) ) ;
2051+ }
2052+
2053+ #[ test]
2054+ fn test_has_maintainer_reaction_skips_stale_endorsement_after_edit ( ) {
2055+ let ctx = ctx_with_endorsement_reactions ( vec ! [ "THUMBS_UP" ] ) ;
2056+ let item = serde_json:: json!( {
2057+ "number" : 42 ,
2058+ "updatedAt" : "2026-04-21T00:00:00Z" ,
2059+ "reactions" : { "nodes" : [ {
2060+ "user" : { "login" : "alice" } ,
2061+ "content" : "THUMBS_UP" ,
2062+ "createdAt" : "2026-04-20T00:00:00Z"
2063+ } ] }
2064+ } ) ;
2065+ assert ! ( !has_maintainer_reaction_with_callback(
2066+ & item, "owner/repo" , & ctx. endorsement_reactions, "approved" , & ctx,
2067+ admin_permission_callback, "endorsement"
2068+ ) ) ;
2069+ }
2070+
2071+ #[ test]
2072+ fn test_has_maintainer_reaction_honors_endorsement_added_after_edit ( ) {
2073+ let ctx = ctx_with_endorsement_reactions ( vec ! [ "THUMBS_UP" ] ) ;
2074+ let item = serde_json:: json!( {
2075+ "number" : 42 ,
2076+ "updated_at" : "2026-04-20T00:00:00Z" ,
2077+ "reactions" : { "nodes" : [ {
2078+ "user" : { "login" : "alice" } ,
2079+ "content" : "THUMBS_UP" ,
2080+ "createdAt" : "2026-04-21T00:00:00Z"
2081+ } ] }
2082+ } ) ;
2083+ assert ! ( has_maintainer_reaction_with_callback(
2084+ & item, "owner/repo" , & ctx. endorsement_reactions, "approved" , & ctx,
2085+ admin_permission_callback, "endorsement"
2086+ ) ) ;
2087+ }
2088+
2089+ #[ test]
2090+ fn test_has_maintainer_reaction_counts_fresh_when_stale_and_fresh_mixed ( ) {
2091+ let ctx = ctx_with_endorsement_reactions ( vec ! [ "THUMBS_UP" ] ) ;
2092+ let item = serde_json:: json!( {
2093+ "number" : 42 ,
2094+ "updatedAt" : "2026-04-21T00:00:00Z" ,
2095+ "reactions" : { "nodes" : [
2096+ {
2097+ "user" : { "login" : "alice" } ,
2098+ "content" : "THUMBS_UP" ,
2099+ "createdAt" : "2026-04-20T00:00:00Z"
2100+ } ,
2101+ {
2102+ "user" : { "login" : "bob" } ,
2103+ "content" : "THUMBS_UP" ,
2104+ "createdAt" : "2026-04-22T00:00:00Z"
2105+ }
2106+ ] }
2107+ } ) ;
2108+ assert ! ( has_maintainer_reaction_with_callback(
2109+ & item, "owner/repo" , & ctx. endorsement_reactions, "approved" , & ctx,
2110+ admin_permission_callback, "endorsement"
2111+ ) ) ;
2112+ }
2113+
2114+ #[ test]
2115+ fn test_has_maintainer_reaction_missing_timestamps_keeps_existing_behavior ( ) {
2116+ let ctx = ctx_with_endorsement_reactions ( vec ! [ "THUMBS_UP" ] ) ;
2117+ let item = serde_json:: json!( {
2118+ "number" : 42 ,
2119+ "reactions" : { "nodes" : [ {
2120+ "user" : { "login" : "alice" } ,
2121+ "content" : "THUMBS_UP"
2122+ } ] }
2123+ } ) ;
2124+ assert ! ( has_maintainer_reaction_with_callback(
2125+ & item, "owner/repo" , & ctx. endorsement_reactions, "approved" , & ctx,
2126+ admin_permission_callback, "endorsement"
2127+ ) ) ;
2128+ }
2129+
20112130 #[ test]
20122131 fn test_cap_integrity_at_none ( ) {
20132132 let ctx = test_ctx ( ) ;
0 commit comments