@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
22import * as logic from './exercise-logic' ;
33import { generateAndValidateExercise } from '@/lib/ai/exercise-generator' ;
44import { saveExerciseToCache , getValidatedExerciseFromCache } from '@/lib/exercise-cache' ;
5+ import { ExerciseRequestParamsSchema , GenerateExerciseResultSchema } from '@/lib/domain/schemas' ;
56import type { ExerciseGenerationParams } from '@/lib/domain/ai' ;
67import type {
78 ExerciseContent ,
@@ -21,7 +22,7 @@ const {
2122 getOrGenerateExercise,
2223} = logic ;
2324
24- const mockParams : ExerciseGenerationParams = {
25+ const mockParams = {
2526 topic : 'Test Topic' ,
2627 passageLanguage : 'en' ,
2728 questionLanguage : 'es' ,
@@ -30,28 +31,28 @@ const mockParams: ExerciseGenerationParams = {
3031 level : 'B1' ,
3132 grammarGuidance : 'Past tense' ,
3233 vocabularyGuidance : 'Travel words' ,
33- } ;
34+ } as const satisfies ExerciseGenerationParams ;
3435
35- const mockRequestParams : ExerciseRequestParams = {
36+ const mockRequestParams = {
3637 passageLanguage : 'en' ,
3738 questionLanguage : 'es' ,
3839 cefrLevel : 'B1' ,
39- } ;
40+ } as const satisfies ExerciseRequestParams ;
4041
4142const mockUserId = 123 ;
4243const mockLanguage = 'en' ;
4344
44- const mockExerciseContent : ExerciseContent = {
45+ const mockExerciseContent = {
4546 paragraph : 'p' ,
4647 question : 'q' ,
4748 options : { A : 'A' , B : 'B' , C : 'C' , D : 'D' } ,
4849 correctAnswer : 'A' ,
4950 allExplanations : { A : 'EA' , B : 'EB' , C : 'EC' , D : 'ED' } ,
5051 relevantText : 'rt' ,
5152 topic : 'Test Topic' ,
52- } ;
53+ } as const satisfies ExerciseContent ;
5354
54- const mockCacheResult : GenerateExerciseResult = {
55+ const mockCacheResult = {
5556 quizData : {
5657 paragraph : 'cached_p' ,
5758 question : 'cached_q' ,
@@ -62,7 +63,7 @@ const mockCacheResult: GenerateExerciseResult = {
6263 quizId : 999 ,
6364 cached : true ,
6465 error : null ,
65- } ;
66+ } as const satisfies GenerateExerciseResult ;
6667
6768describe ( 'Exercise Logic Functions' , ( ) => {
6869 beforeEach ( ( ) => {
@@ -91,25 +92,18 @@ describe('Exercise Logic Functions', () => {
9192 mockUserId
9293 ) ;
9394 } ) ;
94- it ( 'returns failure if generateAndValidateExercise throws an error' , async ( ) => {
95- const error = new Error ( 'AI failed' ) ;
96- vi . mocked ( generateAndValidateExercise ) . mockRejectedValue ( error ) ;
95+ it . each ( [
96+ [ new Error ( 'AI failed' ) , 'Error during AI generation/processing: AI failed' ] ,
97+ [ new Error ( 'Generic failure' ) , 'Error during AI generation/processing: Generic failure' ] ,
98+ [ 123 , 'Error during AI generation/processing: 123' ] ,
99+ [ null , 'Error during AI generation/processing: null' ] ,
100+ ] ) ( 'returns failure if generateAndValidateExercise throws %p' , async ( thrown , expectedMsg ) => {
101+ vi . mocked ( generateAndValidateExercise ) . mockRejectedValue ( thrown ) ;
97102 const result = await tryGenerateAndCacheExercise ( mockParams , mockLanguage , mockUserId ) ;
98103 expect ( result . success ) . toBe ( false ) ;
99104 if ( ! result . success ) {
100- expect ( result . error . error ) . toContain ( 'Error during AI generation/processing: AI failed' ) ;
101- }
102- expect ( saveExerciseToCache ) . not . toHaveBeenCalled ( ) ;
103- } ) ;
104- it ( 'returns failure if generateAndValidateExercise throws a non-standard error' , async ( ) => {
105- const genericError = new Error ( 'Generic failure' ) ;
106- vi . mocked ( generateAndValidateExercise ) . mockRejectedValue ( genericError ) ;
107- const result = await tryGenerateAndCacheExercise ( mockParams , mockLanguage , mockUserId ) ;
108- expect ( result . success ) . toBe ( false ) ;
109- if ( ! result . success ) {
110- expect ( result . error . error ) . toContain (
111- 'Error during AI generation/processing: Generic failure'
112- ) ;
105+ expect ( result . error . error ) . toContain ( expectedMsg ) ;
106+ expect ( typeof result . error . error ) . toBe ( 'string' ) ;
113107 }
114108 expect ( saveExerciseToCache ) . not . toHaveBeenCalled ( ) ;
115109 } ) ;
@@ -125,17 +119,27 @@ describe('Exercise Logic Functions', () => {
125119 }
126120 } ) ;
127121 it ( 'returns failure if saveExerciseToCache throws an error' , async ( ) => {
128- const cacheError = new Error ( 'Cache save failed' ) ;
129122 vi . mocked ( generateAndValidateExercise ) . mockResolvedValue ( mockExerciseContent ) ;
130123 vi . mocked ( saveExerciseToCache ) . mockImplementation ( ( ) => {
131- throw cacheError ;
124+ throw new Error ( 'Cache save failed' ) ;
132125 } ) ;
133126 const result = await tryGenerateAndCacheExercise ( mockParams , mockLanguage , mockUserId ) ;
134127 expect ( result . success ) . toBe ( false ) ;
135128 if ( ! result . success ) {
136129 expect ( result . error . error ) . toBe ( 'Error during AI generation/processing: Cache save failed' ) ;
137130 }
138131 } ) ;
132+ it ( 'returns failure if saveExerciseToCache throws a non-Error value' , async ( ) => {
133+ vi . mocked ( generateAndValidateExercise ) . mockResolvedValue ( mockExerciseContent ) ;
134+ vi . mocked ( saveExerciseToCache ) . mockImplementation ( ( ) => {
135+ throw new Error ( '123' ) ;
136+ } ) ;
137+ const result = await tryGenerateAndCacheExercise ( mockParams , mockLanguage , mockUserId ) ;
138+ expect ( result . success ) . toBe ( false ) ;
139+ if ( ! result . success ) {
140+ expect ( result . error . error ) . toBe ( 'Error during AI generation/processing: 123' ) ;
141+ }
142+ } ) ;
139143 } ) ;
140144
141145 describe ( 'tryGetCachedExercise' , ( ) => {
@@ -152,17 +156,12 @@ describe('Exercise Logic Functions', () => {
152156 mockRequestParams . cefrLevel ,
153157 mockUserId
154158 ) ;
159+ expect ( GenerateExerciseResultSchema . safeParse ( result ) . success ) . toBe ( true ) ;
155160 } ) ;
156161 it ( 'returns null if no cached exercise is found' , async ( ) => {
157162 vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( undefined ) ;
158163 const result = await tryGetCachedExercise ( mockRequestParams , mockUserId ) ;
159164 expect ( result ) . toBeNull ( ) ;
160- expect ( getValidatedExerciseFromCache ) . toHaveBeenCalledWith (
161- mockRequestParams . passageLanguage ,
162- mockRequestParams . questionLanguage ,
163- mockRequestParams . cefrLevel ,
164- mockUserId
165- ) ;
166165 } ) ;
167166 } ) ;
168167
@@ -176,6 +175,7 @@ describe('Exercise Logic Functions', () => {
176175 error : errorMsg ,
177176 cached : false ,
178177 } ) ;
178+ expect ( GenerateExerciseResultSchema . safeParse ( response ) . success ) . toBe ( true ) ;
179179 } ) ;
180180 it ( 'includes details if provided' , ( ) => {
181181 const errorMsg = 'Test error' ;
@@ -187,6 +187,7 @@ describe('Exercise Logic Functions', () => {
187187 error : `${ errorMsg } : ${ JSON . stringify ( details ) } ` ,
188188 cached : false ,
189189 } ) ;
190+ expect ( GenerateExerciseResultSchema . safeParse ( response ) . success ) . toBe ( true ) ;
190191 } ) ;
191192 } ) ;
192193
@@ -199,17 +200,22 @@ describe('Exercise Logic Functions', () => {
199200 expect ( result . data . passageLanguage ) . toBe ( validParams . passageLanguage ) ;
200201 expect ( result . data . questionLanguage ) . toBe ( validParams . questionLanguage ) ;
201202 expect ( result . data . cefrLevel ) . toBe ( validParams . cefrLevel ) ;
203+ expect ( ExerciseRequestParamsSchema . safeParse ( result . data ) . success ) . toBe ( true ) ;
202204 }
203205 } ) ;
204206 it ( 'invalidates incorrect params' , ( ) => {
205207 const invalidParams = { passageLanguage : 'en' , questionLanguage : 'fr' , cefrLevel : 'Z9' } ;
206208 const result = validateRequestParams ( invalidParams ) ;
207209 expect ( result . success ) . toBe ( false ) ;
208210 } ) ;
211+ it ( 'invalidates missing params' , ( ) => {
212+ const result = validateRequestParams ( { } ) ;
213+ expect ( result . success ) . toBe ( false ) ;
214+ } ) ;
209215 } ) ;
210216
211217 describe ( 'getOrGenerateExercise' , ( ) => {
212- const validGenParams : ExerciseGenerationParams = {
218+ const validGenParams = {
213219 passageLanguage : 'en' ,
214220 questionLanguage : 'fr' ,
215221 level : 'A2' ,
@@ -218,10 +224,10 @@ describe('Exercise Logic Functions', () => {
218224 questionLangName : 'French' ,
219225 grammarGuidance : 'Mock Grammar' ,
220226 vocabularyGuidance : 'Mock Vocab' ,
221- } ;
227+ } as const satisfies ExerciseGenerationParams ;
222228 const mockUserId = 1 ;
223229 const generatedSuccessData = { content : mockExerciseContent , id : 555 } ;
224- const cachedResult : GenerateExerciseResult = {
230+ const cachedResult = {
225231 quizData : {
226232 paragraph : 'Cached Para' ,
227233 question : 'Cached Q' ,
@@ -232,35 +238,46 @@ describe('Exercise Logic Functions', () => {
232238 quizId : 999 ,
233239 cached : true ,
234240 error : null ,
235- } ;
241+ } as const satisfies GenerateExerciseResult ;
236242 beforeEach ( ( ) => {
237243 vi . resetAllMocks ( ) ;
238244 } ) ;
239- it ( 'returns generated exercise if cache is low and generation succeeds' , async ( ) => {
240- vi . mocked ( generateAndValidateExercise ) . mockResolvedValue ( mockExerciseContent ) ;
241- vi . mocked ( saveExerciseToCache ) . mockReturnValue ( 555 ) ;
242- vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( undefined ) ;
243- const result = await getOrGenerateExercise ( validGenParams , mockUserId , 10 ) ;
244- expect ( result . quizId ) . toBe ( generatedSuccessData . id ) ;
245- expect ( result . cached ) . toBe ( false ) ;
246- expect ( result . error ) . toBeNull ( ) ;
247- expect ( result . quizData . paragraph ) . toBe ( mockExerciseContent . paragraph ) ;
248- } ) ;
249- it ( 'returns cached exercise if cache is low, generation fails, but cache fallback succeeds' , async ( ) => {
250- vi . mocked ( generateAndValidateExercise ) . mockRejectedValue ( new Error ( 'Generation Failed' ) ) ;
251- vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( {
252- quizData : cachedResult . quizData ,
253- quizId : cachedResult . quizId ,
254- } ) ;
255- const result = await getOrGenerateExercise ( validGenParams , mockUserId , 10 ) ;
256- expect ( result ) . toEqual ( cachedResult ) ;
257- } ) ;
245+ it . each ( [
246+ [ 10 , false ] ,
247+ [ 99 , false ] ,
248+ [ 100 , true ] ,
249+ [ 200 , true ] ,
250+ ] ) (
251+ 'returns correct result for cachedCount=%i (preferGenerate=%s)' ,
252+ async ( cachedCount , preferCache ) => {
253+ if ( preferCache ) {
254+ vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( {
255+ quizData : cachedResult . quizData ,
256+ quizId : cachedResult . quizId ,
257+ } ) ;
258+ const result = await getOrGenerateExercise ( validGenParams , mockUserId , cachedCount ) ;
259+ expect ( result ) . toEqual ( cachedResult ) ;
260+ expect ( GenerateExerciseResultSchema . safeParse ( result ) . success ) . toBe ( true ) ;
261+ } else {
262+ vi . mocked ( generateAndValidateExercise ) . mockResolvedValue ( mockExerciseContent ) ;
263+ vi . mocked ( saveExerciseToCache ) . mockReturnValue ( 555 ) ;
264+ vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( undefined ) ;
265+ const result = await getOrGenerateExercise ( validGenParams , mockUserId , cachedCount ) ;
266+ expect ( result . quizId ) . toBe ( generatedSuccessData . id ) ;
267+ expect ( result . cached ) . toBe ( false ) ;
268+ expect ( result . error ) . toBeNull ( ) ;
269+ expect ( result . quizData . paragraph ) . toBe ( mockExerciseContent . paragraph ) ;
270+ expect ( GenerateExerciseResultSchema . safeParse ( result ) . success ) . toBe ( true ) ;
271+ }
272+ }
273+ ) ;
258274 it ( 'returns generation error if cache is low, generation and cache fallback both fail' , async ( ) => {
259275 vi . mocked ( generateAndValidateExercise ) . mockRejectedValue ( new Error ( 'Generation Failed' ) ) ;
260276 vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( undefined ) ;
261277 const result = await getOrGenerateExercise ( validGenParams , mockUserId , 10 ) ;
262278 expect ( result . error ) . toContain ( 'Generation Failed' ) ;
263279 expect ( result . quizId ) . toBe ( - 1 ) ;
280+ expect ( GenerateExerciseResultSchema . safeParse ( result ) . success ) . toBe ( true ) ;
264281 } ) ;
265282 it ( 'returns generation error if cache is low and generation fails (terminal cache error)' , async ( ) => {
266283 vi . mocked ( generateAndValidateExercise ) . mockResolvedValue ( mockExerciseContent ) ;
@@ -271,31 +288,15 @@ describe('Exercise Logic Functions', () => {
271288 'Exercise generated but failed to save to cache (undefined ID).'
272289 ) ;
273290 expect ( result . quizId ) . toBe ( - 1 ) ;
274- } ) ;
275- it ( 'returns cached exercise if cache is high and cache lookup succeeds' , async ( ) => {
276- vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( {
277- quizData : cachedResult . quizData ,
278- quizId : cachedResult . quizId ,
279- } ) ;
280- const result = await getOrGenerateExercise ( validGenParams , mockUserId , 200 ) ;
281- expect ( result ) . toEqual ( cachedResult ) ;
282- } ) ;
283- it ( 'returns generated exercise if cache is high, cache lookup fails, but generation succeeds' , async ( ) => {
284- vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( undefined ) ;
285- vi . mocked ( generateAndValidateExercise ) . mockResolvedValue ( mockExerciseContent ) ;
286- vi . mocked ( saveExerciseToCache ) . mockReturnValue ( 555 ) ;
287- const result = await getOrGenerateExercise ( validGenParams , mockUserId , 200 ) ;
288- expect ( result . quizId ) . toBe ( generatedSuccessData . id ) ;
289- expect ( result . cached ) . toBe ( false ) ;
290- expect ( result . error ) . toBeNull ( ) ;
291- expect ( result . quizData . paragraph ) . toBe ( mockExerciseContent . paragraph ) ;
291+ expect ( GenerateExerciseResultSchema . safeParse ( result ) . success ) . toBe ( true ) ;
292292 } ) ;
293293 it ( 'returns generation error if cache is high, cache lookup and generation both fail' , async ( ) => {
294294 vi . mocked ( getValidatedExerciseFromCache ) . mockReturnValue ( undefined ) ;
295295 vi . mocked ( generateAndValidateExercise ) . mockRejectedValue ( new Error ( 'Generation Failed' ) ) ;
296296 const result = await getOrGenerateExercise ( validGenParams , mockUserId , 200 ) ;
297297 expect ( result . error ) . toContain ( 'Generation Failed' ) ;
298298 expect ( result . quizId ) . toBe ( - 1 ) ;
299+ expect ( GenerateExerciseResultSchema . safeParse ( result ) . success ) . toBe ( true ) ;
299300 } ) ;
300301 } ) ;
301302} ) ;
0 commit comments