// 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); } }); });