longevity: test unitari e di sessione per la regola dei gruppi (#13)
motore-fisici-gruppi.test.ts (nuovo): un caso per ogni alternativa che da punteggio (handgrip/spinta/arti_inferiori/vo2max/hrr/flamingo/back_scratch, media dx/sx e storico), un lato solo -> nessun punteggio, recenza fra alternative (pushup/panca in entrambe le direzioni), alternativa piu recente ma incompleta esclusa (sit-to-stand senza altezza), i test "solo registrati" non producono chiavi. motore-fisici-sessione.test.ts: due nuovi casi end-to-end su DB (due sessioni, due test diversi per la stessa sotto-metrica) che confermano che lo score usa il test piu recente, in entrambe le direzioni. Refs #13 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// 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({});
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { apriSessione, registraMisure } from '../../src/lib/longevity/misure';
|
||||
import { calcolaSessione, sottoDominiDellaSessione, misureValideAlla } from '../../src/lib/longevity/motore';
|
||||
import { sottoDominiFisici } from '../../src/lib/longevity/motore/fisici';
|
||||
import { campiDelQuestionario } from '../../src/lib/longevity/vista';
|
||||
import * as F from '../../src/lib/longevity/motore/test-fisici'; // per i valori attesi della regola dei gruppi (#13)
|
||||
|
||||
function dbPronto(clientCode: string, sesso: 'M' | 'F' = 'M') {
|
||||
const db = createLongevityDb(':memory:');
|
||||
@@ -232,6 +233,40 @@ describe('un check-up e un questionario sono sessioni separate, ma un cliente e
|
||||
});
|
||||
});
|
||||
|
||||
describe('la regola dei gruppi (#13) su un DB vero: due sessioni, lo score usa il test piu recente', () => {
|
||||
it('pushup a gennaio, panca a marzo: il check-up di marzo usa la panca', () => {
|
||||
const db = dbPronto('ISL-0030', 'M');
|
||||
const sGennaio = apriSessione(db, { client_code: 'ISL-0030', data: '2026-01-15', tipo: 'checkup', eta_alla_data: 38 });
|
||||
registraMisure(db, sGennaio, 'manuale', [
|
||||
{ test_id: 'f_peso_kg', valore_num: 80 },
|
||||
{ test_id: 'f_pushup_rip', valore_num: 24 },
|
||||
]);
|
||||
const sMarzo = apriSessione(db, { client_code: 'ISL-0030', data: '2026-03-10', tipo: 'checkup', eta_alla_data: 38 });
|
||||
registraMisure(db, sMarzo, 'manuale', [
|
||||
{ test_id: 'f_panca_bil_carico_kg', valore_num: 60 }, { test_id: 'f_panca_bil_rip', valore_num: 8 },
|
||||
]);
|
||||
|
||||
// misureValideAlla porta avanti ANCHE il pushup di gennaio (mai sovrascritto): al check-up
|
||||
// di marzo il gruppo "spinta" vede entrambe le alternative complete, e vince la piu recente.
|
||||
const sd = sottoDominiDellaSessione(db, sMarzo);
|
||||
expect(sd.spinta).toBeCloseTo(F.scoreSollevamentoSuPeso(60, 80, 8, F.TIERS_BENCH_M, F.TIERS_BENCH_F, 'M'), 5);
|
||||
});
|
||||
|
||||
it('panca a gennaio, pushup a marzo: il check-up di marzo usa il pushup (il contrario del caso sopra)', () => {
|
||||
const db = dbPronto('ISL-0031', 'M');
|
||||
const sGennaio = apriSessione(db, { client_code: 'ISL-0031', data: '2026-01-15', tipo: 'checkup', eta_alla_data: 38 });
|
||||
registraMisure(db, sGennaio, 'manuale', [
|
||||
{ test_id: 'f_peso_kg', valore_num: 80 },
|
||||
{ test_id: 'f_panca_bil_carico_kg', valore_num: 60 }, { test_id: 'f_panca_bil_rip', valore_num: 8 },
|
||||
]);
|
||||
const sMarzo = apriSessione(db, { client_code: 'ISL-0031', data: '2026-03-10', tipo: 'checkup', eta_alla_data: 38 });
|
||||
registraMisure(db, sMarzo, 'manuale', [{ test_id: 'f_pushup_rip', valore_num: 24 }]);
|
||||
|
||||
const sd = sottoDominiDellaSessione(db, sMarzo);
|
||||
expect(sd.spinta).toBeCloseTo(F.scorePushup(24, 38, 'M'), 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('campiDelQuestionario resta un modulo di sole domande', () => {
|
||||
it('nessun campo f_* (test fisico) compare fra i campi del questionario', () => {
|
||||
const db = createLongevityDb(':memory:');
|
||||
|
||||
Reference in New Issue
Block a user