test: Add Prediction service tests (515 lines, 25 tests)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Activity>;
|
||||
let childRepository: Repository<Child>;
|
||||
|
||||
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>(PredictionService);
|
||||
activityRepository = module.get<Repository<Activity>>(
|
||||
getRepositoryToken(Activity),
|
||||
);
|
||||
childRepository = module.get<Repository<Child>>(
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user