import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { prisma } from '../src/lib/prisma.js'; import { PCQ_QUESTION_IDS } from '../src/modules/assessments/scoring/index.js'; import { COACH_SESSION_START_CUE } from '../src/modules/ai/coachPrompt.js'; import { seedCourseSessions } from '../src/modules/sessions/sessions.service.js'; import { assertNoSecrets, authHeader, createTestApp, isDatabaseAvailable, requestJson, } from './helpers.js'; const app = createTestApp(); const createdUserIds: bigint[] = []; function pcqAnswers(value: number) { return PCQ_QUESTION_IDS.map((questionId) => ({ questionId, selectedValue: String(value), })); } async function registerUser(suffix: string) { const email = `chat-${suffix}-${Date.now()}@example.com`; const { status, body } = await requestJson(app, '/auth/register', { method: 'POST', body: JSON.stringify({ email, password: 'password12' }), }); assert.equal(status, 201); const token = body.data?.sessionToken as string; const externalId = (body.data?.user as { id: string }).id; const user = await prisma.user.findUnique({ where: { externalId } }); assert.ok(user); createdUserIds.push(user.id); return { token, user }; } async function completePreTest(token: string) { const pre = await requestJson(app, '/assessments/attempts', { method: 'POST', headers: authHeader(token), body: JSON.stringify({ phase: 'pre', answers: pcqAnswers(4) }), }); assert.equal(pre.status, 201); } describe('chat persistence and AI runtime', () => { let dbAvailable = false; before(async () => { dbAvailable = await isDatabaseAvailable(); if (dbAvailable) { await seedCourseSessions(); } }); after(async () => { if (dbAvailable && createdUserIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: createdUserIds } } }); } }); it('rejects unauthenticated chat access', async () => { const created = await requestJson(app, '/chat/sessions', { method: 'POST', body: JSON.stringify({}), }); assert.equal(created.status, 401); }); it('does not leak AI secrets through public config', async () => { const { status, body, raw } = await requestJson(app, '/ai/config'); assert.equal(status, 200); assert.equal(typeof body.data?.configured, 'boolean'); assert.equal(typeof body.data?.provider, 'string'); assert.ok((body.data?.provider as string).length > 0); assert.doesNotMatch(raw, /sk-/); assert.doesNotMatch(raw, /OPENAI_API_KEY/); assert.doesNotMatch(raw, /Bearer /); assertNoSecrets(raw); }); it('lists AI runs for the authenticated user only', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const owner = await registerUser('runs-owner'); const other = await registerUser('runs-other'); await completePreTest(owner.token); const created = await requestJson(app, '/chat/sessions', { method: 'POST', headers: authHeader(owner.token), body: JSON.stringify({ sessionOrder: 1 }), }); assert.equal(created.status, 201); const chatId = created.body.data?.id as string; await requestJson(app, `/chat/sessions/${chatId}/messages`, { method: 'POST', headers: authHeader(owner.token), body: JSON.stringify({ content: 'سلام' }), }); const unauthorized = await requestJson(app, '/ai/runs'); assert.equal(unauthorized.status, 401); const runs = await requestJson(app, '/ai/runs?limit=10', { headers: authHeader(owner.token), }); assert.equal(runs.status, 200); const ownerRuns = runs.body.data as { id: string; provider: string; chatSessionId: string | null; status: string; }[]; assert.ok(Array.isArray(ownerRuns)); assert.ok(ownerRuns.length >= 1); assert.ok(ownerRuns.every((run) => typeof run.provider === 'string' && run.provider.length > 0)); assert.ok(ownerRuns.some((run) => run.chatSessionId === chatId)); assert.doesNotMatch(JSON.stringify(runs.body), /sk-/); assert.doesNotMatch(JSON.stringify(runs.body), /OPENAI_API_KEY/); const otherRuns = await requestJson(app, '/ai/runs', { headers: authHeader(other.token), }); assert.equal(otherRuns.status, 200); const otherList = otherRuns.body.data as { chatSessionId: string | null }[]; assert.equal(otherList.some((run) => run.chatSessionId === chatId), false); }); it('persists chat ownership, order, and history without exposing hidden prompts', async (t) => { if (!dbAvailable) { t.skip('MySQL is not running'); return; } const owner = await registerUser('owner'); const other = await registerUser('other'); await completePreTest(owner.token); const lockedFuture = await requestJson(app, '/chat/sessions', { method: 'POST', headers: authHeader(owner.token), body: JSON.stringify({ sessionOrder: 7 }), }); assert.equal(lockedFuture.status, 403); const created = await requestJson(app, '/chat/sessions', { method: 'POST', headers: authHeader(owner.token), body: JSON.stringify({ sessionOrder: 1 }), }); assert.equal(created.status, 201); const chatId = created.body.data?.id as string; assert.ok(chatId); assert.equal(created.body.data?.sessionOrder, 1); assert.equal(created.body.data?.endedAt, null); const listed = await requestJson(app, '/chat/sessions', { headers: authHeader(owner.token), }); assert.equal(listed.status, 200); const listedSessions = listed.body.data as { id: string; sessionOrder: number }[]; assert.ok(listedSessions.some((session) => session.id === chatId)); const start = await requestJson(app, `/chat/sessions/${chatId}/start`, { method: 'POST', headers: authHeader(owner.token), }); assert.ok(start.status === 200 || start.status === 503); if (start.status === 200) { const startMessages = (start.body.data?.messages as { role: string; content: string }[]) ?? []; assert.equal(startMessages.some((message) => message.role === 'system'), false); assert.equal( startMessages.some((message) => message.content.includes(COACH_SESSION_START_CUE)), false, ); } const first = await requestJson(app, `/chat/sessions/${chatId}/messages`, { method: 'POST', headers: authHeader(owner.token), body: JSON.stringify({ content: 'سلام، آماده هستم.' }), }); assert.ok(first.status === 201 || first.status === 503); const second = await requestJson(app, `/chat/sessions/${chatId}/messages`, { method: 'POST', headers: authHeader(owner.token), body: JSON.stringify({ content: 'لطفا ادامه بده.' }), }); assert.ok(second.status === 201 || second.status === 503); const history = await requestJson(app, `/chat/sessions/${chatId}/messages`, { headers: authHeader(owner.token), }); assert.equal(history.status, 200); const messages = (history.body.data?.messages as { role: string; content: string; createdAt: string }[]) ?? []; const userMessages = messages.filter((message) => message.role === 'user'); assert.ok(userMessages.length >= 2); assert.equal(messages.some((message) => message.role === 'system'), false); assert.equal( messages.some((message) => message.content.includes(COACH_SESSION_START_CUE)), false, ); assert.equal(userMessages[0]?.content, 'سلام، آماده هستم.'); assert.equal(userMessages[1]?.content, 'لطفا ادامه بده.'); assert.ok(new Date(userMessages[0].createdAt).getTime() <= new Date(userMessages[1].createdAt).getTime()); const otherRead = await requestJson(app, `/chat/sessions/${chatId}/messages`, { headers: authHeader(other.token), }); assert.equal(otherRead.status, 404); const otherWrite = await requestJson(app, `/chat/sessions/${chatId}/messages`, { method: 'POST', headers: authHeader(other.token), body: JSON.stringify({ content: 'پیام غیرمجاز' }), }); assert.equal(otherWrite.status, 404); }); });