@@ -4,9 +4,12 @@ import {
44 fetchIssuesAndPullRequests ,
55 fetchWorkflowRuns ,
66 fetchPREnrichment ,
7+ fallbackToPreviousEnrichment ,
8+ pickEnrichmentFields ,
79 type RepoRef ,
810} from "../../src/app/services/api" ;
911import { clearCache } from "../../src/app/stores/cache" ;
12+ import { makePullRequest } from "../helpers/factories" ;
1013
1114vi . mock ( "../../src/app/lib/errors" , ( ) => ( {
1215 pushNotification : vi . fn ( ) ,
@@ -597,3 +600,130 @@ describe("fetchPREnrichment mergeStateStatus UNSTABLE override", () => {
597600 expect ( enrichments . get ( 100 ) ! . checkStatus ) . toBe ( "failure" ) ;
598601 } ) ;
599602} ) ;
603+
604+ // ── Enrichment carry-forward on backfill failure ──────────────────────────────
605+
606+ describe ( "fallbackToPreviousEnrichment" , ( ) => {
607+ it ( "returns next unchanged when previous is empty" , ( ) => {
608+ const next = [ makePullRequest ( { id : 1 , enriched : false } ) ] ;
609+ const result = fallbackToPreviousEnrichment ( [ ] , next ) ;
610+ expect ( result ) . toBe ( next ) ;
611+ } ) ;
612+
613+ it ( "passes through a PR that is already enriched in next (no carry-forward)" , ( ) => {
614+ const previous = [ makePullRequest ( { id : 1 , enriched : true , checkStatus : "failure" } ) ] ;
615+ const next = [ makePullRequest ( { id : 1 , enriched : true , checkStatus : "success" } ) ] ;
616+ const result = fallbackToPreviousEnrichment ( previous , next ) ;
617+ // Fresh enriched data wins — prior "failure" must not overwrite fresh "success".
618+ expect ( result [ 0 ] . checkStatus ) . toBe ( "success" ) ;
619+ expect ( result [ 0 ] ) . toBe ( next [ 0 ] ) ;
620+ } ) ;
621+
622+ it ( "passes through an unenriched next PR with no matching previous entry" , ( ) => {
623+ const previous = [ makePullRequest ( { id : 99 , enriched : true } ) ] ;
624+ const next = [ makePullRequest ( { id : 1 , enriched : false } ) ] ;
625+ const result = fallbackToPreviousEnrichment ( previous , next ) ;
626+ expect ( result [ 0 ] ) . toBe ( next [ 0 ] ) ;
627+ expect ( result [ 0 ] . enriched ) . toBe ( false ) ;
628+ } ) ;
629+
630+ it ( "passes through an unenriched next PR whose previous entry was also unenriched" , ( ) => {
631+ const previous = [ makePullRequest ( { id : 1 , enriched : false } ) ] ;
632+ const next = [ makePullRequest ( { id : 1 , enriched : false } ) ] ;
633+ const result = fallbackToPreviousEnrichment ( previous , next ) ;
634+ expect ( result [ 0 ] ) . toBe ( next [ 0 ] ) ;
635+ expect ( result [ 0 ] . enriched ) . toBe ( false ) ;
636+ } ) ;
637+
638+ it ( "carries forward prior enrichment for an unenriched next PR that was enriched before" , ( ) => {
639+ const previous = [
640+ makePullRequest ( {
641+ id : 1 ,
642+ enriched : true ,
643+ checkStatus : "success" ,
644+ additions : 42 ,
645+ deletions : 7 ,
646+ changedFiles : 3 ,
647+ comments : 5 ,
648+ reviewThreads : 2 ,
649+ totalReviewCount : 4 ,
650+ reviewerLogins : [ "reviewer1" ] ,
651+ assigneeLogins : [ "assignee1" ] ,
652+ headSha : "prevsha" ,
653+ } ) ,
654+ ] ;
655+ const next = [
656+ makePullRequest ( {
657+ id : 1 ,
658+ enriched : false ,
659+ state : "OPEN" ,
660+ title : "Fresh title" ,
661+ checkStatus : null ,
662+ additions : 0 ,
663+ deletions : 0 ,
664+ changedFiles : 0 ,
665+ comments : 0 ,
666+ reviewThreads : 0 ,
667+ totalReviewCount : 0 ,
668+ reviewerLogins : [ ] ,
669+ assigneeLogins : [ ] ,
670+ headSha : "" ,
671+ } ) ,
672+ ] ;
673+ const result = fallbackToPreviousEnrichment ( previous , next ) ;
674+ // Heavy fields restored from the prior cycle...
675+ expect ( result [ 0 ] . enriched ) . toBe ( true ) ;
676+ expect ( result [ 0 ] . checkStatus ) . toBe ( "success" ) ;
677+ expect ( result [ 0 ] . additions ) . toBe ( 42 ) ;
678+ expect ( result [ 0 ] . deletions ) . toBe ( 7 ) ;
679+ expect ( result [ 0 ] . changedFiles ) . toBe ( 3 ) ;
680+ expect ( result [ 0 ] . comments ) . toBe ( 5 ) ;
681+ expect ( result [ 0 ] . reviewThreads ) . toBe ( 2 ) ;
682+ expect ( result [ 0 ] . totalReviewCount ) . toBe ( 4 ) ;
683+ expect ( result [ 0 ] . reviewerLogins ) . toEqual ( [ "reviewer1" ] ) ;
684+ expect ( result [ 0 ] . assigneeLogins ) . toEqual ( [ "assignee1" ] ) ;
685+ expect ( result [ 0 ] . headSha ) . toBe ( "prevsha" ) ;
686+ // ...but fresh light fields survive.
687+ expect ( result [ 0 ] . title ) . toBe ( "Fresh title" ) ;
688+ } ) ;
689+
690+ it ( "drops a PR that closed (present in previous, absent from next)" , ( ) => {
691+ const previous = [
692+ makePullRequest ( { id : 1 , enriched : true } ) ,
693+ makePullRequest ( { id : 2 , enriched : true } ) ,
694+ ] ;
695+ const next = [ makePullRequest ( { id : 1 , enriched : false } ) ] ;
696+ const result = fallbackToPreviousEnrichment ( previous , next ) ;
697+ // Only the PR still in `next` remains — the closed PR (id 2) is not resurrected.
698+ expect ( result . map ( ( pr ) => pr . id ) ) . toEqual ( [ 1 ] ) ;
699+ } ) ;
700+ } ) ;
701+
702+ describe ( "pickEnrichmentFields" , ( ) => {
703+ it ( "returns only the heavy enrichment fields, not light fields" , ( ) => {
704+ const pr = makePullRequest ( {
705+ id : 1 ,
706+ title : "light title" ,
707+ state : "OPEN" ,
708+ checkStatus : "success" ,
709+ additions : 10 ,
710+ reviewerLogins : [ "r1" ] ,
711+ } ) ;
712+ const fields = pickEnrichmentFields ( pr ) ;
713+ expect ( fields ) . toEqual ( {
714+ headSha : pr . headSha ,
715+ assigneeLogins : pr . assigneeLogins ,
716+ reviewerLogins : pr . reviewerLogins ,
717+ checkStatus : pr . checkStatus ,
718+ additions : pr . additions ,
719+ deletions : pr . deletions ,
720+ changedFiles : pr . changedFiles ,
721+ comments : pr . comments ,
722+ reviewThreads : pr . reviewThreads ,
723+ totalReviewCount : pr . totalReviewCount ,
724+ } ) ;
725+ expect ( fields ) . not . toHaveProperty ( "title" ) ;
726+ expect ( fields ) . not . toHaveProperty ( "state" ) ;
727+ expect ( fields ) . not . toHaveProperty ( "enriched" ) ;
728+ } ) ;
729+ } ) ;
0 commit comments