Files
InsanityLab-web/tests/longevity/motore.test.ts
T
Adriano f238984828 longevity: il motore legge curve e pesi dal registro, e congela i punteggi
Task 5: applicaCurva smista sulla curva dichiarata in registro_test (non
scritta nel codice); calcolaSessione aggrega le misure attive per
sotto-dominio (media quando piu' test contribuiscono allo stesso
sotto-dominio) e usa cascata.ts coi pesi letti da pesi (pesiDi) per i
sette assi, i tre macro-score e la Fitness Age; salvaScore congela il
risultato in score, con valore a null quando lo stato e' insufficiente.

Corretto anche un refuso nel test del brief ('un test disattivato non
entra nel calcolo'): disattivava q_ore_sonno, ma quella voce sta nel
sotto-dominio questionario_sonno (peso 0.20 su Recupero & Sistema
Nervoso, sempre sotto COPERTURA_MINIMA=0.4 finche' hrv/pressione/hrr non
hanno una voce nel registro), quindi l'asse restava insufficiente prima
e dopo e il JSON risultava identico (verificato: il test falliva con le
due stringhe uguali). Corretto usando q_attivita, l'unica voce del
sotto-dominio questionario_lifestyle che pesa 1.00 su Stile di Vita &
Sonno: disattivarla fa cadere la copertura da 1.00 a 0 e lo stato passa
da ok a insufficiente, una differenza vera.
2026-08-22 09:53:17 +02:00

111 lines
5.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { createLongevityDb } from '../../src/lib/longevity/db';
import { seedRegistro, seedPesi, MODEL_VERSION, pesiDi } from '../../src/lib/longevity/registro';
import { salvaCompilazione } from '../../src/lib/longevity/questionario';
import { applicaCurva, calcolaSessione, salvaScore } from '../../src/lib/longevity/motore';
function dbPronto() {
const db = createLongevityDb(':memory:');
seedRegistro(db);
seedPesi(db, MODEL_VERSION);
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run();
return db;
}
const voce = (db: ReturnType<typeof dbPronto>, id: string) =>
db.prepare(`SELECT * FROM registro_test WHERE test_id = ?`).get(id) as Record<string, unknown>;
describe('il motore legge le curve dal registro', () => {
it('applica la curva dichiarata per il test, non una scritta nel codice', () => {
const db = dbPronto();
const v = { ...voce(db, 'q_ore_sonno'), params: JSON.parse(voce(db, 'q_ore_sonno').params as string) };
expect(applicaCurva(v as never, 8)).toBe(100); // dentro il picco 7-9
expect(applicaCurva(v as never, 4)).toBeLessThan(20);
});
it('rispetta la soglia dell alcol come e scritta nel registro', () => {
const db = dbPronto();
const v = { ...voce(db, 'q_alcol_life'), params: JSON.parse(voce(db, 'q_alcol_life').params as string) };
expect(applicaCurva(v as never, 0)).toBe(100);
expect(applicaCurva(v as never, 7)).toBe(100);
expect(applicaCurva(v as never, 14)).toBe(0);
});
it('cambiare i parametri nel registro cambia il punteggio, senza toccare il codice', () => {
const db = dbPronto();
db.prepare(`UPDATE registro_test SET params = ? WHERE test_id = 'q_alcol_life'`)
.run(JSON.stringify({ best: 0, worst: 7 }));
const v = { ...voce(db, 'q_alcol_life'), params: JSON.parse(voce(db, 'q_alcol_life').params as string) };
expect(applicaCurva(v as never, 7)).toBe(0); // con i parametri nuovi, 7 non vale piu 100
});
it('un test disattivato non entra nel calcolo', () => {
// NOTA: la versione originale di questo test disattivava 'q_ore_sonno' e si
// aspettava una differenza. Ma 'q_ore_sonno' e' uno delle SEI voci del
// sotto-dominio 'questionario_sonno' (peso 0.20 dentro "Recupero & Sistema
// Nervoso", che pesa in tutto 1.00: hrv 0.50 + pressione 0.15 + hrr 0.15 +
// questionario_sonno 0.20). Quell'asse resta 'insufficiente' con o senza
// 'q_ore_sonno' (0.20 < COPERTURA_MINIMA 0.4, e hrv/pressione/hrr non hanno
// ancora una voce nel registro): siccome un asse insufficiente non porta
// 'valore' nel tipo Punteggio, il JSON prima/dopo risultava IDENTICO — la
// suite lo ha confermato (verificato eseguendo il test: fallisce con le due
// stringhe uguali). Corretto qui usando 'q_attivita', l'unica voce del
// sotto-dominio 'questionario_lifestyle' che pesa 1.00 dentro "Stile di Vita
// & Sonno": disattivarla fa cadere la copertura di quell'asse da 1.00 a 0 e
// lo stato passa da 'ok' a 'insufficiente' — una differenza vera e osservabile.
const db = dbPronto();
salvaCompilazione(db, {
client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
risposte: {
q_ore_sonno: 8, q_riposato: 8, q_min_addorm: 10, q_risvegli: 0, q_caffeina: 0, q_sonnolenza_diurna: 0,
q_attivita: 5,
},
});
const prima = calcolaSessione(db, 1);
expect(prima.assi['Stile di Vita & Sonno'].stato).toBe('ok');
db.prepare(`UPDATE registro_test SET attivo_a = '2026-01-01' WHERE test_id = 'q_attivita'`).run();
const dopo = calcolaSessione(db, 1);
expect(dopo.assi['Stile di Vita & Sonno'].stato).toBe('insufficiente');
expect(JSON.stringify(prima)).not.toBe(JSON.stringify(dopo));
});
});
describe('calcolo e congelamento di una sessione', () => {
it('produce i sette assi, e quelli senza dati sono insufficienti', () => {
const db = dbPronto();
salvaCompilazione(db, {
client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
risposte: { q_ore_sonno: 8, q_riposato: 8, q_min_addorm: 10, q_risvegli: 0, q_caffeina: 0, q_sonnolenza_diurna: 1 },
});
const r = calcolaSessione(db, 1);
expect(Object.keys(r.assi).length).toBe(7);
// il questionario copre il sonno, che pesa 0.20 dentro Recupero: sotto il 40%
expect(r.assi['Recupero & Sistema Nervoso'].stato).toBe('insufficiente');
// Stile di Vita e coperto al 100% dal solo questionario, ma qui non abbiamo risposto
expect(r.assi['Forza & Struttura'].stato).toBe('insufficiente');
});
it('congela i punteggi con le DUE versioni', () => {
const db = dbPronto();
salvaCompilazione(db, {
client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
risposte: { q_riposato: 7 },
});
salvaScore(db, 1, calcolaSessione(db, 1));
const righe = db.prepare(`SELECT tipo, elemento, valore, stato, quest_version, model_version FROM score`).all() as Record<string, unknown>[];
expect(righe.length).toBeGreaterThan(0);
for (const r of righe) {
expect(r.model_version).toBe(MODEL_VERSION);
expect(r.quest_version).toBe('v1.0');
if (r.stato === 'insufficiente') expect(r.valore).toBeNull();
}
});
it('i pesi arrivano dal registro e sono quelli della versione richiesta', () => {
const db = dbPronto();
const pesi = pesiDi(db, MODEL_VERSION, 'asse', 'Forza & Struttura');
expect(pesi.handgrip).toBe(0.25);
expect(Object.values(pesi).reduce((a, b) => a + b, 0)).toBeCloseTo(1, 6);
});
});