@@ -16,6 +16,9 @@ import {
1616} from 'date-fns' ;
1717import {
1818 authorizeRequest ,
19+ createGarmrMock ,
20+ createMockBragiPipelinesTransport ,
21+ createMockBragiPipelinesNotFoundTransport ,
1922 createMockNjordTransport ,
2023 disposeGraphQLTesting ,
2124 GraphQLTestClient ,
@@ -152,9 +155,14 @@ import { createClient } from '@connectrpc/connect';
152155import {
153156 Credits ,
154157 EntityType ,
158+ ExtractedProfileTag ,
159+ OnboardingProfileTagsResponse ,
155160 OpportunityState ,
156161 OpportunityType ,
162+ Pipelines ,
157163} from '@dailydotdev/schema' ;
164+ import * as bragiClients from '../src/integrations/bragi/clients' ;
165+ import type { ServiceClient } from '../src/types' ;
158166import { Organization } from '../src/entity' ;
159167import { Opportunity } from '../src/entity/opportunities/Opportunity' ;
160168import { OpportunityJob } from '../src/entity/opportunities/OpportunityJob' ;
@@ -8280,3 +8288,295 @@ describe('mutation setPassword', () => {
82808288 expect ( res . errors [ 0 ] . message ) . toBe ( 'Password too weak' ) ;
82818289 } ) ;
82828290} ) ;
8291+
8292+ describe ( 'githubProfileTags mutation' , ( ) => {
8293+ const GITHUB_PROFILE_TAGS_MUTATION = `
8294+ mutation {
8295+ githubProfileTags {
8296+ includeTags
8297+ }
8298+ }
8299+ ` ;
8300+
8301+ const seedGitHubAccount = async ( userId : string ) => {
8302+ await con . query (
8303+ `INSERT INTO ba_account (id, "accountId", "providerId", "userId", "accessToken", scope, "createdAt", "updatedAt")
8304+ VALUES ($1, $2, 'github', $3, $4, 'user:email', NOW(), NOW())` ,
8305+ [ `ba-${ userId } ` , `gh-${ userId } ` , userId , 'gho_test_token_123' ] ,
8306+ ) ;
8307+ } ;
8308+
8309+ beforeEach ( async ( ) => {
8310+ await deleteKeysByPattern ( `${ rateLimiterName } :*` ) ;
8311+ await saveFixtures ( con , Keyword , keywordsFixture ) ;
8312+ await con . getRepository ( Feed ) . save ( { id : '1' , userId : '1' } ) ;
8313+
8314+ jest . spyOn ( bragiClients , 'getBragiClient' ) . mockImplementation (
8315+ ( ) : ServiceClient < typeof Pipelines > => ( {
8316+ instance : createClient ( Pipelines , createMockBragiPipelinesTransport ( ) ) ,
8317+ garmr : createGarmrMock ( ) ,
8318+ } ) ,
8319+ ) ;
8320+ } ) ;
8321+
8322+ afterEach ( ( ) => {
8323+ jest . restoreAllMocks ( ) ;
8324+ } ) ;
8325+
8326+ it ( 'should return extracted tags for user with GitHub account' , async ( ) => {
8327+ loggedUser = '1' ;
8328+ await seedGitHubAccount ( '1' ) ;
8329+
8330+ const res = await client . mutate ( GITHUB_PROFILE_TAGS_MUTATION ) ;
8331+
8332+ expect ( res . errors ) . toBeFalsy ( ) ;
8333+ expect ( res . data . githubProfileTags . includeTags ) . toEqual (
8334+ expect . arrayContaining ( [ 'webdev' , 'rust' , 'golang' ] ) ,
8335+ ) ;
8336+ } ) ;
8337+
8338+ it ( 'should return error when no GitHub account linked' , async ( ) => {
8339+ loggedUser = '1' ;
8340+
8341+ return testMutationErrorCode (
8342+ client ,
8343+ { mutation : GITHUB_PROFILE_TAGS_MUTATION } ,
8344+ 'NOT_FOUND' ,
8345+ 'No GitHub account linked' ,
8346+ ) ;
8347+ } ) ;
8348+
8349+ it ( 'should require authentication' , async ( ) => {
8350+ loggedUser = null ;
8351+
8352+ return testMutationErrorCode (
8353+ client ,
8354+ { mutation : GITHUB_PROFILE_TAGS_MUTATION } ,
8355+ 'UNAUTHENTICATED' ,
8356+ ) ;
8357+ } ) ;
8358+
8359+ it ( 'should propagate bragi NotFound error' , async ( ) => {
8360+ loggedUser = '1' ;
8361+ await seedGitHubAccount ( '1' ) ;
8362+
8363+ jest . restoreAllMocks ( ) ;
8364+ jest . spyOn ( bragiClients , 'getBragiClient' ) . mockImplementation (
8365+ ( ) : ServiceClient < typeof Pipelines > => ( {
8366+ instance : createClient (
8367+ Pipelines ,
8368+ createMockBragiPipelinesNotFoundTransport ( ) ,
8369+ ) ,
8370+ garmr : createGarmrMock ( ) ,
8371+ } ) ,
8372+ ) ;
8373+
8374+ return testMutationErrorCode (
8375+ client ,
8376+ { mutation : GITHUB_PROFILE_TAGS_MUTATION } ,
8377+ 'NOT_FOUND' ,
8378+ 'GitHub profile tags not found' ,
8379+ ) ;
8380+ } ) ;
8381+
8382+ it ( 'should return saved tags on repeated call without calling bragi' , async ( ) => {
8383+ loggedUser = '1' ;
8384+ await seedGitHubAccount ( '1' ) ;
8385+
8386+ const first = await client . mutate ( GITHUB_PROFILE_TAGS_MUTATION ) ;
8387+ expect ( first . errors ) . toBeFalsy ( ) ;
8388+
8389+ const spy = jest . fn ( ) ;
8390+ jest . restoreAllMocks ( ) ;
8391+ jest . spyOn ( bragiClients , 'getBragiClient' ) . mockImplementation (
8392+ ( ) : ServiceClient < typeof Pipelines > => ( {
8393+ instance : {
8394+ gitHubProfileTags : spy ,
8395+ } as unknown as ReturnType < typeof createClient < typeof Pipelines > > ,
8396+ garmr : createGarmrMock ( ) ,
8397+ } ) ,
8398+ ) ;
8399+
8400+ const second = await client . mutate ( GITHUB_PROFILE_TAGS_MUTATION ) ;
8401+ expect ( second . errors ) . toBeFalsy ( ) ;
8402+ expect ( second . data . githubProfileTags . includeTags ) . toEqual (
8403+ first . data . githubProfileTags . includeTags ,
8404+ ) ;
8405+ expect ( spy ) . not . toHaveBeenCalled ( ) ;
8406+ } ) ;
8407+ } ) ;
8408+
8409+ describe ( 'onboardingProfileTags mutation' , ( ) => {
8410+ const ONBOARDING_PROFILE_TAGS_MUTATION = `
8411+ mutation OnboardingProfileTags($prompt: String!) {
8412+ onboardingProfileTags(prompt: $prompt) {
8413+ includeTags
8414+ }
8415+ }
8416+ ` ;
8417+
8418+ beforeEach ( async ( ) => {
8419+ await deleteKeysByPattern ( `${ rateLimiterName } :*` ) ;
8420+ await saveFixtures ( con , Keyword , keywordsFixture ) ;
8421+ await con . getRepository ( Feed ) . save ( { id : '1' , userId : '1' } ) ;
8422+
8423+ jest . spyOn ( bragiClients , 'getBragiClient' ) . mockImplementation (
8424+ ( ) : ServiceClient < typeof Pipelines > => ( {
8425+ instance : createClient ( Pipelines , createMockBragiPipelinesTransport ( ) ) ,
8426+ garmr : createGarmrMock ( ) ,
8427+ } ) ,
8428+ ) ;
8429+ } ) ;
8430+
8431+ afterEach ( ( ) => {
8432+ jest . restoreAllMocks ( ) ;
8433+ } ) ;
8434+
8435+ it ( 'should return extracted tags from prompt' , async ( ) => {
8436+ loggedUser = '1' ;
8437+
8438+ const res = await client . mutate ( ONBOARDING_PROFILE_TAGS_MUTATION , {
8439+ variables : { prompt : 'I love Python and machine learning' } ,
8440+ } ) ;
8441+
8442+ expect ( res . errors ) . toBeFalsy ( ) ;
8443+ expect ( res . data . onboardingProfileTags . includeTags ) . toEqual (
8444+ expect . arrayContaining ( [ 'webdev' , 'fullstack' ] ) ,
8445+ ) ;
8446+ } ) ;
8447+
8448+ it ( 'should return error for empty prompt' , async ( ) => {
8449+ loggedUser = '1' ;
8450+
8451+ return testMutationErrorCode (
8452+ client ,
8453+ {
8454+ mutation : ONBOARDING_PROFILE_TAGS_MUTATION ,
8455+ variables : { prompt : '' } ,
8456+ } ,
8457+ 'ZOD_VALIDATION_ERROR' ,
8458+ ) ;
8459+ } ) ;
8460+
8461+ it ( 'should require authentication' , async ( ) => {
8462+ loggedUser = null ;
8463+
8464+ return testMutationErrorCode (
8465+ client ,
8466+ {
8467+ mutation : ONBOARDING_PROFILE_TAGS_MUTATION ,
8468+ variables : { prompt : 'I love coding' } ,
8469+ } ,
8470+ 'UNAUTHENTICATED' ,
8471+ ) ;
8472+ } ) ;
8473+
8474+ it ( 'should return saved tags on repeated call without calling bragi' , async ( ) => {
8475+ loggedUser = '1' ;
8476+
8477+ const first = await client . mutate ( ONBOARDING_PROFILE_TAGS_MUTATION , {
8478+ variables : { prompt : 'I love Python and machine learning' } ,
8479+ } ) ;
8480+ expect ( first . errors ) . toBeFalsy ( ) ;
8481+
8482+ const spy = jest . fn ( ) ;
8483+ jest . restoreAllMocks ( ) ;
8484+ jest . spyOn ( bragiClients , 'getBragiClient' ) . mockImplementation (
8485+ ( ) : ServiceClient < typeof Pipelines > => ( {
8486+ instance : {
8487+ onboardingProfileTags : spy ,
8488+ } as unknown as ReturnType < typeof createClient < typeof Pipelines > > ,
8489+ garmr : createGarmrMock ( ) ,
8490+ } ) ,
8491+ ) ;
8492+
8493+ const second = await client . mutate ( ONBOARDING_PROFILE_TAGS_MUTATION , {
8494+ variables : { prompt : 'different prompt' } ,
8495+ } ) ;
8496+ expect ( second . errors ) . toBeFalsy ( ) ;
8497+ expect ( second . data . onboardingProfileTags . includeTags ) . toEqual (
8498+ first . data . onboardingProfileTags . includeTags ,
8499+ ) ;
8500+ expect ( spy ) . not . toHaveBeenCalled ( ) ;
8501+ } ) ;
8502+
8503+ it ( 'should pass tag vocabulary to bragi' , async ( ) => {
8504+ loggedUser = '1' ;
8505+
8506+ jest . restoreAllMocks ( ) ;
8507+ await deleteKeysByPattern ( `${ rateLimiterName } :*` ) ;
8508+
8509+ const bragiSpy = jest . fn ( ) . mockResolvedValue (
8510+ new OnboardingProfileTagsResponse ( {
8511+ id : 'mock-id' ,
8512+ extractedTags : [
8513+ new ExtractedProfileTag ( { name : 'webdev' , confidence : 0.9 } ) ,
8514+ new ExtractedProfileTag ( { name : 'rust' , confidence : 0.7 } ) ,
8515+ ] ,
8516+ } ) ,
8517+ ) ;
8518+
8519+ jest . spyOn ( bragiClients , 'getBragiClient' ) . mockImplementation (
8520+ ( ) : ServiceClient < typeof Pipelines > => ( {
8521+ instance : {
8522+ onboardingProfileTags : bragiSpy ,
8523+ } as unknown as ReturnType < typeof createClient < typeof Pipelines > > ,
8524+ garmr : createGarmrMock ( ) ,
8525+ } ) ,
8526+ ) ;
8527+
8528+ const res = await client . mutate ( ONBOARDING_PROFILE_TAGS_MUTATION , {
8529+ variables : { prompt : 'I like web development and rust' } ,
8530+ } ) ;
8531+
8532+ expect ( res . errors ) . toBeFalsy ( ) ;
8533+ expect ( res . data . onboardingProfileTags . includeTags ) . toEqual (
8534+ expect . arrayContaining ( [ 'webdev' , 'rust' ] ) ,
8535+ ) ;
8536+ expect ( bragiSpy ) . toHaveBeenCalledWith (
8537+ expect . objectContaining ( {
8538+ onboardingPrompt : 'I like web development and rust' ,
8539+ tagVocabulary : expect . arrayContaining ( [ 'webdev' , 'rust' , 'golang' ] ) ,
8540+ } ) ,
8541+ ) ;
8542+ } ) ;
8543+
8544+ it ( 'should propagate bragi NotFound error' , async ( ) => {
8545+ loggedUser = '1' ;
8546+
8547+ jest . restoreAllMocks ( ) ;
8548+ await deleteKeysByPattern ( `${ rateLimiterName } :*` ) ;
8549+ jest . spyOn ( bragiClients , 'getBragiClient' ) . mockImplementation (
8550+ ( ) : ServiceClient < typeof Pipelines > => ( {
8551+ instance : createClient (
8552+ Pipelines ,
8553+ createMockBragiPipelinesNotFoundTransport ( ) ,
8554+ ) ,
8555+ garmr : createGarmrMock ( ) ,
8556+ } ) ,
8557+ ) ;
8558+
8559+ return testMutationErrorCode (
8560+ client ,
8561+ {
8562+ mutation : ONBOARDING_PROFILE_TAGS_MUTATION ,
8563+ variables : { prompt : 'I love coding' } ,
8564+ } ,
8565+ 'NOT_FOUND' ,
8566+ 'Onboarding profile tags not found' ,
8567+ ) ;
8568+ } ) ;
8569+
8570+ it ( 'should return error for prompt exceeding max length' , async ( ) => {
8571+ loggedUser = '1' ;
8572+
8573+ return testMutationErrorCode (
8574+ client ,
8575+ {
8576+ mutation : ONBOARDING_PROFILE_TAGS_MUTATION ,
8577+ variables : { prompt : 'a' . repeat ( 2001 ) } ,
8578+ } ,
8579+ 'ZOD_VALIDATION_ERROR' ,
8580+ ) ;
8581+ } ) ;
8582+ } ) ;
0 commit comments