// La regola dei gruppi (issue #13, manuale batteria test di Nicola del 27/09/2026): per // alcune sotto-metriche (handgrip, spinta, arti_inferiori, vo2max, hrr, flamingo, // back_scratch) sottoDominiFisici sceglie fra piu' alternative COMPLETE quella la cui // misura piu' recente vince (`recenza`, opzionale). Questo file copre la regola da sola, // con `sottoDominiFisici` chiamata direttamente (mappe di misure a mano, non un DB): i // valori attesi si calcolano con le STESSE funzioni di test-fisici.ts (gia' verificate // contro l'oracolo in motore-fisici.test.ts), mai a mano. import { describe, it, expect } from 'vitest'; import { sottoDominiFisici } from '../../src/lib/longevity/motore/fisici'; import * as F from '../../src/lib/longevity/motore/test-fisici'; describe('un caso per ogni alternativa che da punteggio', () => { it('handgrip: media dx/sx (manuale)', () => { const m = new Map([['f_handgrip_dx_kg', 45], ['f_handgrip_sx_kg', 51]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.handgrip).toBe(F.scoreHandgrip(48, 38, 'M')); // media (45+51)/2 = 48 }); it('handgrip: storico f_handgrip_kg (ancora letto)', () => { const m = new Map([['f_handgrip_kg', 44]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.handgrip).toBe(F.scoreHandgrip(44, 38, 'M')); }); it('spinta: panca con bilanciere (carico, ripetizioni, peso)', () => { const m = new Map([['f_panca_bil_carico_kg', 60], ['f_panca_bil_rip', 8], ['f_peso_kg', 80]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.spinta).toBe(F.scoreSollevamentoSuPeso(60, 80, 8, F.TIERS_BENCH_M, F.TIERS_BENCH_F, 'M')); }); it('spinta: push-up (invariato)', () => { const m = new Map([['f_pushup_rip', 24]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.spinta).toBe(F.scorePushup(24, 38, 'M')); }); it('arti_inferiori: sit-to-stand (ripetizioni, eta, sesso, bmi da peso/altezza)', () => { const m = new Map([['f_sit_to_stand_rip', 14], ['f_peso_kg', 65], ['f_altezza_cm', 160]]); const r = sottoDominiFisici(m, 65, 'F'); const bmi = 65 / (160 / 100) ** 2; expect(r.arti_inferiori).toBe(F.scoreSitToStand(14, 65, 'F', bmi)); }); it('vo2max: MILFIT (potenza, peso)', () => { const m = new Map([['f_milfit_watt', 150], ['f_peso_kg', 80]]); const r = sottoDominiFisici(m, 40, 'M'); const vo2 = F.vo2maxDaMilfit(150, 80); expect(r.vo2max).toBe(F.scoreVo2max(vo2, 40, 'M')); }); it('vo2max: camminata 2 km UKK (tempo, fc, eta, bmi)', () => { const m = new Map([ ['f_walk2km_min', 18], ['f_walk2km_fc_bpm', 130], ['f_peso_kg', 80], ['f_altezza_cm', 180], ]); const r = sottoDominiFisici(m, 40, 'M'); const bmi = 80 / (180 / 100) ** 2; const vo2 = F.vo2maxDa2kmWalk(18, 130, 40, bmi); expect(r.vo2max).toBe(F.scoreVo2max(vo2, 40, 'M')); }); it('hrr: MUTT (FC max - FC dopo 1 minuto, manuale)', () => { const m = new Map([['f_mutt_fc_max_bpm', 180], ['f_mutt_fc_1min_bpm', 140]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.hrr).toBe(F.scoreRecuperoCardiaco(40)); // calo = 180 - 140 }); it('hrr: storico f_hrr_1min_bpm (gia un calo, ancora letto)', () => { const m = new Map([['f_hrr_1min_bpm', 18]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.hrr).toBe(F.scoreRecuperoCardiaco(18)); }); it('flamingo: media dx/sx occhi aperti (manuale)', () => { const m = new Map([['f_flamingo_oa_dx', 3], ['f_flamingo_oa_sx', 5]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.flamingo).toBe(F.scoreFlamingo(4)); // media (3+5)/2 = 4 }); it('flamingo: storico f_flamingo_cadute (ancora letto)', () => { const m = new Map([['f_flamingo_cadute', 6]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.flamingo).toBe(F.scoreFlamingo(6)); }); it('back_scratch: media dx/sx (manuale)', () => { const m = new Map([['f_back_scratch_dx_cm', -8], ['f_back_scratch_sx_cm', -4]]); const r = sottoDominiFisici(m, 45, 'F'); expect(r.back_scratch).toBe(F.scoreBackScratch(-6, 45, 'F')); // media (-8-4)/2 = -6 }); it('back_scratch: storico f_back_scratch_cm (ancora letto)', () => { const m = new Map([['f_back_scratch_cm', -10]]); const r = sottoDominiFisici(m, 45, 'F'); expect(r.back_scratch).toBe(F.scoreBackScratch(-10, 45, 'F')); }); it('la cascata del vo2max per una sessione con solo f_vo2max resta uguale', () => { const m = new Map([['f_vo2max', 45]]); const r = sottoDominiFisici(m, 40, 'M'); expect(r.vo2max).toBe(F.scoreVo2max(45, 40, 'M')); }); }); describe('un lato solo, senza storico, non da punteggio', () => { it('handgrip solo destro: nessun punteggio (nessuna media possibile, nessuno storico)', () => { const m = new Map([['f_handgrip_dx_kg', 45]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.handgrip).toBeUndefined(); }); it('flamingo occhi aperti solo sinistro: nessun punteggio', () => { const m = new Map([['f_flamingo_oa_sx', 4]]); const r = sottoDominiFisici(m, 38, 'M'); expect(r.flamingo).toBeUndefined(); }); it('back_scratch solo sinistro: nessun punteggio', () => { const m = new Map([['f_back_scratch_sx_cm', -6]]); const r = sottoDominiFisici(m, 45, 'F'); expect(r.back_scratch).toBeUndefined(); }); }); describe('la regola dei gruppi sceglie per recenza, non solo per ordine di priorita', () => { it('pushup a gennaio + panca a marzo: vale panca (piu recente)', () => { const m = new Map([ ['f_pushup_rip', 24], ['f_panca_bil_carico_kg', 60], ['f_panca_bil_rip', 8], ['f_peso_kg', 80], ]); // recenza: posizione 0 = piu recente. Panca (marzo) piu recente del pushup (gennaio). const recenza = new Map([ ['f_panca_bil_carico_kg', 0], ['f_panca_bil_rip', 0], ['f_pushup_rip', 1], ]); const r = sottoDominiFisici(m, 38, 'M', recenza); expect(r.spinta).toBe(F.scoreSollevamentoSuPeso(60, 80, 8, F.TIERS_BENCH_M, F.TIERS_BENCH_F, 'M')); }); it('panca a gennaio + pushup a marzo: vale pushup (piu recente) — il contrario del caso sopra', () => { const m = new Map([ ['f_pushup_rip', 24], ['f_panca_bil_carico_kg', 60], ['f_panca_bil_rip', 8], ['f_peso_kg', 80], ]); const recenza = new Map([ ['f_pushup_rip', 0], ['f_panca_bil_carico_kg', 1], ['f_panca_bil_rip', 1], ]); const r = sottoDominiFisici(m, 38, 'M', recenza); expect(r.spinta).toBe(F.scorePushup(24, 38, 'M')); }); it('alternativa incompleta ma piu recente (sit-to-stand senza altezza) non entra: vale lo squat precedente', () => { const m = new Map([ ['f_sit_to_stand_rip', 14], // manca f_altezza_cm: bmi non calcolabile, alternativa incompleta ['f_squat_carico_kg', 100], ['f_squat_rip', 5], ['f_peso_kg', 80], ]); // sit-to-stand risulterebbe il piu recente SE fosse completo — ma non lo e', quindi // resta escluso dal gruppo a prescindere dalla recenza (regola: solo le alternative // complete entrano). const recenza = new Map([ ['f_sit_to_stand_rip', 0], ['f_squat_carico_kg', 1], ['f_squat_rip', 1], ]); const r = sottoDominiFisici(m, 65, 'M', recenza); expect(r.arti_inferiori).toBe(F.scoreSollevamentoSuPeso(100, 80, 5, F.TIERS_SQUAT_M, F.TIERS_SQUAT_F, 'M')); }); }); describe('i test "solo registrati" (nessuna tabella nel manuale) non producono chiavi', () => { it('panca manubri, trazioni, plank ginocchia, 6MWT, flamingo occhi chiusi: nessuna chiave nel risultato', () => { const m = new Map([ ['f_panca_man_carico_kg', 12], ['f_panca_man_rip', 10], ['f_trazioni_rip', 8], ['f_plank_ginocchia_sec', 90], ['f_6mwt_m', 550], ['f_flamingo_oc_dx', 10], ['f_flamingo_oc_sx', 12], ['f_mutt_fc_2min_bpm', 110], ['f_fc_riposo_bpm', 60], ]); const r = sottoDominiFisici(m, 38, 'M'); // nessuna di queste misure alimenta una funzione di scoring: il risultato resta {} expect(r).toEqual({}); }); });