@@ -721,6 +721,112 @@ describe('grant-suggestions-handler', () => {
721721 expect ( SuggestionGrant . grantSuggestions ) . to . have . been . calledOnce ;
722722 } ) ;
723723
724+ it ( 'does not revoke V2 re-detected OUTDATED grant (has previousDeployment stamp)' , async ( ) => {
725+ const s1 = {
726+ getId : ( ) => 'sugg-1' , getRank : ( ) => 1 , getStatus : ( ) => 'NEW' ,
727+ } ;
728+
729+ const existingToken = { getId : ( ) => 'tok-1' , getRemaining : ( ) => 1 } ;
730+
731+ // V2 re-detected OUTDATED: has previousDeployment stamp in data
732+ const sugg2V2Outdated = {
733+ getId : ( ) => 'sugg-2' ,
734+ getRank : ( ) => 2 ,
735+ getStatus : ( ) => 'OUTDATED' ,
736+ getData : ( ) => ( {
737+ recommendations : [ { previousDeployment : { altText : 'old text' , deployedAt : '2026-05-01' } } ] ,
738+ } ) ,
739+ } ;
740+
741+ const Suggestion = {
742+ allByOpportunityIdAndStatus : sandbox . stub ( ) . resolves ( [ s1 ] ) ,
743+ batchGetByKeys : sandbox . stub ( ) . resolves ( {
744+ data : [ sugg2V2Outdated ] ,
745+ unprocessed : [ ] ,
746+ } ) ,
747+ } ;
748+
749+ const SuggestionGrant = {
750+ splitSuggestionsByGrantStatus : sandbox . stub ( ) . resolves ( {
751+ grantedIds : [ ] ,
752+ grantIds : [ ] ,
753+ notGrantedIds : [ 'sugg-1' ] ,
754+ } ) ,
755+ allByIndexKeys : sandbox . stub ( ) . resolves ( [
756+ mkGrant ( 'sugg-2' , 'g2' ) ,
757+ ] ) ,
758+ grantSuggestions : sandbox . stub ( ) . resolves ( { success : true } ) ,
759+ revokeSuggestionGrant : sandbox . stub ( ) ,
760+ } ;
761+
762+ const Token = {
763+ findBySiteIdAndTokenType : sandbox . stub ( ) . resolves ( existingToken ) ,
764+ } ;
765+
766+ const dataAccess = { Suggestion, SuggestionGrant, Token } ;
767+
768+ await grantSuggestionsForOpportunity ( dataAccess , site , opportunity ) ;
769+
770+ // V2 OUTDATED grant must NOT be revoked — it's actionable work for PLG re-deploy
771+ expect ( SuggestionGrant . revokeSuggestionGrant ) . to . not . have . been . called ;
772+ // Token had remaining=1, fill with the ungranted NEW suggestion
773+ expect ( SuggestionGrant . grantSuggestions ) . to . have . been . calledOnce ;
774+ } ) ;
775+
776+ it ( 'revokes V1 legacy OUTDATED grant (no previousDeployment stamp)' , async ( ) => {
777+ const s1 = {
778+ getId : ( ) => 'sugg-1' , getRank : ( ) => 1 , getStatus : ( ) => 'NEW' ,
779+ } ;
780+
781+ const existingToken = { getId : ( ) => 'tok-1' , getRemaining : ( ) => 0 } ;
782+ const tokenAfterRevoke = { getId : ( ) => 'tok-1' , getRemaining : ( ) => 1 } ;
783+
784+ // V1 legacy OUTDATED: getData returns no previousDeployment stamp
785+ const sugg2V1Outdated = {
786+ getId : ( ) => 'sugg-2' ,
787+ getRank : ( ) => 2 ,
788+ getStatus : ( ) => 'OUTDATED' ,
789+ getData : ( ) => ( { recommendations : [ { altText : 'some text' } ] } ) ,
790+ } ;
791+
792+ const Suggestion = {
793+ allByOpportunityIdAndStatus : sandbox . stub ( ) . resolves ( [ s1 ] ) ,
794+ batchGetByKeys : sandbox . stub ( ) . resolves ( {
795+ data : [ sugg2V1Outdated ] ,
796+ unprocessed : [ ] ,
797+ } ) ,
798+ } ;
799+
800+ const SuggestionGrant = {
801+ splitSuggestionsByGrantStatus : sandbox . stub ( ) . resolves ( {
802+ grantedIds : [ ] ,
803+ grantIds : [ ] ,
804+ notGrantedIds : [ 'sugg-1' ] ,
805+ } ) ,
806+ allByIndexKeys : sandbox . stub ( ) . resolves ( [
807+ mkGrant ( 'sugg-2' , 'g2' ) ,
808+ ] ) ,
809+ grantSuggestions : sandbox . stub ( ) . resolves ( { success : true } ) ,
810+ revokeSuggestionGrant : sandbox . stub ( ) . resolves ( { success : true } ) ,
811+ } ;
812+
813+ const Token = {
814+ findBySiteIdAndTokenType : sandbox . stub ( ) ,
815+ } ;
816+ Token . findBySiteIdAndTokenType
817+ . onFirstCall ( ) . resolves ( existingToken )
818+ . onSecondCall ( ) . resolves ( tokenAfterRevoke ) ;
819+
820+ const dataAccess = { Suggestion, SuggestionGrant, Token } ;
821+
822+ await grantSuggestionsForOpportunity ( dataAccess , site , opportunity ) ;
823+
824+ // V1 OUTDATED grant must be revoked (stale, no re-detection stamp)
825+ expect ( SuggestionGrant . revokeSuggestionGrant ) . to . have . been . calledOnce ;
826+ expect ( SuggestionGrant . revokeSuggestionGrant ) . to . have . been . calledWith ( 'g2' ) ;
827+ expect ( SuggestionGrant . grantSuggestions ) . to . have . been . calledOnce ;
828+ } ) ;
829+
724830 it ( 'revokes grants with PENDING_VALIDATION status as stale' , async ( ) => {
725831 const s1 = {
726832 getId : ( ) => 'sugg-1' , getRank : ( ) => 1 , getStatus : ( ) => 'NEW' ,
0 commit comments