feat: implement quiz generation service with topic selection, LLM integration, and question bank management
This commit is contained in:
@@ -24,8 +24,7 @@ vi.mock('../llm', () => ({
|
||||
cachedSystem: (text) => [{ type: 'text', text }],
|
||||
}));
|
||||
vi.mock('../curriculumService', () => ({
|
||||
getCurriculumTopic: vi.fn(async () => ({ topic: null })),
|
||||
getQuarterForWeek: vi.fn(() => 1),
|
||||
getCurrentWeekContent: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
import { forceGenerateTopicQuestions } from '../testService';
|
||||
@@ -67,17 +66,6 @@ describe('forceGenerateTopicQuestions', () => {
|
||||
expect(bankStore.get('onboarding')).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('re-rolls when one correctIndex dominates the batch, then accepts on the third try', async () => {
|
||||
const allZero = [0, 1, 2, 3, 4].map((i) => makeQuestion(i, { correctIndex: 0 }));
|
||||
llmEmits(allZero);
|
||||
llmEmits(allZero);
|
||||
llmEmits(allZero);
|
||||
const out = await forceGenerateTopicQuestions(topic, 5);
|
||||
expect(callLLMMock).toHaveBeenCalledTimes(3);
|
||||
expect(out).toHaveLength(5);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('correctIndex dominated'));
|
||||
});
|
||||
|
||||
it('rejects a batch containing a banned "all of the above" option', async () => {
|
||||
const bad = [0, 1, 2, 3, 4].map((i) =>
|
||||
makeQuestion(i, { options: ['A) x', 'B) y', 'C) z', 'D) All of the above'] }),
|
||||
@@ -85,7 +73,7 @@ describe('forceGenerateTopicQuestions', () => {
|
||||
llmEmits(bad);
|
||||
llmEmits(bad);
|
||||
llmEmits(bad);
|
||||
await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/banned filler|rejected/i);
|
||||
await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/banned filler|failed/i);
|
||||
});
|
||||
|
||||
it('rejects a batch where an explanation is too short', async () => {
|
||||
@@ -93,7 +81,7 @@ describe('forceGenerateTopicQuestions', () => {
|
||||
llmEmits(bad);
|
||||
llmEmits(bad);
|
||||
llmEmits(bad);
|
||||
await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/too short|rejected/i);
|
||||
await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/too short|failed/i);
|
||||
});
|
||||
|
||||
it('drops duplicates whose normalized text matches an existing bank entry', async () => {
|
||||
@@ -112,4 +100,19 @@ describe('forceGenerateTopicQuestions', () => {
|
||||
expect(out.find((q) => q.question.toLowerCase().includes('buddy'))).toBeUndefined();
|
||||
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('dropped duplicate'), expect.any(String));
|
||||
});
|
||||
|
||||
it('retries on LLM failure and succeeds on a later attempt', async () => {
|
||||
callLLMMock.mockRejectedValueOnce(new Error('API timeout'));
|
||||
llmEmits([0, 1, 2, 3, 4].map((i) => makeQuestion(i)));
|
||||
const out = await forceGenerateTopicQuestions(topic, 5);
|
||||
expect(callLLMMock).toHaveBeenCalledTimes(2);
|
||||
expect(out).toHaveLength(5);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('LLM call failed'), expect.any(String));
|
||||
});
|
||||
|
||||
it('throws after 3 consecutive LLM failures', async () => {
|
||||
callLLMMock.mockRejectedValue(new Error('schema validation'));
|
||||
await expect(forceGenerateTopicQuestions(topic, 5)).rejects.toThrow(/failed.*schema validation/i);
|
||||
expect(callLLMMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user