Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/bot/__tests__/acceptReviewRequest.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { acceptReviewRequest } from '@bot/acceptReviewRequest';
import { buildMockActionParam, buildMockApp } from '@utils/slackMocks';
import { BlockId } from '@bot/enums';
import { BlockId, CandidateType } from '@bot/enums';
import { chatService } from '@/services/ChatService';
import { userRepo } from '@repos/userRepo';
import { addUserToAcceptedReviewers } from '@/services/RequestService';
Expand All @@ -20,6 +20,13 @@ describe('acceptReviewRequest', () => {
activeReviewRepo.getReviewByThreadIdOrUndefined = jest.fn();
});

const expectedCandidateTypeBlock = {
type: 'section',
text: {
type: 'mrkdwn',
text: '*Candidate Type:* Full-time',
},
};
const expectedHackerRankUrlBlock = {
type: 'section',
text: {
Expand Down Expand Up @@ -71,6 +78,7 @@ describe('acceptReviewRequest', () => {
(activeReviewRepo.getReviewByThreadIdOrUndefined as jest.Mock).mockResolvedValue({
hackerRankUrl: 'https://www.sourceallies.com',
requestorId: 'requester123',
candidateType: CandidateType.FULL_TIME,
acceptedReviewers: [],
declinedReviewers: [],
pendingReviewers: [{ userId: action.body.user.id }],
Expand Down Expand Up @@ -126,6 +134,7 @@ describe('acceptReviewRequest', () => {
);
expectUpdatedWithBlocks(
action,
expectedCandidateTypeBlock,
expectedHackerRankUrlBlock,
expectedHackerRankInstructionsBlock,
expectedHackerRankAccountHelpBlock,
Expand Down
3 changes: 2 additions & 1 deletion src/bot/__tests__/getReviewInfo.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { App } from '@slack/bolt';
import { getReviewInfo } from '@bot/getReviewInfo';
import { buildMockGlobalShortcutParam, buildMockWebClient } from '@utils/slackMocks';
import { Deadline, Interaction } from '@bot/enums';
import { CandidateType, Deadline, Interaction } from '@bot/enums';
import { GlobalShortcutParam } from '@/slackTypes';
import { activeReviewRepo } from '@repos/activeReviewsRepo';
import { ActiveReview } from '@models/ActiveReview';
Expand Down Expand Up @@ -47,6 +47,7 @@ describe('getReviewInfo', () => {
requestedAt: new Date(1650504468906),
dueBy: Deadline.END_OF_DAY,
candidateIdentifier: 'some-id',
candidateType: CandidateType.FULL_TIME,
reviewersNeededCount: 1,
acceptedReviewers: [],
declinedReviewers: [],
Expand Down
26 changes: 23 additions & 3 deletions src/bot/__tests__/requestReview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { activeReviewRepo } from '@/database/repos/activeReviewsRepo';
import { QueueService } from '@/services';
import { chatService } from '@/services/ChatService';
import { ShortcutParam } from '@/slackTypes';
import { ActionId, Deadline, Interaction } from '@bot/enums';
import { ActionId, CandidateType, Deadline, Interaction } from '@bot/enums';
import { requestReview } from '@bot/requestReview';
import { languageRepo } from '@repos/languageRepo';
import { App, SlackViewAction, ViewStateValue } from '@slack/bolt';
Expand Down Expand Up @@ -157,10 +157,17 @@ describe('requestReview', () => {
expect(blocks[2].element.initial_value).toEqual('2');
});

it('should setup the fifth response block for the HackerRank URL input', () => {
it('should setup the fifth response block for the candidate type dropdown', () => {
const { mock } = param.client.views.open as jest.Mock;
const blocks = mock.calls[0][0].view.blocks;
expect(blocks[4]).toEqual({
expect(blocks[4].block_id).toEqual(ActionId.CANDIDATE_TYPE);
expect(blocks[4].type).toEqual('input');
});

it('should setup the sixth response block for the HackerRank URL input', () => {
const { mock } = param.client.views.open as jest.Mock;
const blocks = mock.calls[0][0].view.blocks;
expect(blocks[5]).toEqual({
type: 'input',
block_id: ActionId.HACKERRANK_URL,
label: {
Expand Down Expand Up @@ -265,6 +272,15 @@ describe('requestReview', () => {
value: 'some-identifier',
},
},
[ActionId.CANDIDATE_TYPE]: {
[ActionId.CANDIDATE_TYPE]: {
type: 'static_select',
selected_option: {
text: { type: 'plain_text', text: 'Full-time' },
value: CandidateType.FULL_TIME,
},
},
},
[ActionId.HACKERRANK_URL]: {
[ActionId.HACKERRANK_URL]: {
type: 'plain_text_input',
Expand Down Expand Up @@ -331,10 +347,13 @@ describe('requestReview', () => {
• Go
• Javascript

*Candidate Type: Full-time*

*The review is needed by end of day Monday*

_Candidate Identifier: some-identifier_
`.trim(),
token: undefined,
});
});

Expand All @@ -354,6 +373,7 @@ _Candidate Identifier: some-identifier_
requestedAt: expect.any(Date),
dueBy: Deadline.MONDAY,
candidateIdentifier: 'some-identifier',
candidateType: CandidateType.FULL_TIME,
reviewersNeededCount: '1',
acceptedReviewers: [],
declinedReviewers: [],
Expand Down
5 changes: 4 additions & 1 deletion src/bot/acceptReviewRequest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ActionParam } from '@/slackTypes';
import { App } from '@slack/bolt';
import log from '@utils/log';
import { ActionId, BlockId } from './enums';
import { ActionId, BlockId, CandidateTypeLabel } from './enums';
import { userRepo } from '@repos/userRepo';
import { mention, textBlock } from '@utils/text';
import { reportErrorAndContinue } from '@utils/reportError';
Expand Down Expand Up @@ -86,6 +86,9 @@ export const acceptReviewRequest = {
// Add HackerRank URL with instructions if available
const review = await activeReviewRepo.getReviewByThreadIdOrUndefined(threadId);
if (review) {
blocks.push(
textBlock(`*Candidate Type:* ${CandidateTypeLabel.get(review.candidateType)}`),
);
blocks.push(
textBlock(`*HackerRank Report:* <${review.hackerRankUrl}|View Candidate Assessment>`),
);
Expand Down
11 changes: 11 additions & 0 deletions src/bot/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const enum ActionId {
REVIEW_DEADLINE = 'review-deadline',
NUMBER_OF_REVIEWERS = 'number-of-reviewers',
CANDIDATE_IDENTIFIER = 'candidate-identifier',
CANDIDATE_TYPE = 'candidate-type',
REVIEWER_DM_ACCEPT = 'reviewer-dm-accept',
REVIEWER_DM_DECLINE = 'reviewer-dm-deny',
HACKERRANK_URL = 'hackerrank-url',
Expand Down Expand Up @@ -46,3 +47,13 @@ export const DeadlineLabel = new Map<Deadline, string>([
[Deadline.THURSDAY, 'Thursday'],
[Deadline.FRIDAY, 'Friday'],
]);

export const enum CandidateType {
FULL_TIME = 'full-time',
APPRENTICE = 'apprentice',
}

export const CandidateTypeLabel = new Map<CandidateType, string>([
[CandidateType.FULL_TIME, 'Full-time'],
[CandidateType.APPRENTICE, 'Apprentice'],
]);
44 changes: 43 additions & 1 deletion src/bot/requestReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ import { blockUtils } from '@utils/blocks';
import log from '@utils/log';
import { bold, codeBlock, compose, italic, mention, ul } from '@utils/text';
import { PendingReviewer } from '@models/ActiveReview';
import { ActionId, Deadline, DeadlineLabel, Interaction } from './enums';
import {
ActionId,
CandidateType,
CandidateTypeLabel,
Deadline,
DeadlineLabel,
Interaction,
} from './enums';
import { chatService } from '@/services/ChatService';
import { determineExpirationTime } from '@utils/reviewExpirationUtils';

Expand Down Expand Up @@ -89,6 +96,19 @@ export const requestReview = {
},
},
},
{
type: 'input',
block_id: ActionId.CANDIDATE_TYPE,
label: {
text: 'What type of candidate is this?',
type: 'plain_text',
},
element: {
type: 'static_select',
action_id: ActionId.CANDIDATE_TYPE,
options: buildCandidateTypeOptions(),
},
},
{
type: 'input',
block_id: ActionId.HACKERRANK_URL,
Expand Down Expand Up @@ -160,19 +180,24 @@ export const requestReview = {
const deadline = blockUtils.getBlockValue(body, ActionId.REVIEW_DEADLINE);
const numberOfRequestedReviewers = blockUtils.getBlockValue(body, ActionId.NUMBER_OF_REVIEWERS);
const candidateIdentifier = blockUtils.getBlockValue(body, ActionId.CANDIDATE_IDENTIFIER);
const candidateType = blockUtils.getBlockValue(body, ActionId.CANDIDATE_TYPE);
const hackerRankUrl = blockUtils.getBlockValue(body, ActionId.HACKERRANK_URL);

const numberOfReviewersValue = numberOfRequestedReviewers.value;
const deadlineValue = deadline.selected_option.value;
const deadlineDisplay = deadline.selected_option.text.text;
const candidateIdentifierValue = candidateIdentifier.value;
const candidateTypeValue = candidateType.selected_option.value;
const candidateTypeDisplay = candidateType.selected_option.text.text;
const hackerRankUrlValue = hackerRankUrl.value;
log.d(
'requestReview.callback',
'Parsed values:',
JSON.stringify({
numberOfReviewersValue,
candidateIdentifierValue,
candidateTypeValue,
candidateTypeDisplay,
hackerRankUrlValue,
deadlineValue,
deadlineDisplay,
Expand All @@ -190,6 +215,7 @@ export const requestReview = {
user,
)} has requested ${numberOfReviewersValue} reviews for a HackerRank done in the following languages:`,
ul(...languages),
bold(`Candidate Type: ${candidateTypeDisplay}`),
bold(`The review is needed by end of day ${deadlineDisplay}`),
candidateIdentifierValue ? italic(`Candidate Identifier: ${candidateIdentifierValue}`) : '',
),
Expand Down Expand Up @@ -226,6 +252,7 @@ export const requestReview = {
{ id: user.id },
languages,
deadlineDisplay,
candidateTypeDisplay,
);
const pendingReviewer: PendingReviewer = {
userId: reviewer.id,
Expand All @@ -242,6 +269,7 @@ export const requestReview = {
requestedAt: new Date(),
dueBy: deadlineValue,
candidateIdentifier: candidateIdentifierValue,
candidateType: candidateTypeValue,
reviewersNeededCount: numberOfReviewersValue,
acceptedReviewers: [],
declinedReviewers: [],
Expand All @@ -265,3 +293,17 @@ function buildDeadlineOptions(): PlainTextOption[] {
function buildOption(deadline: Deadline): PlainTextOption {
return { text: { text: DeadlineLabel.get(deadline) || '', type: 'plain_text' }, value: deadline };
}

function buildCandidateTypeOptions(): PlainTextOption[] {
return [
buildCandidateTypeOption(CandidateType.FULL_TIME),
buildCandidateTypeOption(CandidateType.APPRENTICE),
];
}

function buildCandidateTypeOption(candidateType: CandidateType): PlainTextOption {
return {
text: { text: CandidateTypeLabel.get(candidateType) || '', type: 'plain_text' },
value: candidateType,
};
}
3 changes: 2 additions & 1 deletion src/cron/__tests__/reviewProcessor.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Deadline } from '@/bot/enums';
import { CandidateType, Deadline } from '@/bot/enums';
import { ActiveReview, PendingReviewer } from '@/database/models/ActiveReview';
import { activeReviewRepo } from '@/database/repos/activeReviewsRepo';
import { RequestService } from '@/services';
Expand All @@ -15,6 +15,7 @@ function mockReview(pendingReviewers: PendingReviewer[]): ActiveReview {
acceptedReviewers: [],
dueBy: Deadline.MONDAY,
candidateIdentifier: '',
candidateType: CandidateType.FULL_TIME,
languages: [],
pendingReviewers,
declinedReviewers: [],
Expand Down
6 changes: 5 additions & 1 deletion src/database/models/ActiveReview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Deadline } from '@bot/enums';
import { CandidateType, Deadline } from '@bot/enums';

export interface ActiveReview {
threadId: string;
Expand All @@ -7,6 +7,10 @@ export interface ActiveReview {
requestedAt: Date;
dueBy: Deadline;
candidateIdentifier: string;
/**
* The type of candidate (full-time or apprentice)
*/
candidateType: CandidateType;
/**
* The number of reviewers requested for this review. It should not change over the life of the
* review
Expand Down
3 changes: 3 additions & 0 deletions src/database/repos/activeReviewsRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum Column {
REQUESTED_AT = 'requestedAt',
DUE_BY = 'dueBy',
CANDIDATE_IDENTIFIER = 'candidateIdentifier',
CANDIDATE_TYPE = 'candidateType',
REVIEWERS_NEEDED_COUNT = 'reviewersNeededCount',
ACCEPTED_REVIEWERS = 'acceptedReviewers',
PENDING_REVIEWERS = 'pendingReviewers',
Expand All @@ -34,6 +35,7 @@ function mapRowToActiveReview(row: GoogleSpreadsheetRow): ActiveReview {
requestedAt: parseDateRow(row.get(Column.REQUESTED_AT)),
dueBy: row.get(Column.DUE_BY),
candidateIdentifier: row.get(Column.CANDIDATE_IDENTIFIER),
candidateType: row.get(Column.CANDIDATE_TYPE),
reviewersNeededCount: Number(row.get(Column.REVIEWERS_NEEDED_COUNT)),
acceptedReviewers: JSON.parse(row.get(Column.ACCEPTED_REVIEWERS)),
pendingReviewers: JSON.parse(row.get(Column.PENDING_REVIEWERS)),
Expand All @@ -50,6 +52,7 @@ function mapActiveReviewToRow(activeReview: ActiveReview): Record<string, any> {
[Column.REQUESTED_AT]: activeReview.requestedAt.getTime(),
[Column.DUE_BY]: activeReview.dueBy,
[Column.CANDIDATE_IDENTIFIER]: activeReview.candidateIdentifier,
[Column.CANDIDATE_TYPE]: activeReview.candidateType,
[Column.REVIEWERS_NEEDED_COUNT]: activeReview.reviewersNeededCount,
[Column.ACCEPTED_REVIEWERS]: JSON.stringify(activeReview.acceptedReviewers),
[Column.PENDING_REVIEWERS]: JSON.stringify(activeReview.pendingReviewers),
Expand Down
2 changes: 2 additions & 0 deletions src/services/ChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,15 @@ export const chatService = {
requestor: { id: string },
languages: string[],
deadlineDisplay: string,
candidateTypeDisplay: string,
): Promise<string> {
const request = requestBuilder.buildReviewRequest(
reviewerId,
threadId,
requestor,
languages,
deadlineDisplay,
candidateTypeDisplay,
);
const requestWithToken = {
...request,
Expand Down
4 changes: 3 additions & 1 deletion src/services/RequestService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { activeReviewRepo } from '@/database/repos/activeReviewsRepo';
import { WebClient } from '@/slackTypes';
import { QueueService } from '@services';
import { chatService } from '@/services/ChatService';
import { DeadlineLabel } from '@bot/enums';
import { CandidateTypeLabel, DeadlineLabel } from '@bot/enums';
import { requestBuilder } from '@utils/RequestBuilder';
import { textBlock } from '@utils/text';
import { App } from '@slack/bolt';
Expand Down Expand Up @@ -64,6 +64,7 @@ const closeRequestInternal = async (
{ id: updatedReview.requestorId },
updatedReview.languages,
DeadlineLabel.get(updatedReview.dueBy) || 'Unknown',
CandidateTypeLabel.get(updatedReview.candidateType) || 'Unknown',
);
const closeMessageBlock = textBlock(closeMessage);
await chatService.updateDirectMessage(
Expand Down Expand Up @@ -107,6 +108,7 @@ async function requestNextUserReview(review: ActiveReview, _client: WebClient):
{ id: review.requestorId },
review.languages,
DeadlineLabel.get(review.dueBy) || '',
CandidateTypeLabel.get(review.candidateType) || 'Unknown',
);
const pendingReviewer: PendingReviewer = {
...nextUser,
Expand Down
Loading
Loading