Skip to content

Commit 4211431

Browse files
authored
Fix shortcut callback ID, enrich acceptance DM, list all confirmed sl… (#540)
* Fix shortcut callback ID, enrich acceptance DM, list all confirmed slots in channel message * Move queue position update to session close, fix acceptance DM wording * Remove dead findConfirmedSlot, decline teammate on zero-slot submit
1 parent c28726f commit 4211431

5 files changed

Lines changed: 114 additions & 26 deletions

File tree

src/bot/__tests__/acceptPairingSlot.test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('acceptPairingSlot', () => {
105105
);
106106
});
107107

108-
it('should mark the user as last reviewed', async () => {
108+
it('should not mark the user as last reviewed on submit — only on confirmed close', async () => {
109109
const param = buildMockActionParam();
110110
param.body.actions = [{ value: 'thread-1', action_id: 'pairing-submit-slots' } as any];
111111
param.body.user = { id: 'u1', name: 'Alice' } as any;
@@ -120,7 +120,7 @@ describe('acceptPairingSlot', () => {
120120

121121
await acceptPairingSlot.handleSubmitSlots(param);
122122

123-
expect(userRepo.markNowAsLastReviewedDate).toHaveBeenCalledWith('u1');
123+
expect(userRepo.markNowAsLastReviewedDate).not.toHaveBeenCalled();
124124
});
125125

126126
it('should call closeIfComplete after recording slot selections', async () => {
@@ -162,6 +162,36 @@ describe('acceptPairingSlot', () => {
162162
expect(chatService.updateDirectMessage).toHaveBeenCalled();
163163
});
164164

165+
it('should call declineTeammate when no slots are selected', async () => {
166+
jest
167+
.spyOn(PairingRequestService.pairingRequestService, 'declineTeammate')
168+
.mockResolvedValue(makeInterview());
169+
170+
const param = buildMockActionParam();
171+
param.body.actions = [{ value: 'thread-1', action_id: 'pairing-submit-slots' } as any];
172+
param.body.user = { id: 'u1', name: 'Alice' } as any;
173+
param.body.message = { ts: 'msg-ts-1' } as any;
174+
param.body.state = {
175+
values: {
176+
'pairing-dm-slots': {
177+
'pairing-slot-selections': { selected_options: [] },
178+
},
179+
},
180+
} as any;
181+
182+
await acceptPairingSlot.handleSubmitSlots(param);
183+
184+
expect(PairingRequestService.pairingRequestService.declineTeammate).toHaveBeenCalledWith(
185+
app,
186+
expect.anything(),
187+
'u1',
188+
expect.stringContaining("didn't select"),
189+
);
190+
expect(
191+
PairingRequestService.pairingRequestService.recordSlotSelections,
192+
).not.toHaveBeenCalled();
193+
});
194+
165195
it('should not record slot selections if user is not pending', async () => {
166196
pairingSessionsRepo.getByThreadIdOrUndefined = jest
167197
.fn()

src/bot/acceptPairingSlot.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { ActionParam } from '@/slackTypes';
22
import { App } from '@slack/bolt';
33
import log from '@utils/log';
4-
import { ActionId, BlockId } from './enums';
4+
import { ActionId, BlockId, CandidateTypeLabel, InterviewFormatLabel } from './enums';
55
import { userRepo } from '@repos/userRepo';
66
import { pairingSessionsRepo } from '@repos/pairingSessionsRepo';
77
import { pairingRequestService } from '@/services/PairingRequestService';
88
import { pairingSessionCloser } from '@/services/PairingSessionCloser';
99
import { reviewLockManager } from '@utils/reviewLockManager';
1010
import { lockedExecute } from '@utils/lockedExecute';
1111
import { reportErrorAndContinue } from '@utils/reportError';
12-
import { textBlock } from '@utils/text';
12+
import { bold, compose, textBlock, ul } from '@utils/text';
1313
import { chatService } from '@/services/ChatService';
1414

1515
export const acceptPairingSlot = {
@@ -48,6 +48,16 @@ export const acceptPairingSlot = {
4848
return;
4949
}
5050

51+
if (selectedSlotIds.length === 0) {
52+
await pairingRequestService.declineTeammate(
53+
acceptPairingSlot.app,
54+
interview,
55+
userId,
56+
"You didn't select any available slots — we've moved on to the next person.",
57+
);
58+
return;
59+
}
60+
5161
const user = await userRepo.find(userId);
5262
const userFormats = user?.formats ?? [];
5363

@@ -57,10 +67,24 @@ export const acceptPairingSlot = {
5767
selectedSlotIds,
5868
userFormats,
5969
);
60-
await userRepo.markNowAsLastReviewedDate(userId);
6170

71+
const selectedSlots = interview.slots.filter(s => selectedSlotIds.includes(s.id));
72+
const slotLines = selectedSlots.map(s => `${s.date}, ${s.startTime}${s.endTime}`);
6273
await chatService.updateDirectMessage(client, userId, messageTimestamp, [
63-
textBlock(`*Thanks! You've submitted your availability.*`),
74+
textBlock(
75+
compose(
76+
`*Thanks for your availability!* Here's what you submitted:`,
77+
compose(
78+
bold(
79+
`Candidate: ${interview.candidateName} (${CandidateTypeLabel.get(interview.candidateType) ?? interview.candidateType})`,
80+
),
81+
bold(`Languages: ${interview.languages.join(', ')}`),
82+
bold(`Format: ${InterviewFormatLabel.get(interview.format) ?? interview.format}`),
83+
),
84+
`*Your available slots:*\n${ul(...slotLines)}`,
85+
`If enough teammates overlap on the same slot, the recruiting team will be notified to coordinate scheduling.`,
86+
),
87+
),
6488
]);
6589

6690
await pairingSessionCloser.closeIfComplete(acceptPairingSlot.app, threadId);

src/bot/enums.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const enum Interaction {
22
SHORTCUT_REQUEST_REVIEW = 'shortcut-request-review',
33
SUBMIT_REQUEST_REVIEW = 'submit-request-review',
44

5-
SHORTCUT_JOIN_QUEUE = 'shortcut-join-queue',
5+
SHORTCUT_JOIN_QUEUE = 'shortcut-queue-preferences',
66
SUBMIT_JOIN_QUEUE = 'submit-join-queue',
77

88
SHORTCUT_TRIGGER_CRON = 'shortcut-trigger-cron',

src/services/PairingSessionCloser.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { PairingSession, PairingSlot } from '@models/PairingSession';
22
import { InterviewFormat } from '@bot/enums';
33
import { pairingSessionsRepo } from '@repos/pairingSessionsRepo';
4+
import { userRepo } from '@repos/userRepo';
45
import { chatService } from '@/services/ChatService';
56
import { App } from '@slack/bolt';
6-
import { mention } from '@utils/text';
7+
import { mention, ul } from '@utils/text';
78
import { reviewLockManager } from '@utils/reviewLockManager';
89
import log from '@utils/log';
910

10-
export function findConfirmedSlot(interview: PairingSession): PairingSlot | undefined {
11-
return interview.slots.find(slot =>
11+
export function findConfirmedSlots(interview: PairingSession): PairingSlot[] {
12+
return interview.slots.filter(slot =>
1213
isSlotConfirmed(slot, interview.format, interview.teammatesNeededCount),
1314
);
1415
}
@@ -36,16 +37,25 @@ export const pairingSessionCloser = {
3637
return;
3738
}
3839

39-
const confirmedSlot = findConfirmedSlot(interview);
40+
const confirmedSlots = findConfirmedSlots(interview);
4041

41-
if (confirmedSlot) {
42+
if (confirmedSlots.length > 0) {
43+
const teammates = Array.from(
44+
new Map(
45+
confirmedSlots
46+
.flatMap(s => s.interestedTeammates)
47+
.map(t => [t.userId, t] as [string, typeof t]),
48+
).values(),
49+
);
50+
const slotLines = confirmedSlots.map(s => `${s.date}, ${s.startTime}${s.endTime}`);
4251
await chatService.replyToReviewThread(
4352
app.client,
4453
threadId,
45-
`${mention({ id: interview.requestorId })} Pairing session for ${interview.candidateName} is confirmed! ` +
46-
`Slot: ${confirmedSlot.date}, ${confirmedSlot.startTime}${confirmedSlot.endTime}. ` +
47-
`Teammates: ${confirmedSlot.interestedTeammates.map(t => mention({ id: t.userId })).join(' and ')}.`,
54+
`${mention({ id: interview.requestorId })} Pairing session for *${interview.candidateName}* is ready to schedule!\n\n` +
55+
`*Teammates:* ${teammates.map(t => mention({ id: t.userId })).join(', ')}\n\n` +
56+
`*Available slots (${confirmedSlots.length}):*\n${ul(...slotLines)}`,
4857
);
58+
await Promise.all(teammates.map(t => userRepo.markNowAsLastReviewedDate(t.userId)));
4959
await pairingSessionsRepo.remove(threadId);
5060
reviewLockManager.releaseLock(threadId);
5161
return;

src/services/__tests__/PairingSessionCloser.test.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { PairingSession, PairingSlot } from '@models/PairingSession';
22
import { CandidateType, InterviewFormat } from '@bot/enums';
33
import { pairingSessionsRepo } from '@repos/pairingSessionsRepo';
44
import { chatService } from '@/services/ChatService';
5-
import { pairingSessionCloser, findConfirmedSlot } from '../PairingSessionCloser';
5+
import { pairingSessionCloser, findConfirmedSlots } from '../PairingSessionCloser';
66
import { buildMockApp } from '@utils/slackMocks';
77
import { App } from '@slack/bolt';
88
import { reviewLockManager } from '@utils/reviewLockManager';
9+
import { userRepo } from '@repos/userRepo';
910

1011
function makeSlot(overrides: Partial<PairingSlot> = {}): PairingSlot {
1112
return {
@@ -44,19 +45,20 @@ describe('PairingSessionCloser', () => {
4445
pairingSessionsRepo.remove = jest.fn().mockResolvedValue(undefined);
4546
pairingSessionsRepo.getByThreadIdOrUndefined = jest.fn();
4647
reviewLockManager.releaseLock = jest.fn();
48+
userRepo.markNowAsLastReviewedDate = jest.fn().mockResolvedValue(undefined);
4749
});
4850

49-
describe('findConfirmedSlot', () => {
50-
it('should return undefined when no slot has 2 interested teammates', () => {
51+
describe('findConfirmedSlots', () => {
52+
it('should return empty array when no slot has 2 interested teammates', () => {
5153
const slot = makeSlot({
5254
interestedTeammates: [{ userId: 'u1', acceptedAt: 1, formats: [InterviewFormat.REMOTE] }],
5355
});
5456
const interview = makeInterview({ slots: [slot] });
5557

56-
expect(findConfirmedSlot(interview)).toBeUndefined();
58+
expect(findConfirmedSlots(interview)).toEqual([]);
5759
});
5860

59-
it('should return a slot with 2+ interested teammates for remote interviews', () => {
61+
it('should return confirmed slots for remote interviews', () => {
6062
const slot = makeSlot({
6163
interestedTeammates: [
6264
{ userId: 'u1', acceptedAt: 1, formats: [InterviewFormat.REMOTE] },
@@ -65,10 +67,30 @@ describe('PairingSessionCloser', () => {
6567
});
6668
const interview = makeInterview({ format: InterviewFormat.REMOTE, slots: [slot] });
6769

68-
expect(findConfirmedSlot(interview)).toEqual(slot);
70+
expect(findConfirmedSlots(interview)).toEqual([slot]);
6971
});
7072

71-
it('should return a slot with 2+ interested teammates for in-person interviews', () => {
73+
it('should return all confirmed slots when multiple qualify', () => {
74+
const slot1 = makeSlot({
75+
id: 'slot-1',
76+
interestedTeammates: [
77+
{ userId: 'u1', acceptedAt: 1, formats: [InterviewFormat.REMOTE] },
78+
{ userId: 'u2', acceptedAt: 2, formats: [InterviewFormat.REMOTE] },
79+
],
80+
});
81+
const slot2 = makeSlot({
82+
id: 'slot-2',
83+
interestedTeammates: [
84+
{ userId: 'u1', acceptedAt: 1, formats: [InterviewFormat.REMOTE] },
85+
{ userId: 'u2', acceptedAt: 2, formats: [InterviewFormat.REMOTE] },
86+
],
87+
});
88+
const interview = makeInterview({ format: InterviewFormat.REMOTE, slots: [slot1, slot2] });
89+
90+
expect(findConfirmedSlots(interview)).toEqual([slot1, slot2]);
91+
});
92+
93+
it('should return confirmed slots for in-person interviews', () => {
7294
const slot = makeSlot({
7395
interestedTeammates: [
7496
{ userId: 'u1', acceptedAt: 1, formats: [InterviewFormat.IN_PERSON] },
@@ -77,7 +99,7 @@ describe('PairingSessionCloser', () => {
7799
});
78100
const interview = makeInterview({ format: InterviewFormat.IN_PERSON, slots: [slot] });
79101

80-
expect(findConfirmedSlot(interview)).toEqual(slot);
102+
expect(findConfirmedSlots(interview)).toEqual([slot]);
81103
});
82104

83105
describe('hybrid interviews', () => {
@@ -90,7 +112,7 @@ describe('PairingSessionCloser', () => {
90112
});
91113
const interview = makeInterview({ format: InterviewFormat.HYBRID, slots: [slot] });
92114

93-
expect(findConfirmedSlot(interview)).toBeUndefined();
115+
expect(findConfirmedSlots(interview)).toEqual([]);
94116
});
95117

96118
it('should confirm a slot where at least 1 teammate is in-person capable', () => {
@@ -102,7 +124,7 @@ describe('PairingSessionCloser', () => {
102124
});
103125
const interview = makeInterview({ format: InterviewFormat.HYBRID, slots: [slot] });
104126

105-
expect(findConfirmedSlot(interview)).toEqual(slot);
127+
expect(findConfirmedSlots(interview)).toEqual([slot]);
106128
});
107129

108130
it('should confirm a slot where both teammates are in-person capable', () => {
@@ -114,7 +136,7 @@ describe('PairingSessionCloser', () => {
114136
});
115137
const interview = makeInterview({ format: InterviewFormat.HYBRID, slots: [slot] });
116138

117-
expect(findConfirmedSlot(interview)).toEqual(slot);
139+
expect(findConfirmedSlots(interview)).toEqual([slot]);
118140
});
119141
});
120142
});
@@ -154,6 +176,8 @@ describe('PairingSessionCloser', () => {
154176
'thread-1',
155177
expect.stringContaining('2026-03-31'),
156178
);
179+
expect(userRepo.markNowAsLastReviewedDate).toHaveBeenCalledWith('u1');
180+
expect(userRepo.markNowAsLastReviewedDate).toHaveBeenCalledWith('u2');
157181
expect(pairingSessionsRepo.remove).toHaveBeenCalledWith('thread-1');
158182
expect(reviewLockManager.releaseLock).toHaveBeenCalledWith('thread-1');
159183
});

0 commit comments

Comments
 (0)