- Split behavioral inputs into two screens (Habits + Lifestyle) - Added 5 new modifiable factors: diet quality, processed food, drug use, social connection, and stress level - Updated hazard ratios for all new factors based on meta-analyses - Model version bumped to 1.1 - Simplified welcome screen with clearer value proposition - Updated tests for expanded behavioral model Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
244 lines
8 KiB
Dart
244 lines
8 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:add_months/models/models.dart';
|
|
import 'package:add_months/risk_engine/risk_engine.dart';
|
|
|
|
void main() {
|
|
group('Hazard Ratios', () {
|
|
test('smoking current produces HR > 1.8', () {
|
|
expect(getSmokingHR(SmokingStatus.current, 10), greaterThanOrEqualTo(1.8));
|
|
});
|
|
|
|
test('smoking heavy (>20/day) produces highest HR', () {
|
|
final lightHR = getSmokingHR(SmokingStatus.current, 5);
|
|
final moderateHR = getSmokingHR(SmokingStatus.current, 15);
|
|
final heavyHR = getSmokingHR(SmokingStatus.current, 25);
|
|
|
|
expect(heavyHR, greaterThan(moderateHR));
|
|
expect(moderateHR, greaterThan(lightHR));
|
|
expect(heavyHR, equals(2.8));
|
|
});
|
|
|
|
test('never smoker has HR of 1.0', () {
|
|
expect(getSmokingHR(SmokingStatus.never, 0), equals(1.0));
|
|
});
|
|
|
|
test('sedentary activity has higher HR than active', () {
|
|
expect(
|
|
getActivityHR(ActivityLevel.sedentary),
|
|
greaterThan(getActivityHR(ActivityLevel.high)),
|
|
);
|
|
});
|
|
|
|
test('very heavy alcohol has highest HR', () {
|
|
expect(getAlcoholHR(AlcoholLevel.veryHeavy), equals(1.6));
|
|
expect(getAlcoholHR(AlcoholLevel.none), equals(1.0));
|
|
});
|
|
|
|
test('short sleep has higher HR than optimal', () {
|
|
expect(getSleepHR(5.0, true), greaterThan(getSleepHR(7.5, true)));
|
|
});
|
|
|
|
test('inconsistent sleep adds to HR', () {
|
|
expect(getSleepHR(7.0, false), greaterThan(getSleepHR(7.0, true)));
|
|
});
|
|
});
|
|
|
|
group('Mortality Tables', () {
|
|
test('Japan is in Group A', () {
|
|
expect(getCountryGroup('Japan'), equals(MortalityGroup.groupA));
|
|
});
|
|
|
|
test('United States is in Group B', () {
|
|
expect(getCountryGroup('United States'), equals(MortalityGroup.groupB));
|
|
});
|
|
|
|
test('female LE is higher than male', () {
|
|
final maleLE =
|
|
getLifeExpectancyAtBirth(MortalityGroup.groupB, Sex.male);
|
|
final femaleLE =
|
|
getLifeExpectancyAtBirth(MortalityGroup.groupB, Sex.female);
|
|
|
|
expect(femaleLE, greaterThan(maleLE));
|
|
});
|
|
|
|
test('remaining LE decreases with age', () {
|
|
final le30 = getRemainingLifeExpectancy(30, Sex.male, 'United States');
|
|
final le50 = getRemainingLifeExpectancy(50, Sex.male, 'United States');
|
|
|
|
expect(le30, greaterThan(le50));
|
|
});
|
|
|
|
test('getSupportedCountries returns sorted list', () {
|
|
final countries = getSupportedCountries();
|
|
expect(countries, isNotEmpty);
|
|
expect(countries.first, equals('Argentina'));
|
|
expect(countries, contains('United States'));
|
|
});
|
|
});
|
|
|
|
group('Calculator', () {
|
|
final healthyProfile = UserProfile(
|
|
age: 40,
|
|
sex: Sex.male,
|
|
country: 'United States',
|
|
heightCm: 175,
|
|
weightKg: 75,
|
|
);
|
|
|
|
final optimalInputs = BehavioralInputs.optimal;
|
|
|
|
final unhealthyInputs = BehavioralInputs(
|
|
smoking: SmokingStatus.current,
|
|
cigarettesPerDay: 20,
|
|
alcohol: AlcoholLevel.heavy,
|
|
sleepHours: 5.0,
|
|
sleepConsistent: false,
|
|
activity: ActivityLevel.sedentary,
|
|
diet: DietQuality.poor,
|
|
processedFood: ProcessedFoodLevel.daily,
|
|
drugUse: DrugUse.regular,
|
|
social: SocialConnection.isolated,
|
|
stress: StressLevel.chronic,
|
|
driving: DrivingExposure.veryHigh,
|
|
workHours: WorkHoursLevel.extreme,
|
|
);
|
|
|
|
test('optimal inputs produce low combined HR', () {
|
|
final hr = computeCombinedHazard(optimalInputs, 22.0);
|
|
expect(hr, closeTo(1.0, 0.1));
|
|
});
|
|
|
|
test('unhealthy inputs produce high combined HR', () {
|
|
final hr = computeCombinedHazard(unhealthyInputs, 35.0);
|
|
expect(hr, greaterThan(3.0));
|
|
});
|
|
|
|
test('combined HR is capped at 4.0', () {
|
|
final hr = computeCombinedHazard(unhealthyInputs, 45.0);
|
|
expect(hr, lessThanOrEqualTo(4.0));
|
|
});
|
|
|
|
test('ranking puts smoking above driving for heavy smoker', () {
|
|
final smokerInputs = BehavioralInputs(
|
|
smoking: SmokingStatus.current,
|
|
cigarettesPerDay: 20,
|
|
alcohol: AlcoholLevel.none,
|
|
sleepHours: 7.5,
|
|
sleepConsistent: true,
|
|
activity: ActivityLevel.high,
|
|
diet: DietQuality.excellent,
|
|
processedFood: ProcessedFoodLevel.rarely,
|
|
drugUse: DrugUse.none,
|
|
social: SocialConnection.strong,
|
|
stress: StressLevel.low,
|
|
driving: DrivingExposure.veryHigh,
|
|
workHours: WorkHoursLevel.normal,
|
|
);
|
|
|
|
final result = calculateRankedFactors(healthyProfile, smokerInputs);
|
|
|
|
expect(result.rankedFactors, isNotEmpty);
|
|
expect(result.dominantFactor?.behaviorKey, equals('smoking'));
|
|
});
|
|
|
|
test('optimal profile returns empty factors', () {
|
|
final result = calculateRankedFactors(healthyProfile, optimalInputs);
|
|
expect(result.rankedFactors, isEmpty);
|
|
});
|
|
|
|
test('result includes model version', () {
|
|
final result = calculateRankedFactors(healthyProfile, unhealthyInputs);
|
|
expect(result.modelVersion, equals('1.1'));
|
|
});
|
|
|
|
test('sedentary person sees activity as factor', () {
|
|
final sedentaryInputs = BehavioralInputs(
|
|
smoking: SmokingStatus.never,
|
|
cigarettesPerDay: 0,
|
|
alcohol: AlcoholLevel.none,
|
|
sleepHours: 7.5,
|
|
sleepConsistent: true,
|
|
activity: ActivityLevel.sedentary,
|
|
diet: DietQuality.excellent,
|
|
processedFood: ProcessedFoodLevel.rarely,
|
|
drugUse: DrugUse.none,
|
|
social: SocialConnection.strong,
|
|
stress: StressLevel.low,
|
|
driving: DrivingExposure.low,
|
|
workHours: WorkHoursLevel.normal,
|
|
);
|
|
|
|
final result = calculateRankedFactors(healthyProfile, sedentaryInputs);
|
|
|
|
expect(result.rankedFactors, isNotEmpty);
|
|
expect(result.dominantFactor?.behaviorKey, equals('activity'));
|
|
});
|
|
|
|
test('delta ranges are reasonable', () {
|
|
final result = calculateRankedFactors(healthyProfile, unhealthyInputs);
|
|
|
|
for (final factor in result.rankedFactors) {
|
|
// Low should be less than or equal to high
|
|
expect(factor.delta.lowMonths, lessThanOrEqualTo(factor.delta.highMonths));
|
|
// Months should be reasonable (not hundreds of years)
|
|
expect(factor.delta.highMonths, lessThan(240)); // < 20 years
|
|
}
|
|
});
|
|
});
|
|
|
|
group('Existing Conditions', () {
|
|
test('cardiovascular disease increases diagnosis HR', () {
|
|
final hr = getDiagnosisHR({Diagnosis.cardiovascular});
|
|
expect(hr, equals(1.5));
|
|
});
|
|
|
|
test('multiple conditions compound', () {
|
|
final singleHR = getDiagnosisHR({Diagnosis.diabetes});
|
|
final multiHR = getDiagnosisHR({Diagnosis.diabetes, Diagnosis.hypertension});
|
|
|
|
expect(multiHR, greaterThan(singleHR));
|
|
});
|
|
|
|
test('conditions reduce effective baseline', () {
|
|
final healthyProfile = UserProfile(
|
|
age: 50,
|
|
sex: Sex.male,
|
|
country: 'United States',
|
|
heightCm: 175,
|
|
weightKg: 80,
|
|
diagnoses: {},
|
|
);
|
|
|
|
final unhealthyProfile = healthyProfile.copyWith(
|
|
diagnoses: {Diagnosis.cardiovascular, Diagnosis.diabetes},
|
|
);
|
|
|
|
final sedentaryInputs = BehavioralInputs(
|
|
smoking: SmokingStatus.never,
|
|
cigarettesPerDay: 0,
|
|
alcohol: AlcoholLevel.none,
|
|
sleepHours: 7.5,
|
|
sleepConsistent: true,
|
|
activity: ActivityLevel.sedentary,
|
|
diet: DietQuality.excellent,
|
|
processedFood: ProcessedFoodLevel.rarely,
|
|
drugUse: DrugUse.none,
|
|
social: SocialConnection.strong,
|
|
stress: StressLevel.low,
|
|
driving: DrivingExposure.low,
|
|
workHours: WorkHoursLevel.normal,
|
|
);
|
|
|
|
final healthyResult = calculateRankedFactors(healthyProfile, sedentaryInputs);
|
|
final unhealthyResult = calculateRankedFactors(unhealthyProfile, sedentaryInputs);
|
|
|
|
// Unhealthy person has less to gain (lower baseline)
|
|
if (healthyResult.dominantFactor != null && unhealthyResult.dominantFactor != null) {
|
|
expect(
|
|
unhealthyResult.dominantFactor!.delta.highMonths,
|
|
lessThan(healthyResult.dominantFactor!.delta.highMonths),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|