import { describe, it, expect, vi, beforeEach } from 'vitest'; import { QuestionService } from '../../src/services/questions/question.service.js'; import { createMockDb, selectChainOrdered, selectChainSimple, insertChain, updateChain, } from '../test-utils.js'; const mockLlmQuestions = [ { questionBankId: 'qb-new', type: 'single_choice' as const, questionText: 'New question?', options: [{ key: 'a', text: 'A' }, { key: 'b', text: 'B' }], correctAnswer: 'a', explanation: 'Explanation', stack: 'js' as const, level: 'beginner' as const, }, ]; describe('QuestionService', () => { let mockDb: ReturnType; let mockLlmService: { generateQuestions: ReturnType }; let questionService: QuestionService; beforeEach(() => { vi.clearAllMocks(); mockDb = createMockDb(); mockLlmService = { generateQuestions: vi.fn().mockResolvedValue({ questions: mockLlmQuestions, meta: { llmModel: 'test-model', promptHash: 'hash', generationTimeMs: 100, rawResponse: {}, }, }), }; questionService = new QuestionService(mockDb as never, mockLlmService as never); }); describe('getQuestionsForTest', () => { it('returns questions from bank when enough approved exist', async () => { const approvedRows = [ { id: 'qb-1', type: 'single_choice', questionText: 'Q1?', options: [{ key: 'a', text: 'A' }], correctAnswer: 'a', explanation: 'e', }, ]; (mockDb.select as ReturnType) .mockReturnValueOnce(selectChainOrdered(approvedRows)) .mockReturnValueOnce(selectChainSimple([])); (mockDb.insert as ReturnType).mockReturnValueOnce(insertChain([])); (mockDb.update as ReturnType).mockReturnValueOnce(updateChain([])); const result = await questionService.getQuestionsForTest( 'user-1', 'js', 'beginner', 1 ); expect(result).toHaveLength(1); expect(result[0].questionBankId).toBe('qb-1'); expect(mockLlmService.generateQuestions).not.toHaveBeenCalled(); }); it('calls LLM when not enough questions in bank', async () => { const approvedRows: unknown[] = []; const seenIds: unknown[] = []; (mockDb.select as ReturnType) .mockReturnValueOnce(selectChainOrdered(approvedRows)) .mockReturnValueOnce(selectChainSimple(seenIds)); (mockDb.insert as ReturnType) .mockReturnValueOnce(insertChain([{ id: 'qb-new', ...mockLlmQuestions[0] }])) .mockReturnValueOnce(insertChain([])) .mockReturnValueOnce(insertChain([])); (mockDb.update as ReturnType).mockReturnValueOnce(updateChain([])); const result = await questionService.getQuestionsForTest( 'user-1', 'js', 'beginner', 1 ); expect(result).toHaveLength(1); expect(mockLlmService.generateQuestions).toHaveBeenCalledWith({ stack: 'js', level: 'beginner', count: 1, }); }); it('throws when not enough questions available', async () => { (mockDb.select as ReturnType) .mockReturnValueOnce(selectChainOrdered([])) .mockReturnValueOnce(selectChainSimple([])); mockLlmService.generateQuestions.mockResolvedValueOnce({ questions: [], meta: { llmModel: 'x', promptHash: 'y', generationTimeMs: 0, rawResponse: {} }, }); await expect( questionService.getQuestionsForTest('user-1', 'js', 'beginner', 5) ).rejects.toMatchObject({ code: 'QUESTIONS_UNAVAILABLE' }); }); }); });