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:
2025-10-02 20:29:57 +00:00
parent 17aa39e6a3
commit fc53e10b71

View File

@@ -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);
});
});
});