@@ -26,10 +26,14 @@ import { RequestError } from '@octokit/request-error'
2626import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
2727
2828import {
29+ cacheFetch ,
30+ getOctokit ,
31+ getOctokitGraphql ,
2932 handleGitHubApiError ,
3033 handleGraphqlError ,
3134 isGraphqlRateLimitError ,
3235 withGitHubRetry ,
36+ writeCache ,
3337} from '../../../../src/utils/git/github.mts'
3438
3539// Mock debug utilities to suppress output during tests.
@@ -510,3 +514,98 @@ describe('withGitHubRetry', () => {
510514 expect ( operation ) . toHaveBeenCalledTimes ( 2 )
511515 } )
512516} )
517+
518+ describe ( 'getOctokit' , ( ) => {
519+ it ( 'returns an Octokit instance' , ( ) => {
520+ const octokit = getOctokit ( )
521+ expect ( octokit ) . toBeDefined ( )
522+ expect ( octokit . pulls ) . toBeDefined ( )
523+ expect ( octokit . repos ) . toBeDefined ( )
524+ } )
525+
526+ it ( 'returns the same instance on subsequent calls' , ( ) => {
527+ const octokit1 = getOctokit ( )
528+ const octokit2 = getOctokit ( )
529+ expect ( octokit1 ) . toBe ( octokit2 )
530+ } )
531+ } )
532+
533+ describe ( 'getOctokitGraphql' , ( ) => {
534+ it ( 'returns a GraphQL client' , ( ) => {
535+ const graphql = getOctokitGraphql ( )
536+ expect ( graphql ) . toBeDefined ( )
537+ expect ( typeof graphql ) . toBe ( 'function' )
538+ } )
539+
540+ it ( 'returns the same instance on subsequent calls' , ( ) => {
541+ const graphql1 = getOctokitGraphql ( )
542+ const graphql2 = getOctokitGraphql ( )
543+ expect ( graphql1 ) . toBe ( graphql2 )
544+ } )
545+ } )
546+
547+ describe ( 'cacheFetch' , ( ) => {
548+ afterEach ( ( ) => {
549+ vi . useRealTimers ( )
550+ } )
551+
552+ it ( 'calls the fetcher when cache is empty' , async ( ) => {
553+ const fetcher = vi . fn ( ) . mockResolvedValue ( { value : 'fetched' } )
554+ // Use unique key to avoid cache from other tests.
555+ const key = `test-fetch-${ Date . now ( ) } -${ Math . random ( ) } `
556+
557+ const result = await cacheFetch ( key , fetcher )
558+
559+ expect ( result ) . toEqual ( { value : 'fetched' } )
560+ expect ( fetcher ) . toHaveBeenCalledTimes ( 1 )
561+ } )
562+
563+ it ( 'returns cached value on subsequent calls' , async ( ) => {
564+ const key = `test-cached-${ Date . now ( ) } -${ Math . random ( ) } `
565+ const fetcher = vi . fn ( ) . mockResolvedValue ( { value : 'cached' } )
566+
567+ // First call populates cache.
568+ await cacheFetch ( key , fetcher )
569+
570+ // Second call should use cache.
571+ const result = await cacheFetch ( key , fetcher )
572+
573+ expect ( result ) . toEqual ( { value : 'cached' } )
574+ // Fetcher should only be called once.
575+ expect ( fetcher ) . toHaveBeenCalledTimes ( 1 )
576+ } )
577+
578+ it ( 'prevents concurrent fetches for the same key' , async ( ) => {
579+ const key = `test-concurrent-${ Date . now ( ) } -${ Math . random ( ) } `
580+ let resolvePromise : ( value : { value : string } ) => void
581+ const slowFetcher = vi . fn ( ) . mockReturnValue (
582+ new Promise ( resolve => {
583+ resolvePromise = resolve
584+ } ) ,
585+ )
586+
587+ // Start two concurrent fetches.
588+ const promise1 = cacheFetch ( key , slowFetcher )
589+ const promise2 = cacheFetch ( key , slowFetcher )
590+
591+ // Resolve the slow fetcher.
592+ resolvePromise ! ( { value : 'slow-result' } )
593+
594+ const [ result1 , result2 ] = await Promise . all ( [ promise1 , promise2 ] )
595+
596+ expect ( result1 ) . toEqual ( { value : 'slow-result' } )
597+ expect ( result2 ) . toEqual ( { value : 'slow-result' } )
598+ // Fetcher should only be called once.
599+ expect ( slowFetcher ) . toHaveBeenCalledTimes ( 1 )
600+ } )
601+ } )
602+
603+ describe ( 'writeCache' , ( ) => {
604+ it ( 'writes cache data without throwing' , async ( ) => {
605+ const key = `test-write-${ Date . now ( ) } -${ Math . random ( ) } `
606+ const data = { test : 'data' , value : 123 }
607+
608+ // Should not throw.
609+ await expect ( writeCache ( key , data ) ) . resolves . not . toThrow ( )
610+ } )
611+ } )
0 commit comments