From d22f88ad71b979076e94954f5d91ec40d6d8bc9c Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Sat, 22 Aug 2026 10:07:28 +0200 Subject: [PATCH] longevity: la prova sul profilo con una sola area misurata Task 6 - la prova sul caso reale documentato dal cliente (dati sintetici, nessuna persona reale). Il test smontava un difetto nel task 5: RisultatoSessione esponeva fitnessAge come Punteggio invece che number|null (come lo produce calcolaFitnessAge in cascata.ts), quindi un fitnessAge insufficiente non era mai `null`. Corretto in motore/index.ts, con fitnessAgeCopertura a fianco per non perdere l'informazione che salvaScore scrive in `score`. --- src/lib/longevity/motore/index.ts | 22 +++++++---- tests/longevity/motore-caso-reale.test.ts | 46 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 tests/longevity/motore-caso-reale.test.ts diff --git a/src/lib/longevity/motore/index.ts b/src/lib/longevity/motore/index.ts index f68bb12..cd53bd3 100644 --- a/src/lib/longevity/motore/index.ts +++ b/src/lib/longevity/motore/index.ts @@ -65,11 +65,19 @@ export function applicaCurva(voce: VoceRegistro, valore: number): number | null } } -/** Esito del calcolo di una sessione: i sette assi, i tre macro-score, la Fitness Age. */ +/** + * Esito del calcolo di una sessione: i sette assi, i tre macro-score, la Fitness Age. + * `fitnessAge` è `number | null` — non `Punteggio` come gli assi e i macro-score — perché + * e' cosi' che la produce `calcolaFitnessAge` in `cascata.ts` (l'oracolo del cliente la + * tratta come un numero singolo, non come un contenitore stato/valore): `null` quando il + * composito e' insufficiente. `fitnessAgeCopertura` porta la copertura del composito, che + * altrimenti andrebbe persa: serve a `salvaScore` per scrivere la riga 'fitness_age'. + */ export type RisultatoSessione = { assi: Record; macro: Record; - fitnessAge: Punteggio; + fitnessAge: number | null; + fitnessAgeCopertura: number; modelVersion: string; questVersion: string | null; }; @@ -152,12 +160,9 @@ export function calcolaSessione(db: Database.Database, sessioneId: number): Risu pesiDi(db, MODEL_VERSION, 'fitness_age', 'fitness_age'), vociFitnessAge ); - const fitnessAgePunteggio: Punteggio = composito.stato === 'insufficiente' - ? { stato: 'insufficiente', copertura: composito.copertura } - : { stato: 'ok', valore: fitnessAge as number, copertura: composito.copertura }; return { - assi, macro, fitnessAge: fitnessAgePunteggio, + assi, macro, fitnessAge, fitnessAgeCopertura: composito.copertura, modelVersion: MODEL_VERSION, questVersion: sessione.quest_version, }; } @@ -189,7 +194,10 @@ export function salvaScore(db: Database.Database, sessioneId: number, risultato: const tx = db.transaction(() => { for (const [nome, p] of Object.entries(risultato.assi)) riga('asse', nome, p); for (const [nome, p] of Object.entries(risultato.macro)) riga('macro', nome, p); - riga('fitness_age', 'fitness_age', risultato.fitnessAge); + const fitnessAgePunteggio: Punteggio = risultato.fitnessAge === null + ? { stato: 'insufficiente', copertura: risultato.fitnessAgeCopertura } + : { stato: 'ok', valore: risultato.fitnessAge, copertura: risultato.fitnessAgeCopertura }; + riga('fitness_age', 'fitness_age', fitnessAgePunteggio); }); tx(); } diff --git a/tests/longevity/motore-caso-reale.test.ts b/tests/longevity/motore-caso-reale.test.ts new file mode 100644 index 0000000..1e00659 --- /dev/null +++ b/tests/longevity/motore-caso-reale.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect } from 'vitest'; +import { createLongevityDb } from '../../src/lib/longevity/db'; +import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro'; +import { apriSessione, registraMisure } from '../../src/lib/longevity/misure'; +import { calcolaSessione } from '../../src/lib/longevity/motore'; + +/** + * Riproduce la situazione descritta nella documentazione del cliente: una persona di cui + * si conosce solo la composizione corporea, misurata dalla Wellness Tower. Un asse pieno, + * tutti gli altri scoperti. È il caso in cui un motore ingenuo mostrerebbe sei zeri. + * Dati sintetici: nessuna persona reale. + */ +describe('un profilo con una sola area misurata', () => { + it('mostra l asse coperto e dichiara insufficienti gli altri sei', () => { + const db = createLongevityDb(':memory:'); + seedRegistro(db); + seedPesi(db, MODEL_VERSION); + db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','M')`).run(); + + // i tre test di composizione esistono nel registro solo dopo il piano degli import: + // finche non ci sono, questo test dimostra il comportamento con zero misure fisiche + const s = apriSessione(db, { client_code: 'ISL-0001', data: '2026-08-22', tipo: 'checkup', eta_alla_data: 38 }); + registraMisure(db, s, 'questionario', []); + + const r = calcolaSessione(db, s); + const insufficienti = Object.values(r.assi).filter((a) => a.stato === 'insufficiente').length; + expect(insufficienti).toBe(7); + expect(r.fitnessAge).toBeNull(); + }); + + it('nessun asse insufficiente porta con se un valore da mostrare per sbaglio', () => { + const db = createLongevityDb(':memory:'); + seedRegistro(db); + seedPesi(db, MODEL_VERSION); + db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0002','F')`).run(); + const s = apriSessione(db, { client_code: 'ISL-0002', data: '2026-08-22', tipo: 'checkup', eta_alla_data: 35 }); + registraMisure(db, s, 'questionario', []); + + const r = calcolaSessione(db, s); + for (const [nome, asse] of Object.entries(r.assi)) { + if (asse.stato === 'insufficiente') { + expect((asse as { valore?: number }).valore, `${nome} espone un valore`).toBeUndefined(); + } + } + }); +});