Files
InsanityLab-web/tests/longevity/referto.test.ts
T
Adriano 6630be7a02 longevity: la prova sul congelamento ora discrimina davvero il ricalcolo
Il peso mutato prima era l'unico sotto-dominio del suo asse (questionario_lifestyle,
1.00 in Stile di Vita & Sonno): in una media pesata a un solo elemento disponibile
il peso si semplifica sia al numeratore sia al denominatore, quindi il test restava
verde anche se refertoDi avesse ricalcolato invece di leggere gli score congelati.
Sostituita con il peso di un sotto-dominio ASSENTE in un asse a piu' elementi con
copertura parziale non nulla (hrv in Recupero & Sistema Nervoso): li' un ricalcolo
sposta davvero la copertura oltre soglia. Verificato rosso col ricalcolo forzato,
verde ripristinato.
2026-08-22 14:32:43 +02:00

104 lines
4.9 KiB
TypeScript

// tests/longevity/referto.test.ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { createLongevityDb } from '../../src/lib/longevity/db';
import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro';
import { salvaCompilazione } from '../../src/lib/longevity/questionario';
import { calcolaSessione, salvaScore } from '../../src/lib/longevity/motore';
import { refertoDi } from '../../src/lib/longevity/vista';
function conCompilazione() {
const db = createLongevityDb(':memory:');
seedRegistro(db); seedPesi(db, MODEL_VERSION);
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run();
const s = salvaCompilazione(db, {
client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
risposte: { q_attivita: 5, q_alimentazione: 8, q_sigarette: 0, q_alcol_life: 2, q_luce: 2, q_schermi: 10 },
});
salvaScore(db, s, calcolaSessione(db, s));
return { db, s };
}
/**
* Come conCompilazione, con IN PIU' una risposta di questionario_sonno (q_riposato).
* Serve solo alla prova sul congelamento, che ha bisogno di un asse con PIU' sotto-domini
* pesati e copertura parziale non nulla — vedi il commento sopra la mutazione qui sotto per
* il perche' conCompilazione da sola non basta.
*/
function conCoperturaParziale() {
const db = createLongevityDb(':memory:');
seedRegistro(db); seedPesi(db, MODEL_VERSION);
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run();
const s = salvaCompilazione(db, {
client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
risposte: {
q_attivita: 5, q_alimentazione: 8, q_sigarette: 0, q_alcol_life: 2, q_luce: 2, q_schermi: 10,
q_riposato: 8,
},
});
salvaScore(db, s, calcolaSessione(db, s));
return { db, s };
}
describe('il referto del cliente', () => {
it('restituisce i sette assi', () => {
const { db, s } = conCompilazione();
expect(refertoDi(db, s).assi.length).toBe(7);
});
it('un asse senza dati NON porta un numero', () => {
const { db, s } = conCompilazione();
const forza = refertoDi(db, s).assi.find((a) => a.nome === 'Forza & Struttura')!;
expect(forza.valore).toBeNull();
});
it('e dice cosa manca per completarlo, invece di lasciare un buco', () => {
const { db, s } = conCompilazione();
const forza = refertoDi(db, s).assi.find((a) => a.nome === 'Forza & Struttura')!;
expect(forza.mancano.length).toBeGreaterThan(0);
});
it('un asse coperto porta il suo numero', () => {
const { db, s } = conCompilazione();
const stile = refertoDi(db, s).assi.find((a) => a.nome === 'Stile di Vita & Sonno')!;
expect(stile.valore).not.toBeNull();
expect(stile.mancano).toEqual([]);
});
it('legge i punteggi congelati, non li ricalcola al volo', () => {
const { db, s } = conCoperturaParziale();
const prima = JSON.stringify(refertoDi(db, s));
// ATTENZIONE alla scelta della mutazione: non una qualunque, una che DISCRIMINA
// davvero un ricalcolo da una lettura congelata.
//
// 'Recupero & Sistema Nervoso' pesa QUATTRO sotto-domini (hrv 0.50, pressione 0.15,
// hrr 0.15, questionario_sonno 0.20); questa sessione ne misura UNO solo
// (questionario_sonno, via q_riposato in conCoperturaParziale) — copertura vera
// 0.20/1.00=0.20, sotto COPERTURA_MINIMA (0.4): l'asse e' 'insufficiente', ma con
// disponibili.length===1, non 0 — la formula del peso viene davvero eseguita, non
// bypassata dalla scorciatoia "nessun dato disponibile" di aggrega() in cascata.ts.
// Azzerando il peso di 'hrv' (un sotto-dominio ASSENTE) il totale scende a 0.50 e la
// copertura vera diventa 0.20/0.50=0.40: attraversa la soglia, l'asse passerebbe da
// insufficiente a ok SE il referto ricalcolasse (provato: vedi task-3-report.md).
//
// Una mutazione come questa NON discrimina, ed e' un errore da non ripetere:
// il peso dell'UNICO sotto-dominio di un asse a un solo elemento (es.
// 'questionario_lifestyle', 1.00 in 'Stile di Vita & Sonno') si semplifica sempre,
// sia al numeratore sia al denominatore della media pesata — cambiarlo non altera
// ne' il valore ne' la copertura, quindi il test resterebbe verde anche se
// refertoDi ricalcolasse davvero (era il bug di questo stesso test, corretto qui).
db.prepare(
`UPDATE pesi SET peso = 0 WHERE model_version = ? AND livello='asse' AND contenitore='Recupero & Sistema Nervoso' AND elemento='hrv'`
).run(MODEL_VERSION);
expect(JSON.stringify(refertoDi(db, s))).toBe(prima);
});
it('nessuna pagina interroga il database da sola', () => {
for (const f of ['src/pages/longevity/io.astro', 'src/pages/longevity/questionario.astro']) {
const src = readFileSync(join(process.cwd(), f), 'utf8');
expect(src, `${f} contiene SQL`).not.toMatch(/\bSELECT\b|\.prepare\(/i);
}
});
});