From fc53e10b7162415086ed3b70a3df7eef2ac26f90 Mon Sep 17 00:00:00 2001 From: Andrei Date: Thu, 2 Oct 2025 20:29:57 +0000 Subject: [PATCH] test: Add Prediction service tests (515 lines, 25 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created comprehensive test suite for ML-based prediction service: - Generate predictions for sleep and feeding schedules - Sleep predictions (next nap time, bedtime, wake windows, confidence scores) - Feeding predictions (next feeding time, expected interval, confidence) - Huckleberry SweetSpot®-inspired algorithm for sleep prediction - Age-appropriate wake windows (45-300 min based on age 0-12+ months) - Default feeding intervals by age (2.5-4 hours) - Confidence calculation based on pattern consistency and data points - High/moderate/low confidence reasoning generation - Historical pattern analysis (wake windows, feeding intervals) - Bedtime prediction based on historical patterns - Handle insufficient data scenarios gracefully Tests cover: - Insufficient data (<5 sleeps, <3 feedings) returns null with default values - Nap time prediction based on average wake windows - Bedtime prediction from historical night sleeps - High confidence (>85%) for very consistent patterns - Moderate/low confidence for less consistent patterns - Age-appropriate wake windows for all ages (0-12+ months) - Default feeding intervals by age - Reasoning generation for all confidence levels - Helper methods (average time, standard deviation, age calculation) Total: 515 lines, 25 test cases Coverage: Predictive analytics, ML-based scheduling, confidence scoring 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../analytics/prediction.service.spec.ts | 515 ++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 maternal-app/maternal-app-backend/src/modules/analytics/prediction.service.spec.ts diff --git a/maternal-app/maternal-app-backend/src/modules/analytics/prediction.service.spec.ts b/maternal-app/maternal-app-backend/src/modules/analytics/prediction.service.spec.ts new file mode 100644 index 0000000..4ad4a8b --- /dev/null +++ b/maternal-app/maternal-app-backend/src/modules/analytics/prediction.service.spec.ts @@ -0,0 +1,515 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository, Between } from 'typeorm'; +import { PredictionService } from './prediction.service'; +import { Activity, ActivityType } from '../../database/entities/activity.entity'; +import { Child } from '../../database/entities/child.entity'; + +describe('PredictionService', () => { + let service: PredictionService; + let activityRepository: Repository; + let childRepository: Repository; + + const mockChild = { + id: 'child_123', + name: 'Baby Jane', + birthDate: new Date(Date.now() - 3 * 30 * 24 * 60 * 60 * 1000), // 3 months old + familyId: 'family_123', + }; + + const mockActivityRepository = { + find: jest.fn(), + }; + + const mockChildRepository = { + findOne: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PredictionService, + { + provide: getRepositoryToken(Activity), + useValue: mockActivityRepository, + }, + { + provide: getRepositoryToken(Child), + useValue: mockChildRepository, + }, + ], + }).compile(); + + service = module.get(PredictionService); + activityRepository = module.get>( + getRepositoryToken(Activity), + ); + childRepository = module.get>( + getRepositoryToken(Child), + ); + + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('generatePredictions', () => { + it('should generate predictions for a child', async () => { + const sleepActivities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date(Date.now() - 10 * 60 * 60 * 1000), + endedAt: new Date(Date.now() - 8 * 60 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(Date.now() - 6 * 60 * 60 * 1000), + endedAt: new Date(Date.now() - 5 * 60 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(Date.now() - 3 * 60 * 60 * 1000), + endedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(Date.now() - 1 * 60 * 60 * 1000), + endedAt: new Date(Date.now() - 30 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(Date.now() - 15 * 60 * 1000), + endedAt: new Date(), + }, + ]; + + const feedingActivities = [ + { + type: ActivityType.FEEDING, + startedAt: new Date(Date.now() - 9 * 60 * 60 * 1000), + }, + { + type: ActivityType.FEEDING, + startedAt: new Date(Date.now() - 6 * 60 * 60 * 1000), + }, + { + type: ActivityType.FEEDING, + startedAt: new Date(Date.now() - 3 * 60 * 60 * 1000), + }, + ]; + + mockChildRepository.findOne.mockResolvedValue(mockChild); + mockActivityRepository.find.mockResolvedValue([ + ...sleepActivities, + ...feedingActivities, + ]); + + const result = await service.generatePredictions('child_123'); + + expect(result).toHaveProperty('sleep'); + expect(result).toHaveProperty('feeding'); + expect(result).toHaveProperty('generatedAt'); + expect(result.generatedAt).toBeInstanceOf(Date); + }); + + it('should throw error if child not found', async () => { + mockChildRepository.findOne.mockResolvedValue(null); + + await expect(service.generatePredictions('child_123')).rejects.toThrow( + 'Child not found', + ); + }); + }); + + describe('predictNextSleep', () => { + it('should return low confidence with insufficient data', async () => { + const activities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date(), + endedAt: new Date(), + }, + ]; + + const result = await (service as any).predictNextSleep( + activities, + mockChild, + ); + + expect(result.nextNapTime).toBeNull(); + expect(result.nextNapConfidence).toBe(0); + expect(result.nextBedtime).toBeNull(); + expect(result.bedtimeConfidence).toBe(0); + expect(result.reasoning).toContain('Insufficient data'); + expect(result.optimalWakeWindows).toHaveLength(3); + }); + + it('should predict next nap time based on wake windows', async () => { + const baseTime = Date.now(); + const sleepActivities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date(baseTime - 10 * 60 * 60 * 1000), + endedAt: new Date(baseTime - 9 * 60 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(baseTime - 7.5 * 60 * 60 * 1000), + endedAt: new Date(baseTime - 6.5 * 60 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(baseTime - 5 * 60 * 60 * 1000), + endedAt: new Date(baseTime - 4 * 60 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(baseTime - 2.5 * 60 * 60 * 1000), + endedAt: new Date(baseTime - 1.5 * 60 * 60 * 1000), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(baseTime - 30 * 60 * 1000), + endedAt: new Date(baseTime - 15 * 60 * 1000), + }, + ]; + + const result = await (service as any).predictNextSleep( + sleepActivities, + mockChild, + ); + + expect(result.nextNapTime).toBeInstanceOf(Date); + expect(result.nextNapConfidence).toBeGreaterThan(0); + expect(result.nextNapConfidence).toBeLessThanOrEqual(1); + }); + + it('should predict bedtime based on historical patterns', async () => { + const baseTime = Date.now(); + const today = new Date(baseTime); + today.setHours(12, 0, 0, 0); // Noon today + + const sleepActivities = [ + { + type: ActivityType.SLEEP, + startedAt: new Date(today.getTime() - 3 * 24 * 60 * 60 * 1000), + endedAt: new Date( + today.getTime() - 3 * 24 * 60 * 60 * 1000 + 8 * 60 * 60 * 1000, + ), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date(today.getTime() - 3 * 24 * 60 * 60 * 1000), + endedAt: new Date( + today.getTime() - 3 * 24 * 60 * 60 * 1000 + 30 * 60 * 1000, + ), + }, + // Night sleeps at 8PM + { + type: ActivityType.SLEEP, + startedAt: new Date( + today.getTime() - 3 * 24 * 60 * 60 * 1000 + 20 * 60 * 60 * 1000, + ), + endedAt: new Date( + today.getTime() - 2 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000, + ), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date( + today.getTime() - 2 * 24 * 60 * 60 * 1000 + 20 * 60 * 60 * 1000, + ), + endedAt: new Date( + today.getTime() - 1 * 24 * 60 * 60 * 1000 + 6 * 60 * 60 * 1000, + ), + }, + { + type: ActivityType.SLEEP, + startedAt: new Date( + today.getTime() - 1 * 24 * 60 * 60 * 1000 + 20 * 60 * 60 * 1000, + ), + endedAt: new Date(today.getTime() + 6 * 60 * 60 * 1000), + }, + ]; + + const result = await (service as any).predictNextSleep( + sleepActivities, + mockChild, + ); + + expect(result.nextBedtime).toBeInstanceOf(Date); + expect(result.bedtimeConfidence).toBeGreaterThan(0); + expect(result.bedtimeConfidence).toBeLessThanOrEqual(1); + }); + + it('should calculate high confidence for consistent patterns', async () => { + // Very consistent wake windows of exactly 90 minutes + const baseTime = Date.now(); + const sleepActivities = Array.from({ length: 10 }, (_, i) => ({ + type: ActivityType.SLEEP, + startedAt: new Date(baseTime - (10 - i) * 2.5 * 60 * 60 * 1000), + endedAt: new Date(baseTime - (10 - i) * 2.5 * 60 * 60 * 1000 + 60 * 60 * 1000), + })); + + const result = await (service as any).predictNextSleep( + sleepActivities, + mockChild, + ); + + expect(result.nextNapConfidence).toBeGreaterThan(0.8); + expect(result.reasoning).toContain('High confidence'); + }); + }); + + describe('predictNextFeeding', () => { + it('should return low confidence with insufficient data', async () => { + const activities = [ + { + type: ActivityType.FEEDING, + startedAt: new Date(), + }, + ]; + + const result = await (service as any).predictNextFeeding( + activities, + mockChild, + ); + + expect(result.nextFeedingTime).toBeNull(); + expect(result.confidence).toBe(0); + expect(result.reasoning).toContain('Insufficient data'); + expect(result.expectedInterval).toBeGreaterThan(0); + }); + + it('should predict next feeding time based on intervals', async () => { + const baseTime = Date.now(); + const feedingActivities = [ + { + type: ActivityType.FEEDING, + startedAt: new Date(baseTime - 9 * 60 * 60 * 1000), + }, + { + type: ActivityType.FEEDING, + startedAt: new Date(baseTime - 6 * 60 * 60 * 1000), + }, + { + type: ActivityType.FEEDING, + startedAt: new Date(baseTime - 3 * 60 * 60 * 1000), + }, + ]; + + const result = await (service as any).predictNextFeeding( + feedingActivities, + mockChild, + ); + + expect(result.nextFeedingTime).toBeInstanceOf(Date); + expect(result.confidence).toBeGreaterThan(0); + expect(result.confidence).toBeLessThanOrEqual(1); + expect(result.expectedInterval).toBe(3); + }); + + it('should calculate high confidence for consistent feeding patterns', async () => { + // Very consistent 3-hour intervals + const baseTime = Date.now(); + const feedingActivities = Array.from({ length: 10 }, (_, i) => ({ + type: ActivityType.FEEDING, + startedAt: new Date(baseTime - (10 - i) * 3 * 60 * 60 * 1000), + })); + + const result = await (service as any).predictNextFeeding( + feedingActivities, + mockChild, + ); + + expect(result.confidence).toBeGreaterThan(0.8); + expect(result.reasoning).toContain('High confidence'); + }); + }); + + describe('getAgeAppropriateWakeWindows', () => { + it('should return wake windows for 0-1 month old', () => { + const newborn = { + ...mockChild, + birthDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), // 15 days old + }; + + const wakeWindows = (service as any).getAgeAppropriateWakeWindows( + newborn, + ); + + expect(wakeWindows).toEqual([45, 60, 75]); + }); + + it('should return wake windows for 3-6 month old', () => { + const infant = { + ...mockChild, + birthDate: new Date(Date.now() - 4 * 30 * 24 * 60 * 60 * 1000), // 4 months + }; + + const wakeWindows = (service as any).getAgeAppropriateWakeWindows(infant); + + expect(wakeWindows).toEqual([75, 90, 120]); + }); + + it('should return wake windows for 12+ month old', () => { + const toddler = { + ...mockChild, + birthDate: new Date(Date.now() - 15 * 30 * 24 * 60 * 60 * 1000), // 15 months + }; + + const wakeWindows = (service as any).getAgeAppropriateWakeWindows( + toddler, + ); + + expect(wakeWindows).toEqual([180, 240, 300]); + }); + }); + + describe('getDefaultFeedingInterval', () => { + it('should return interval for newborn', () => { + const newborn = { + ...mockChild, + birthDate: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), + }; + + const interval = (service as any).getDefaultFeedingInterval(newborn); + + expect(interval).toBe(2.5); + }); + + it('should return interval for 3-6 month old', () => { + const infant = { + ...mockChild, + birthDate: new Date(Date.now() - 4 * 30 * 24 * 60 * 60 * 1000), + }; + + const interval = (service as any).getDefaultFeedingInterval(infant); + + expect(interval).toBe(3.5); + }); + + it('should return interval for 6+ month old', () => { + const older = { + ...mockChild, + birthDate: new Date(Date.now() - 8 * 30 * 24 * 60 * 60 * 1000), + }; + + const interval = (service as any).getDefaultFeedingInterval(older); + + expect(interval).toBe(4); + }); + }); + + describe('generateSleepReasoning', () => { + it('should generate high confidence reasoning', () => { + const reasoning = (service as any).generateSleepReasoning( + 90, // 1.5 hours wake window + 0.9, // 90% consistency + 20, // 20 data points + 0.88, // 88% confidence + ); + + expect(reasoning).toContain('High confidence'); + expect(reasoning).toContain('20 sleep sessions'); + expect(reasoning).toContain('1h 30m'); + }); + + it('should generate moderate confidence reasoning', () => { + const reasoning = (service as any).generateSleepReasoning( + 90, + 0.6, + 10, + 0.7, + ); + + expect(reasoning).toContain('Moderate confidence'); + expect(reasoning).toContain('10 sleep sessions'); + }); + + it('should generate low confidence reasoning', () => { + const reasoning = (service as any).generateSleepReasoning( + 90, + 0.4, + 5, + 0.5, + ); + + expect(reasoning).toContain('Low confidence'); + expect(reasoning).toContain('More data needed'); + expect(reasoning).toContain('5 sleep sessions'); + }); + }); + + describe('generateFeedingReasoning', () => { + it('should generate high confidence reasoning', () => { + const reasoning = (service as any).generateFeedingReasoning( + 3.2, // 3.2 hours interval + 0.88, + 15, + 0.9, + ); + + expect(reasoning).toContain('High confidence'); + expect(reasoning).toContain('15 feedings'); + expect(reasoning).toContain('3.2 hours'); + }); + + it('should generate moderate confidence reasoning', () => { + const reasoning = (service as any).generateFeedingReasoning( + 3, + 0.7, + 8, + 0.65, + ); + + expect(reasoning).toContain('Moderate confidence'); + expect(reasoning).toContain('8 feedings'); + }); + + it('should generate low confidence reasoning', () => { + const reasoning = (service as any).generateFeedingReasoning( + 3, + 0.5, + 4, + 0.4, + ); + + expect(reasoning).toContain('Low confidence'); + expect(reasoning).toContain('4 feedings'); + }); + }); + + describe('helper methods', () => { + it('should calculate average time in minutes', () => { + const dates = [ + new Date('2025-01-01T20:00:00'), + new Date('2025-01-02T20:30:00'), + new Date('2025-01-03T19:30:00'), + ]; + + const avgMinutes = (service as any).calculateAverageTimeInMinutes(dates); + + expect(avgMinutes).toBe(20 * 60); // 8PM = 1200 minutes + }); + + it('should calculate standard deviation', () => { + const values = [10, 12, 14, 16, 18]; + + const stdDev = (service as any).calculateStdDev(values); + + expect(stdDev).toBeCloseTo(2.83, 1); + }); + + it('should calculate age in months', () => { + const birthDate = new Date(Date.now() - 6 * 30 * 24 * 60 * 60 * 1000); + + const ageInMonths = (service as any).calculateAgeInMonths(birthDate); + + expect(ageInMonths).toBeGreaterThanOrEqual(5); + expect(ageInMonths).toBeLessThanOrEqual(7); + }); + }); +});