Skip to content

Commit c260e4d

Browse files
capJavertclaude
andauthored
feat: add githubProfileTags and onboardingProfileTags mutations (#3775)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d848cc4 commit c260e4d

7 files changed

Lines changed: 565 additions & 6 deletions

File tree

__tests__/helpers.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@ import {
5050
ExtractMarkdownResponse,
5151
FindCompanyNewsResponse,
5252
FindContactActivityResponse,
53+
ExtractedProfileTag,
5354
FindJobVacanciesResponse,
55+
GitHubProfileTagsResponse,
5456
JobVacancy,
5557
NewsItem,
58+
OnboardingProfileTagsResponse,
5659
ParseCVResponse,
5760
ParseError,
5861
ParseOpportunityResponse,
@@ -792,6 +795,23 @@ export const createMockBragiPipelinesTransport = () =>
792795
}),
793796
],
794797
}),
798+
gitHubProfileTags: () =>
799+
new GitHubProfileTagsResponse({
800+
id: 'mock-id',
801+
extractedTags: [
802+
new ExtractedProfileTag({ name: 'webdev', confidence: 0.95 }),
803+
new ExtractedProfileTag({ name: 'rust', confidence: 0.88 }),
804+
new ExtractedProfileTag({ name: 'golang', confidence: 0.72 }),
805+
],
806+
}),
807+
onboardingProfileTags: () =>
808+
new OnboardingProfileTagsResponse({
809+
id: 'mock-id',
810+
extractedTags: [
811+
new ExtractedProfileTag({ name: 'webdev', confidence: 0.91 }),
812+
new ExtractedProfileTag({ name: 'fullstack', confidence: 0.85 }),
813+
],
814+
}),
795815
});
796816
});
797817

@@ -807,6 +827,12 @@ export const createMockBragiPipelinesNotFoundTransport = () =>
807827
findContactActivity: () => {
808828
throw new ConnectError('not found', ConnectCode.NotFound);
809829
},
830+
gitHubProfileTags: () => {
831+
throw new ConnectError('not found', ConnectCode.NotFound);
832+
},
833+
onboardingProfileTags: () => {
834+
throw new ConnectError('not found', ConnectCode.NotFound);
835+
},
810836
});
811837
});
812838

__tests__/users.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
} from 'date-fns';
1717
import {
1818
authorizeRequest,
19+
createGarmrMock,
20+
createMockBragiPipelinesTransport,
21+
createMockBragiPipelinesNotFoundTransport,
1922
createMockNjordTransport,
2023
disposeGraphQLTesting,
2124
GraphQLTestClient,
@@ -152,9 +155,14 @@ import { createClient } from '@connectrpc/connect';
152155
import {
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';
158166
import { Organization } from '../src/entity';
159167
import { Opportunity } from '../src/entity/opportunities/Opportunity';
160168
import { 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+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@connectrpc/connect-fastify": "^1.6.1",
3939
"@connectrpc/connect-node": "^1.6.1",
4040
"@dailydotdev/graphql-redis-subscriptions": "^2.4.3",
41-
"@dailydotdev/schema": "0.3.2",
41+
"@dailydotdev/schema": "0.3.3",
4242
"@dailydotdev/ts-ioredis-pool": "^1.0.2",
4343
"@fastify/cookie": "^11.0.2",
4444
"@fastify/cors": "^11.2.0",

0 commit comments

Comments
 (0)