Files
InsanityLab-web/apps/sito/tests/longevity/referto.test.ts
T
AdrianoDev 510d7ca4ec Struttura: il sito esistente si sposta sotto apps/sito
Primo passo della separazione sito / piattaforme decisa da Adriano il 03/09:
due applicazioni autosufficienti, senza workspace di root, cosi' che ognuna
resti un progetto Astro completo ed estraibile.

Qui non cambia niente del codice: solo la radice. I pattern del .gitignore
che avevano una barra erano ancorati alla radice e dopo lo spostamento non
avrebbero piu' coperto niente (data/*.db*, uploads/*): resi validi a
qualunque profondita'.

Suite: 45 file, 347 test verdi prima e dopo lo spostamento.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
2026-09-03 11:05:54 +00:00

125 lines
6.2 KiB
TypeScript

// tests/longevity/referto.test.ts
import { describe, it, expect } from 'vitest';
import { readFileSync, readdirSync } 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('espone la copertura della Fitness Age anche quando è insufficiente (serve alla nota in pagina)', () => {
// conCompilazione() non porta nessuna misura del composito Fitness Age (cardio,
// handgrip, hrv, forza_resto, composizione, stabilita: nessuno di questi sotto-domini
// ha dati in questa sessione, solo il questionario), quindi fitnessAge è null — ma
// PRIMA di questa correzione refertoDi scartava anche la copertura insieme al valore:
// la pagina non aveva modo di dire "41%", solo di dire "manca".
const { db, s } = conCompilazione();
const referto = refertoDi(db, s);
expect(referto.fitnessAge).toBeNull();
expect(referto.fitnessAgeCopertura).toBe(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', () => {
// Scandisce TUTTE le pagine sotto src/pages/longevity/, non un elenco scritto qui:
// un elenco di due nomi non protegge una pagina nuova, che passerebbe indisturbata
// anche con una query dentro (verificato: una prova-query.astro con un
// db.prepare('SELECT 1') non veniva vista dalla versione precedente di questo test).
const dir = join(process.cwd(), 'src/pages/longevity');
const pagine = (readdirSync(dir, { recursive: true }) as string[])
.filter((f) => f.endsWith('.astro'));
expect(pagine.length).toBeGreaterThan(0); // se la cartella si svuota, il test non deve tacere per errore
for (const f of pagine) {
const percorso = `src/pages/longevity/${f}`;
const src = readFileSync(join(dir, f), 'utf8');
expect(src, `${percorso} contiene SQL`).not.toMatch(/\bSELECT\b|\.prepare\(/i);
}
});
});