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`.
This commit is contained in:
2026-08-22 10:07:28 +02:00
parent ac8b377f84
commit d22f88ad71
2 changed files with 61 additions and 7 deletions
+15 -7
View File
@@ -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 = { export type RisultatoSessione = {
assi: Record<string, Punteggio>; assi: Record<string, Punteggio>;
macro: Record<string, Punteggio>; macro: Record<string, Punteggio>;
fitnessAge: Punteggio; fitnessAge: number | null;
fitnessAgeCopertura: number;
modelVersion: string; modelVersion: string;
questVersion: string | null; questVersion: string | null;
}; };
@@ -152,12 +160,9 @@ export function calcolaSessione(db: Database.Database, sessioneId: number): Risu
pesiDi(db, MODEL_VERSION, 'fitness_age', 'fitness_age'), pesiDi(db, MODEL_VERSION, 'fitness_age', 'fitness_age'),
vociFitnessAge vociFitnessAge
); );
const fitnessAgePunteggio: Punteggio = composito.stato === 'insufficiente'
? { stato: 'insufficiente', copertura: composito.copertura }
: { stato: 'ok', valore: fitnessAge as number, copertura: composito.copertura };
return { return {
assi, macro, fitnessAge: fitnessAgePunteggio, assi, macro, fitnessAge, fitnessAgeCopertura: composito.copertura,
modelVersion: MODEL_VERSION, questVersion: sessione.quest_version, modelVersion: MODEL_VERSION, questVersion: sessione.quest_version,
}; };
} }
@@ -189,7 +194,10 @@ export function salvaScore(db: Database.Database, sessioneId: number, risultato:
const tx = db.transaction(() => { 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.assi)) riga('asse', nome, p);
for (const [nome, p] of Object.entries(risultato.macro)) riga('macro', 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(); tx();
} }
+46
View File
@@ -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();
}
}
});
});