import { afterAll, describe, expect, it } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, createTestProgram, createTestProject, createTestCompetition, createTestRound, cleanupTestData, uid, } from '../helpers' import { mentorRouter } from '../../src/server/routers/mentor' import type { UserRole } from '@prisma/client' /** * The default `createTestUser` helper only populates the singular `role` * column — but `mentor.getCandidates` filters on the multi-role `roles[]` * array. Wrap creation here so test users land with the right shape. */ async function createUserWithRoles( primaryRole: UserRole, rolesArray: UserRole[], overrides: { expertiseTags?: string[]; maxAssignments?: number | null; country?: string | null } = {}, ) { const id = uid('user') return prisma.user.create({ data: { id, email: `${id}@test.local`, name: `Test ${primaryRole}`, role: primaryRole, roles: rolesArray, status: 'ACTIVE', expertiseTags: overrides.expertiseTags ?? [], maxAssignments: overrides.maxAssignments ?? null, country: overrides.country ?? null, }, }) } describe('mentor.getCandidates', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await prisma.mentorAssignment.deleteMany({ where: { project: { programId } } }) await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) it('returns all MENTOR-role users sorted by expertise overlap, excluding non-mentors', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `getCandidates-${uid()}` }) programIds.push(program.id) const project = await createTestProject(program.id, { title: 'Reef Monitor', description: 'Coral reef IoT sensor for ocean acidification.', tags: ['coral', 'iot', 'sensors'], }) const mentorHigh = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['coral', 'iot', 'marine biology'], }) userIds.push(mentorHigh.id) const mentorLow = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['marketing'], }) userIds.push(mentorLow.id) const nonMentor = await createUserWithRoles('JURY_MEMBER', ['JURY_MEMBER'], { expertiseTags: ['coral', 'iot'], }) userIds.push(nonMentor.id) const caller = createCaller(mentorRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) const result = await caller.getCandidates({ projectId: project.id }) const mentorIds = result.candidates.map((c: { id: string }) => c.id) expect(mentorIds).toContain(mentorHigh.id) expect(mentorIds).toContain(mentorLow.id) expect(mentorIds).not.toContain(nonMentor.id) const high = result.candidates.find((c: { id: string }) => c.id === mentorHigh.id)! const low = result.candidates.find((c: { id: string }) => c.id === mentorLow.id)! expect(high.overlapScore).toBeGreaterThan(low.overlapScore) // Default sort: highest overlap first const indexHigh = mentorIds.indexOf(mentorHigh.id) const indexLow = mentorIds.indexOf(mentorLow.id) expect(indexHigh).toBeLessThan(indexLow) }) it('exposes load + capacity per candidate', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `getCandidates-load-${uid()}` }) programIds.push(program.id) const project = await createTestProject(program.id, { title: 'P1', tags: ['x'] }) const otherProject = await createTestProject(program.id, { title: 'P2', tags: ['x'] }) const mentor = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['x'], maxAssignments: 3, }) userIds.push(mentor.id) await prisma.mentorAssignment.create({ data: { projectId: otherProject.id, mentorId: mentor.id, method: 'MANUAL', assignedBy: admin.id, }, }) const caller = createCaller(mentorRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) const result = await caller.getCandidates({ projectId: project.id }) const found = result.candidates.find((c: { id: string }) => c.id === mentor.id)! expect(found.currentAssignments).toBe(1) expect(found.maxAssignments).toBe(3) }) }) describe('mentor.autoAssignBulkForRound', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await prisma.mentorAssignment.deleteMany({ where: { project: { programId } } }) await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) it('respects requested_only eligibility — skips projects without wantsMentorship=true', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `bulk-requested-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'MENTORING', configJson: { eligibility: 'requested_only' }, }) const projWithRequest = await prisma.project.create({ data: { id: uid('proj'), title: 'Wants Mentorship', programId: program.id, tags: ['x'], wantsMentorship: true, }, }) const projWithoutRequest = await prisma.project.create({ data: { id: uid('proj'), title: 'No Request', programId: program.id, tags: ['x'], wantsMentorship: false, }, }) await prisma.projectRoundState.create({ data: { projectId: projWithRequest.id, roundId: round.id, state: 'PENDING' }, }) await prisma.projectRoundState.create({ data: { projectId: projWithoutRequest.id, roundId: round.id, state: 'PENDING' }, }) // Both projects must have CONFIRMED FinalistConfirmation to qualify for // mentor assignment — the wantsMentorship gate is what we're isolating here. await prisma.finalistConfirmation.createMany({ data: [ { projectId: projWithRequest.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: new Date(), }, { projectId: projWithoutRequest.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: new Date(), }, ], }) const mentor = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['x'] }) userIds.push(mentor.id) const caller = createCaller(mentorRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) const result = await caller.autoAssignBulkForRound({ roundId: round.id, useAI: false }) expect(result.assigned).toBe(1) const requestedAssigned = await prisma.mentorAssignment.findFirst({ where: { projectId: projWithRequest.id }, }) expect(requestedAssigned).not.toBeNull() const skippedNotAssigned = await prisma.mentorAssignment.findFirst({ where: { projectId: projWithoutRequest.id }, }) expect(skippedNotAssigned).toBeNull() }) it('skips projects already assigned (any method)', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `bulk-skip-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'MENTORING', configJson: { eligibility: 'all_advancing' }, }) const existingMentor = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['x'] }) const otherMentor = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['x'] }) userIds.push(existingMentor.id, otherMentor.id) const projAlreadyAssigned = await prisma.project.create({ data: { id: uid('proj'), title: 'Already', programId: program.id, tags: ['x'], wantsMentorship: true }, }) const projUnassigned = await prisma.project.create({ data: { id: uid('proj'), title: 'Open', programId: program.id, tags: ['x'], wantsMentorship: false }, }) await prisma.projectRoundState.create({ data: { projectId: projAlreadyAssigned.id, roundId: round.id, state: 'PENDING' }, }) await prisma.projectRoundState.create({ data: { projectId: projUnassigned.id, roundId: round.id, state: 'PENDING' }, }) // Both projects must have CONFIRMED FinalistConfirmation to qualify for // mentor assignment — the existing-assignment gate is what we're isolating here. await prisma.finalistConfirmation.createMany({ data: [ { projectId: projAlreadyAssigned.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: new Date(), }, { projectId: projUnassigned.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: new Date(), }, ], }) await prisma.mentorAssignment.create({ data: { projectId: projAlreadyAssigned.id, mentorId: existingMentor.id, method: 'MANUAL', assignedBy: admin.id, }, }) const caller = createCaller(mentorRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) const result = await caller.autoAssignBulkForRound({ roundId: round.id, useAI: false }) expect(result.assigned).toBe(1) expect(result.skipped).toBe(1) const stillExisting = await prisma.mentorAssignment.findFirst({ where: { projectId: projAlreadyAssigned.id }, }) expect(stillExisting?.mentorId).toBe(existingMentor.id) // unchanged }) it('only assigns mentors to projects with CONFIRMED FinalistConfirmation', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `bulk-finalist-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'MENTORING', configJson: { eligibility: 'all_advancing' }, }) const projConfirmed = await prisma.project.create({ data: { id: uid('proj'), title: 'Confirmed Finalist', programId: program.id, tags: ['x'], wantsMentorship: true, }, }) const projPending = await prisma.project.create({ data: { id: uid('proj'), title: 'Pending Finalist', programId: program.id, tags: ['x'], wantsMentorship: true, }, }) const projNoConfirmation = await prisma.project.create({ data: { id: uid('proj'), title: 'No Confirmation', programId: program.id, tags: ['x'], wantsMentorship: true, }, }) await prisma.projectRoundState.createMany({ data: [ { projectId: projConfirmed.id, roundId: round.id, state: 'PENDING' }, { projectId: projPending.id, roundId: round.id, state: 'PENDING' }, { projectId: projNoConfirmation.id, roundId: round.id, state: 'PENDING' }, ], }) await prisma.finalistConfirmation.create({ data: { projectId: projConfirmed.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: new Date(), }, }) await prisma.finalistConfirmation.create({ data: { projectId: projPending.id, category: 'STARTUP', status: 'PENDING', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, }, }) const mentor = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['x'] }) userIds.push(mentor.id) const caller = createCaller(mentorRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const result = await caller.autoAssignBulkForRound({ roundId: round.id, useAI: false }) expect(result.assigned).toBe(1) const confirmedAssigned = await prisma.mentorAssignment.findFirst({ where: { projectId: projConfirmed.id }, }) expect(confirmedAssigned).not.toBeNull() const pendingAssigned = await prisma.mentorAssignment.findFirst({ where: { projectId: projPending.id }, }) expect(pendingAssigned).toBeNull() const noConfAssigned = await prisma.mentorAssignment.findFirst({ where: { projectId: projNoConfirmation.id }, }) expect(noConfAssigned).toBeNull() }) it('refuses with admin_selected eligibility (admin must pick manually)', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `bulk-admin-selected-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'MENTORING', configJson: { eligibility: 'admin_selected' }, }) const caller = createCaller(mentorRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) await expect( caller.autoAssignBulkForRound({ roundId: round.id, useAI: false }), ).rejects.toThrow(/admin_selected|admin-selected|manually/i) }) }) describe('mentor.getSuggestions source flag', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) it('marks source=fallback when OPENAI_API_KEY is missing', async () => { const original = process.env.OPENAI_API_KEY delete process.env.OPENAI_API_KEY try { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `source-flag-${uid()}` }) programIds.push(program.id) const project = await createTestProject(program.id, { title: 'Fallback test', tags: ['x'] }) const mentor = await createUserWithRoles('MENTOR', ['MENTOR'], { expertiseTags: ['x'] }) userIds.push(mentor.id) const caller = createCaller(mentorRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN' }) const result = await caller.getSuggestions({ projectId: project.id, limit: 5 }) expect(result.source).toBe('fallback') } finally { if (original) process.env.OPENAI_API_KEY = original } }) })