feat(workspace): mentor + applicant message previews (§F.2)

mentor.getRecentMessages: last N unread messages from teams across all
of a mentor's assignments. Drives a Recent Messages card on /mentor.

applicant.getMentorConversationPreview: last 3 messages + unread count
for a given project. Drives a 'Conversation with [Mentor]' card on
/applicant — auto-hides when no mentor is assigned.

Both procedures use the existing MentorMessage(projectId, createdAt)
composite index — no new index needed.

Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
This commit is contained in:
Matt
2026-04-28 16:14:11 +02:00
parent 70a9752d73
commit 26ff8ed111
7 changed files with 352 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
import { afterAll, describe, expect, it } from 'vitest'
import { prisma, createCaller } from '../setup'
import {
createTestProgram,
createTestProject,
cleanupTestData,
uid,
} from '../helpers'
import { mentorRouter } from '../../src/server/routers/mentor'
import { applicantRouter } from '../../src/server/routers/applicant'
import type { UserRole } from '@prisma/client'
async function createUserWithRoles(primaryRole: UserRole, rolesArray: UserRole[]) {
const id = uid('user')
return prisma.user.create({
data: {
id,
email: `${id}@test.local`,
name: `Test ${primaryRole}`,
role: primaryRole,
roles: rolesArray,
status: 'ACTIVE',
},
})
}
describe('mentor.getRecentMessages', () => {
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 recent unread messages from team across mentor assignments', async () => {
const program = await createTestProgram({ name: `recent-msgs-${uid()}` })
programIds.push(program.id)
const mentor = await createUserWithRoles('MENTOR', ['MENTOR'])
const applicant = await createUserWithRoles('APPLICANT', ['APPLICANT'])
userIds.push(mentor.id, applicant.id)
const project = await createTestProject(program.id)
const ma = await prisma.mentorAssignment.create({
data: {
projectId: project.id,
mentorId: mentor.id,
method: 'MANUAL',
workspaceEnabled: true,
},
})
await prisma.mentorMessage.create({
data: {
projectId: project.id,
senderId: applicant.id,
message: 'Question 1',
workspaceId: ma.id,
isRead: false,
},
})
await prisma.mentorMessage.create({
data: {
projectId: project.id,
senderId: applicant.id,
message: 'Question 2',
workspaceId: ma.id,
isRead: false,
},
})
await prisma.mentorMessage.create({
data: {
projectId: project.id,
senderId: applicant.id,
message: 'old',
workspaceId: ma.id,
isRead: true,
},
})
const caller = createCaller(mentorRouter, { id: mentor.id, email: mentor.email, role: 'MENTOR' })
const result = await caller.getRecentMessages({ limit: 5 })
expect(result.unread.length).toBe(2)
expect(result.unread[0].message).toContain('Question')
})
})
describe('applicant.getMentorConversationPreview', () => {
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 last 3 messages + unread count for the project', async () => {
const program = await createTestProgram({ name: `conv-preview-${uid()}` })
programIds.push(program.id)
const mentor = await createUserWithRoles('MENTOR', ['MENTOR'])
const applicant = await createUserWithRoles('APPLICANT', ['APPLICANT'])
userIds.push(mentor.id, applicant.id)
const project = await createTestProject(program.id)
await prisma.teamMember.create({
data: { projectId: project.id, userId: applicant.id, role: 'LEAD' },
})
const ma = await prisma.mentorAssignment.create({
data: {
projectId: project.id,
mentorId: mentor.id,
method: 'MANUAL',
workspaceEnabled: true,
},
})
for (let i = 0; i < 5; i++) {
await prisma.mentorMessage.create({
data: {
projectId: project.id,
senderId: mentor.id,
message: `mentor msg ${i}`,
workspaceId: ma.id,
isRead: i < 2,
},
})
}
const caller = createCaller(applicantRouter, {
id: applicant.id,
email: applicant.email,
role: 'APPLICANT',
})
const result = await caller.getMentorConversationPreview({ projectId: project.id })
expect(result.messages.length).toBe(3)
expect(result.unreadCount).toBe(3)
expect(result.mentor?.id).toBe(mentor.id)
})
})