// tests/longevity/progressione.test.ts // L'andamento nel tempo: progressioneDi deve leggere SOLO ciò che è congelato in `score` // (mai un ricalcolo, stessa regola di refertoDi) e marcare correttamente dove il modo di // calcolare è cambiato (quest_version/model_version), senza farsi confondere da un // check-up in mezzo a due questionari (quest_version NULL per un check-up). import { describe, it, expect } from 'vitest'; import { createLongevityDb } from '../../src/lib/longevity/db'; import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro'; import { salvaDalForm } from '../../src/lib/longevity/vista'; import { apriSessione } from '../../src/lib/longevity/misure'; import { calcolaSessione, salvaScore, leggiScore } from '../../src/lib/longevity/motore'; import { progressioneDi } from '../../src/lib/longevity/progressione'; const RISPOSTE_COMPLETE = { q_ore_sonno: 7.5, q_min_addorm: 15, q_risvegli: 0, q_riposato: 8, q_caffeina: 1, q_sonnolenza_diurna: 1, q_energia_media: 7, q_esaurimento: 0, q_calo_pomeridiano: 3, q_sopraffatto: 1, q_controllo: 8, q_sicurezza_gestione: 8, q_tensione: 1, q_pensieri_lavoro: 0.5, q_attivita: 5, q_alimentazione: 8, q_sigarette: 0, q_alcol_life: 2, q_luce: 2, q_schermi: 10, }; 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; } /** Una sessione di check-up, calcolata e congelata come farebbe l'operatore in sala: * nessuna domanda del questionario, quindi `quest_version` resta NULL (vedi db.ts). */ function aggiungiCheckup(db: ReturnType, data: string) { const sessioneId = apriSessione(db, { client_code: 'ISL-0001', data, tipo: 'checkup', eta_alla_data: 35 }); salvaScore(db, sessioneId, calcolaSessione(db, sessioneId)); return sessioneId; } /** Un questionario compilato per intero, via salvaDalForm — la stessa funzione che * chiama l'endpoint reale (vedi salvataggio-e2e.test.ts): calcola e congela da sola. */ function aggiungiQuestionario(db: ReturnType, data: string) { const esito = salvaDalForm(db, { client_code: 'ISL-0001', data, eta: 35, risposte: RISPOSTE_COMPLETE, consensoSanitario: 'Si', liberi: {}, }); if (!esito.ok) throw new Error(esito.errore); return esito.sessioneId; } describe('progressioneDi: due questionari e un check-up', () => { it('produce tre punti, in ordine cronologico', () => { const db = dbPronto(); const s1 = aggiungiQuestionario(db, '2026-01-10'); const s2 = aggiungiCheckup(db, '2026-02-10'); const s3 = aggiungiQuestionario(db, '2026-03-10'); const punti = progressioneDi(db, 'ISL-0001'); expect(punti.length).toBe(3); expect(punti.map((p) => p.sessioneId)).toEqual([s1, s2, s3]); expect(punti.map((p) => p.data)).toEqual(['2026-01-10', '2026-02-10', '2026-03-10']); expect(punti[0].tipo).toBe('questionario'); expect(punti[1].tipo).toBe('checkup'); }); it('i valori coincidono con leggiScore, mai un ricalcolo', () => { const db = dbPronto(); const s1 = aggiungiQuestionario(db, '2026-01-10'); const punti = progressioneDi(db, 'ISL-0001'); const punteggi = leggiScore(db, s1); for (const [nome, p] of Object.entries(punteggi.assi)) { expect(punti[0].assi[nome]).toBe(p.stato === 'ok' ? p.valore : null); } for (const [nome, p] of Object.entries(punteggi.macro)) { expect(punti[0].macro[nome]).toBe(p.stato === 'ok' ? p.valore : null); } expect(punti[0].fitnessAge).toBe(punteggi.fitnessAge?.stato === 'ok' ? punteggi.fitnessAge.valore : null); }); it('un asse senza dati sufficienti resta null, mai un numero inventato', () => { const db = dbPronto(); aggiungiQuestionario(db, '2026-01-10'); const punti = progressioneDi(db, 'ISL-0001'); // 'Forza & Struttura' non ha nessuna misura fisica in questa sessione (solo questionario). expect(punti[0].assi['Forza & Struttura']).toBeNull(); }); it('quest_version NULL per il check-up', () => { const db = dbPronto(); aggiungiQuestionario(db, '2026-01-10'); aggiungiCheckup(db, '2026-02-10'); const punti = progressioneDi(db, 'ISL-0001'); expect(punti[1].questVersion).toBeNull(); }); it('nessun cambio di versione: cambioVersione resta null su ogni punto dopo il primo', () => { const db = dbPronto(); aggiungiQuestionario(db, '2026-01-10'); aggiungiCheckup(db, '2026-02-10'); aggiungiQuestionario(db, '2026-03-10'); const punti = progressioneDi(db, 'ISL-0001'); expect(punti[0].cambioVersione).toBeNull(); // primo punto: mai un "prima" da confrontare expect(punti[1].cambioVersione).toBeNull(); // il check-up in mezzo expect(punti[2].cambioVersione).toBeNull(); // il secondo questionario, stessa versione }); it('un check-up fra due questionari alla stessa versione non produce un cambio', () => { // Stesso scenario del test sopra, isolato: qui si verifica in particolare che il // check-up (quest_version NULL) non "nasconda" né inventi un cambio fra i due // questionari v1.0 che lo circondano. const db = dbPronto(); aggiungiQuestionario(db, '2026-01-10'); aggiungiCheckup(db, '2026-02-10'); const s3 = aggiungiQuestionario(db, '2026-03-10'); const punti = progressioneDi(db, 'ISL-0001'); const puntoCheckup = punti.find((p) => p.tipo === 'checkup')!; const puntoSecondoQuest = punti.find((p) => p.sessioneId === s3)!; expect(puntoCheckup.cambioVersione).toBeNull(); expect(puntoSecondoQuest.cambioVersione).toBeNull(); }); it('un cambio di quest_version produce il testo del cambio', () => { const db = dbPronto(); aggiungiQuestionario(db, '2026-01-10'); const s3 = aggiungiQuestionario(db, '2026-03-10'); // Nessuna seconda versione delle domande esiste davvero oggi (QUEST_VERSION è una sola // costante): si simula il cambio scrivendo direttamente la riga congelata, come farebbe // un domani un vero v1.1 del questionario. db.prepare(`UPDATE score SET quest_version = 'v1.1' WHERE sessione_id = ?`).run(s3); const punti = progressioneDi(db, 'ISL-0001'); const punto = punti.find((p) => p.sessioneId === s3)!; expect(punto.cambioVersione).toMatch(/questionario v1\.0 → v1\.1/); }); it('una sessione senza righe in score resta esclusa dalla progressione', () => { const db = dbPronto(); aggiungiQuestionario(db, '2026-01-10'); // Sessione aperta ma mai calcolata: nessuna riga in score (consenso non dato, o un // calcolo mai lanciato — non un caso teorico, vedi il commento in salvaDalForm). apriSessione(db, { client_code: 'ISL-0001', data: '2026-02-01', tipo: 'checkup', eta_alla_data: 35 }); const punti = progressioneDi(db, 'ISL-0001'); expect(punti.length).toBe(1); }); });