118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
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<typeof createMockDb>;
|
|
let mockLlmService: { generateQuestions: ReturnType<typeof vi.fn> };
|
|
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<typeof vi.fn>)
|
|
.mockReturnValueOnce(selectChainOrdered(approvedRows))
|
|
.mockReturnValueOnce(selectChainSimple([]));
|
|
(mockDb.insert as ReturnType<typeof vi.fn>).mockReturnValueOnce(insertChain([]));
|
|
(mockDb.update as ReturnType<typeof vi.fn>).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<typeof vi.fn>)
|
|
.mockReturnValueOnce(selectChainOrdered(approvedRows))
|
|
.mockReturnValueOnce(selectChainSimple(seenIds));
|
|
(mockDb.insert as ReturnType<typeof vi.fn>)
|
|
.mockReturnValueOnce(insertChain([{ id: 'qb-new', ...mockLlmQuestions[0] }]))
|
|
.mockReturnValueOnce(insertChain([]))
|
|
.mockReturnValueOnce(insertChain([]));
|
|
(mockDb.update as ReturnType<typeof vi.fn>).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<typeof vi.fn>)
|
|
.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' });
|
|
});
|
|
});
|
|
});
|