6301f521fd
Campus, Stress Index e Longevity escono dal sito e diventano apps.insanitylab.it, con utenti, sessioni, login e database propri. Deciso da Adriano il 03/09/2026: una repo sola, due applicazioni autosufficienti, cosi' che ognuna resti estraibile. Misura che ha guidato il taglio: le tre aree condividevano col sito SOLO l'identita' - ~170 righe fra auth.ts, middleware.ts e safe-next.ts. Layout, librerie e dati erano gia' disgiunti. Lato piattaforme: - ruoli piattaforme/cliente/trainer/admin, senza quelli redazionali - canAccessPath con default CHIUSO (nel sito era aperto: li' le rotte non elencate erano di chiunque fosse loggato, qui ogni sezione e' di qualcuno) - tutta l'app protetta, si elencano le eccezioni invece dei prefissi protetti - /api risponde 401/403, non un redirect verso una pagina di login - tre database distinti: utenti, dati clinici senza nomi, mappatura coi nomi Lato sito: - ROLES tornano i tre redazionali; via le regole e il login pubblico - il login del pannello RESPINGE un account emigrato invece di aprirgli una sessione: senza, landingFor lo mandava su /admin, il middleware glielo negava, e si formava un ciclo di redirect che sembra un guasto del sito - l'header non mostra piu' un menu delle piattaforme: un link che esce Verificato: sito 20 file / 146 test, piattaforme 26 file / 197 test, entrambi i build passano, e l'app girata davvero in locale - il ruolo piattaforme entra in Campus e viene rimbalzato da Longevity, il cliente il contrario. Non ancora in produzione: manca il record DNS di apps.insanitylab.it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
125 lines
6.2 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|