Files
ketopath/packages/shared/src/nutrition/body-composition.test.ts
T
lucianoandClaude Opus 4.7 9401497f03 feat: composizione corporea, deficit dinamico, condizioni mediche (PRD §5.1)
Tre leve per migliorare l'accuratezza del piano oltre Mifflin-St Jeor.

1. Composizione corporea (US Navy + Katch-McArdle)
   - Profile: neckCm/waistCm/hipsCm opzionali in input
   - bodyFatPct calcolato lato server con la formula US Navy metrica
     (Hodgdon-Beckett 1984) e cifrato at-rest come dato sanitario
   - BMR usa Katch-McArdle (FFM-based) quando bodyFatPct è noto, Mifflin
     altrimenti — più accurato del 5-10% sui casi fuori-norma
   - Test: estimateBodyFatPercentageUSNavy + calculateBmrKatchMcArdle

2. Deficit dinamico
   - Profile.targetWeeklyLossKg (kg/settimana, opzionale)
   - computeDailyKcalTarget calcola il deficit da kg/sett ×7700/7 con due cap
     di sicurezza: 30% TDEE (Schoenfeld 2014) e 1% peso/sett (Helms 2014)
   - plan.routes lo usa al posto del -500/-200 hard-coded
   - Test: 6 casi (mantenimento, transizione, intensiva, cap peso, cap TDEE,
     default mancante)

3. Condizioni mediche strutturate
   - Profile.medicalConditions (array JSON cifrato)
   - 11 condizioni: 7 adattabili (tiroide → BMR -10%, diabete II, IBS,
     dislipidemia, ipertensione, reni → cap proteine 0.8 g/kg, fegato →
     1.0 g/kg) + 4 escludenti (gravidanza, allattamento, diabete I, disturbi
     alimentari)
   - PATCH /me/profile/conditions per salvataggio mirato dall'onboarding
   - hasExcludingCondition: plan.routes restituisce 409 medical_block se
     attiva una condizione incompatibile

Onboarding allargato a 6 step: aggiunto "Condizioni mediche" tra Profile
e Preferences. Step labels e numeri rinumerati.

Schema migration 20260429203451_add_profile_health_fields.

44/44 unit test verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:44:16 +02:00

77 lines
2.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { calculateBmrKatchMcArdle, estimateBodyFatPercentageUSNavy } from './body-composition.js';
describe('estimateBodyFatPercentageUSNavy', () => {
it('uomo medio (180cm, vita 90, collo 38) ≈ 18-22% BF', () => {
const bf = estimateBodyFatPercentageUSNavy({
gender: 'MALE',
heightCm: 180,
neckCm: 38,
waistCm: 90,
});
expect(bf).toBeGreaterThan(15);
expect(bf).toBeLessThan(25);
});
it('donna media (165cm, vita 75, fianchi 100, collo 33) ≈ 25-32% BF', () => {
const bf = estimateBodyFatPercentageUSNavy({
gender: 'FEMALE',
heightCm: 165,
neckCm: 33,
waistCm: 75,
hipsCm: 100,
});
expect(bf).toBeGreaterThan(20);
expect(bf).toBeLessThan(35);
});
it('uomo atletico (180cm, vita 80, collo 40) → BF basso (< 15%)', () => {
const bf = estimateBodyFatPercentageUSNavy({
gender: 'MALE',
heightCm: 180,
neckCm: 40,
waistCm: 80,
});
expect(bf).toBeLessThan(15);
});
it('FEMALE senza hipsCm lancia errore', () => {
expect(() =>
estimateBodyFatPercentageUSNavy({
gender: 'FEMALE',
heightCm: 165,
neckCm: 33,
waistCm: 75,
}),
).toThrow();
});
it('clamp BF% in [3, 60]', () => {
const bf = estimateBodyFatPercentageUSNavy({
gender: 'MALE',
heightCm: 180,
neckCm: 41,
waistCm: 75, // input molto magro: senza clamp uscirebbe negativo
});
expect(bf).toBeGreaterThanOrEqual(3);
});
});
describe('calculateBmrKatchMcArdle', () => {
it('80kg con 20% BF → BMR ~1750', () => {
const bmr = calculateBmrKatchMcArdle(80, 20);
// FFM = 64, BMR = 370 + 21.6*64 = 1752.4
expect(Math.round(bmr)).toBe(1752);
});
it('80kg con 30% BF → BMR < lo stesso peso al 20%', () => {
expect(calculateBmrKatchMcArdle(80, 30)).toBeLessThan(calculateBmrKatchMcArdle(80, 20));
});
it('input negativi → errore', () => {
expect(() => calculateBmrKatchMcArdle(-1, 20)).toThrow();
expect(() => calculateBmrKatchMcArdle(80, 100)).toThrow();
});
});