@@ -354,6 +354,47 @@ describe("discoverUpstreamRepos", () => {
354354 expect ( result ) . toHaveLength ( 1 ) ;
355355 expect ( result [ 0 ] ) . toEqual ( { owner : "my-org" , name : "my-repo" , fullName : "my-org/my-repo" } ) ;
356356 } ) ;
357+
358+ it ( "discovers repos from tracked users in addition to primary user" , async ( ) => {
359+ const octokit = makeOctokit (
360+ async ( ) => ( { } ) ,
361+ async ( _query : string , vars : unknown ) => {
362+ const v = vars as { q : string } ;
363+ if ( v . q . includes ( "involves:primary" ) && v . q . includes ( "is:issue" ) ) {
364+ return makeSearchPage ( [ "org/primary-repo" ] ) ;
365+ }
366+ if ( v . q . includes ( "involves:tracked1" ) && v . q . includes ( "is:issue" ) ) {
367+ return makeSearchPage ( [ "org/tracked1-repo" ] ) ;
368+ }
369+ return makeSearchPage ( [ ] ) ;
370+ }
371+ ) ;
372+
373+ const trackedUsers = [ makeTrackedUser ( "tracked1" ) ] ;
374+ const result = await discoverUpstreamRepos ( octokit as never , "primary" , new Set ( ) , trackedUsers ) ;
375+ const names = result . map ( ( r ) => r . fullName ) ;
376+ expect ( names ) . toContain ( "org/primary-repo" ) ;
377+ expect ( names ) . toContain ( "org/tracked1-repo" ) ;
378+ } ) ;
379+
380+ it ( "deduplicates repos found by both primary and tracked users" , async ( ) => {
381+ const octokit = makeOctokit (
382+ async ( ) => ( { } ) ,
383+ async ( _query : string , vars : unknown ) => {
384+ const v = vars as { q : string } ;
385+ if ( v . q . includes ( "is:issue" ) ) {
386+ // Both users discover the same repo
387+ return makeSearchPage ( [ "org/shared-repo" ] ) ;
388+ }
389+ return makeSearchPage ( [ ] ) ;
390+ }
391+ ) ;
392+
393+ const trackedUsers = [ makeTrackedUser ( "tracked1" ) ] ;
394+ const result = await discoverUpstreamRepos ( octokit as never , "primary" , new Set ( ) , trackedUsers ) ;
395+ expect ( result ) . toHaveLength ( 1 ) ;
396+ expect ( result [ 0 ] . fullName ) . toBe ( "org/shared-repo" ) ;
397+ } ) ;
357398} ) ;
358399
359400// ── multi-user search (fetchIssuesAndPullRequests with trackedUsers) ───────────
@@ -471,16 +512,14 @@ describe("multi-user search", () => {
471512 async ( ) => ( { } ) ,
472513 async ( _query : string , vars : unknown ) => {
473514 const v = vars as Record < string , unknown > ;
474- // Heavy backfill query has 'ids' variable
475515 if ( "ids" in v ) return makeBackfillResponse ( [ ] ) ;
476- // Light combined query: distinguish by issueQ content
516+ // Both main and tracked user searches are now repo-scoped;
517+ // distinguish by the involves: login in the query
477518 const issueQ = v [ "issueQ" ] as string | undefined ;
478- if ( issueQ ?. includes ( "repo:" ) ) {
479- // Main user search (scoped to repos)
519+ if ( issueQ ?. includes ( "involves:mainuser" ) || issueQ ?. includes ( "involves:trackeduser" ) ) {
480520 return makeLightCombinedResponse ( [ { databaseId : sharedIssueId , repoFullName : "org/repo" } ] ) ;
481521 }
482- // Tracked user search (unscoped)
483- return makeLightCombinedResponse ( [ { databaseId : sharedIssueId , repoFullName : "org/repo" } ] ) ;
522+ return makeLightCombinedResponse ( [ ] ) ;
484523 }
485524 ) ;
486525
@@ -506,14 +545,17 @@ describe("multi-user search", () => {
506545 const v = vars as Record < string , unknown > ;
507546 if ( "ids" in v ) return makeBackfillResponse ( [ ] ) ;
508547 const issueQ = v [ "issueQ" ] as string | undefined ;
509- if ( issueQ ?. includes ( "repo: " ) ) {
548+ if ( issueQ ?. includes ( "involves:mainuser " ) ) {
510549 return makeLightCombinedResponse ( [ { databaseId : mainIssueId , repoFullName : "org/repo" } ] ) ;
511550 }
512- // Tracked user has both the shared one and a unique one
513- return makeLightCombinedResponse ( [
514- { databaseId : mainIssueId , repoFullName : "org/repo" } ,
515- { databaseId : trackedOnlyIssueId , repoFullName : "org/other-repo" } ,
516- ] ) ;
551+ if ( issueQ ?. includes ( "involves:trackeduser" ) ) {
552+ // Tracked user has both the shared one and a unique one
553+ return makeLightCombinedResponse ( [
554+ { databaseId : mainIssueId , repoFullName : "org/repo" } ,
555+ { databaseId : trackedOnlyIssueId , repoFullName : "org/repo" } ,
556+ ] ) ;
557+ }
558+ return makeLightCombinedResponse ( [ ] ) ;
517559 }
518560 ) ;
519561
@@ -541,8 +583,7 @@ describe("multi-user search", () => {
541583 const v = vars as Record < string , unknown > ;
542584 if ( "ids" in v ) return makeBackfillResponse ( [ ] ) ;
543585 const issueQ = v [ "issueQ" ] as string | undefined ;
544- if ( issueQ ?. includes ( "repo:" ) ) {
545- // Main user: only shared item
586+ if ( issueQ ?. includes ( "involves:mainuser" ) ) {
546587 return makeLightCombinedResponse ( [ { databaseId : sharedId , repoFullName : "org/repo" } ] ) ;
547588 }
548589 if ( issueQ ?. includes ( "involves:usera" ) ) {
@@ -609,7 +650,7 @@ describe("multi-user search", () => {
609650 const v = vars as Record < string , unknown > ;
610651 if ( "ids" in v ) return makeBackfillResponse ( [ ] ) ;
611652 const issueQ = v [ "issueQ" ] as string | undefined ;
612- if ( issueQ ?. includes ( "repo: " ) ) {
653+ if ( issueQ ?. includes ( "involves:mainuser " ) ) {
613654 return makeLightCombinedResponse ( [ { databaseId : 1001 , repoFullName : "org/repo" } ] ) ;
614655 }
615656 // Tracked user search fails
@@ -626,7 +667,6 @@ describe("multi-user search", () => {
626667 expect ( result . issues ) . toHaveLength ( 1 ) ;
627668 expect ( result . issues [ 0 ] . id ) . toBe ( 1001 ) ;
628669 expect ( result . issues [ 0 ] . surfacedBy ) . toEqual ( [ "mainuser" ] ) ;
629- // Error is captured (graphqlGlobalUserSearch uses "global-search:" prefix internally)
630670 expect ( result . errors . length ) . toBeGreaterThan ( 0 ) ;
631671 expect ( callCount ) . toBeGreaterThan ( 0 ) ;
632672 } ) ;
@@ -641,16 +681,13 @@ describe("multi-user search", () => {
641681 async ( _query : string , vars : unknown ) => {
642682 const v = vars as Record < string , unknown > ;
643683 if ( "ids" in v ) {
644- // Heavy backfill: return enrichment for the PR
645684 return makeBackfillResponse ( [ prId ] ) ;
646685 }
647686 const issueQ = v [ "issueQ" ] as string | undefined ;
648- if ( issueQ ?. includes ( "repo:" ) ) {
649- // Main user light search: includes a PR
687+ if ( issueQ ?. includes ( "involves:mainuser" ) || issueQ ?. includes ( "involves:trackeduser" ) ) {
650688 return makeLightCombinedResponse ( [ ] , [ { databaseId : prId , nodeId : prNodeId , repoFullName : "org/repo" } ] ) ;
651689 }
652- // Tracked user search: same PR
653- return makeLightCombinedResponse ( [ ] , [ { databaseId : prId , nodeId : prNodeId , repoFullName : "org/repo" } ] ) ;
690+ return makeLightCombinedResponse ( [ ] ) ;
654691 }
655692 ) ;
656693
@@ -660,42 +697,30 @@ describe("multi-user search", () => {
660697
661698 expect ( result . pullRequests ) . toHaveLength ( 1 ) ;
662699 const pr = result . pullRequests [ 0 ] ;
663- // surfacedBy survives enrichment (spread preserves it since PREnrichmentData lacks surfacedBy)
664700 expect ( pr . surfacedBy ) . toContain ( "mainuser" ) ;
665701 expect ( pr . surfacedBy ) . toContain ( "trackeduser" ) ;
666- // And it's fully enriched
667702 expect ( pr . enriched ) . toBe ( true ) ;
668703 expect ( pr . checkStatus ) . toBe ( "success" ) ;
669704 } ) ;
670705
671- it ( "empty repos with tracked users still runs tracked user searches" , async ( ) => {
672- const trackedIssueId = 9001 ;
673-
706+ it ( "empty repos returns empty results even with tracked users" , async ( ) => {
674707 const octokit = makeOctokit (
675- async ( ) => ( { } ) ,
676- async ( _query : string , vars : unknown ) => {
677- const v = vars as Record < string , unknown > ;
678- if ( "ids" in v ) return makeBackfillResponse ( [ ] ) ;
679- // Only tracked user search should run (no repo: qualifier)
680- const issueQ = v [ "issueQ" ] as string | undefined ;
681- if ( issueQ && ! issueQ . includes ( "repo:" ) ) {
682- return makeLightCombinedResponse ( [ { databaseId : trackedIssueId , repoFullName : "org/tracked-repo" } ] ) ;
683- }
684- return makeLightCombinedResponse ( [ ] ) ;
685- }
708+ async ( ) => { throw new Error ( "should not be called" ) ; } ,
709+ async ( ) => { throw new Error ( "should not be called" ) ; }
686710 ) ;
687711
688712 const result = await fetchIssuesAndPullRequests (
689713 octokit as never ,
690- [ ] , // empty repos — skip main user search
714+ [ ] , // empty repos — tracked user searches are also repo-scoped
691715 "mainuser" ,
692716 undefined ,
693717 [ makeTrackedUser ( "trackeduser" ) ]
694718 ) ;
695719
696- expect ( result . issues ) . toHaveLength ( 1 ) ;
697- expect ( result . issues [ 0 ] . id ) . toBe ( trackedIssueId ) ;
698- expect ( result . issues [ 0 ] . surfacedBy ) . toEqual ( [ "trackeduser" ] ) ;
720+ expect ( result . issues ) . toEqual ( [ ] ) ;
721+ expect ( result . pullRequests ) . toEqual ( [ ] ) ;
722+ expect ( result . errors ) . toEqual ( [ ] ) ;
723+ expect ( octokit . graphql ) . not . toHaveBeenCalled ( ) ;
699724 } ) ;
700725
701726 it ( "surfacedBy logins are always lowercase" , async ( ) => {
0 commit comments