Files
InsanityLab-web/.superpowers/sdd/2026-08-22-longevity-interfaccia/task-2-brief.md
T
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

6.7 KiB

Task 2: Il questionario in pagina

Sostituisce il prototipo del cliente, che salva nel browser e che il documento stesso dichiara da buttare in produzione.

Files:

  • Create: src/components/longevity/Questionario.tsx, src/pages/longevity/questionario.astro, src/pages/api/longevity/questionario.ts
  • Test: tests/longevity/questionario-pagina.test.ts

Interfaces:

  • Consumes: testAttivi da registro.ts; salvaCompilazione, QUEST_VERSION, CAMPI_LIBERI da questionario.ts; codicePerUtente da anagrafica.ts
  • Produces: POST /api/longevity/questionario → { sessioneId } o errore

Le domande vengono dal registro, non dal codice. La pagina legge testAttivi e costruisce i campi da lì: etichetta, tipo, minimo e massimo. Aggiungere una domanda resta una modifica ai dati — è la promessa della spec, e qui va mantenuta.

⚠️ Tre regole che l'endpoint deve rispettare, e sono di sostanza:

  1. Il codice cliente si ricava dalla sessione, mai dalla richiesta. Un codice che arriva dal browser va ignorato. È la regola §8 della spec, e questo è il primo endpoint su cui si può finalmente provare.
  2. Il consenso al trattamento dei dati sanitari non è «compilato», è «dato». Il prototipo del cliente blocca il salvataggio solo se il campo è vuoto: rispondendo «No» i dati vengono salvati lo stesso. È l'esatto contrario di ciò per cui il campo esiste. Qui: se il consenso non è affermativo, la compilazione non si salva, e la risposta lo dice chiaramente.
  3. Il consenso va registrato, non solo controllato. Un consenso è un fatto che un domani va dimostrato: chi, quando, a cosa. Salvalo fra le note di profilo con la sua data e la versione del questionario, anche quando la risposta è no — il rifiuto è precisamente ciò di cui bisogna poter provare il rispetto.
  • Step 1: Write the failing test
// tests/longevity/questionario-pagina.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 } from '../../src/lib/longevity/registro';
import { campiDelQuestionario, salvaDalForm } from '../../src/lib/longevity/vista';

function dbPronto() {
  const db = createLongevityDb(':memory:');
  seedRegistro(db);
  db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run();
  return db;
}

describe('i campi del questionario vengono dal registro', () => {
  it('costruisce i campi leggendo il registro, non una lista nel codice', () => {
    const db = dbPronto();
    const campi = campiDelQuestionario(db, '2026-08-22');
    expect(campi.length).toBe(20);
    const alcol = campi.find((c) => c.id === 'q_alcol_life')!;
    expect(alcol.etichetta).toMatch(/alcoliche/i);
    expect(alcol.max).toBe(30);
  });

  it('una domanda disattivata sparisce dal modulo senza toccare il codice', () => {
    const db = dbPronto();
    db.prepare(`UPDATE registro_test SET attivo_a = '2026-01-01' WHERE test_id = 'q_sigarette'`).run();
    const campi = campiDelQuestionario(db, '2026-08-22');
    expect(campi.map((c) => c.id)).not.toContain('q_sigarette');
    expect(campi.length).toBe(19);
  });
});

describe('il salvataggio, e il consenso', () => {
  const risposte = { q_riposato: 8, q_ore_sonno: 7.5 };

  it('salva una compilazione quando il consenso e dato', () => {
    const db = dbPronto();
    const esito = salvaDalForm(db, {
      client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
      risposte, consensoSanitario: 'Si', liberi: {},
    });
    expect(esito.ok).toBe(true);
    const n = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number };
    expect(n.n).toBe(1);
  });

  it('NON salva se il consenso e rifiutato: "No" non e un consenso', () => {
    const db = dbPronto();
    const esito = salvaDalForm(db, {
      client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
      risposte, consensoSanitario: 'No', liberi: {},
    });
    expect(esito.ok).toBe(false);
    const n = db.prepare(`SELECT COUNT(*) n FROM sessioni`).get() as { n: number };
    expect(n.n).toBe(0);   // niente sessione, niente misure
  });

  it('NON salva se il consenso manca del tutto', () => {
    const db = dbPronto();
    const esito = salvaDalForm(db, {
      client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
      risposte, consensoSanitario: '', liberi: {},
    });
    expect(esito.ok).toBe(false);
  });

  it('registra il consenso anche quando e rifiutato: il rifiuto va dimostrato', () => {
    const db = dbPronto();
    salvaDalForm(db, {
      client_code: 'ISL-0001', data: '2026-08-22', eta: 35,
      risposte, consensoSanitario: 'No', liberi: {},
    });
    const note = db.prepare(
      `SELECT campo_id, testo FROM profilo_note WHERE campo_id LIKE 'consenso%'`
    ).all() as { campo_id: string; testo: string }[];
    expect(note.length).toBeGreaterThan(0);
    expect(note[0].testo).toMatch(/No/);
  });

  it('il codice cliente NON puo arrivare da chi chiama la pagina', () => {
    const src = readFileSync(join(process.cwd(), 'src/pages/api/longevity/questionario.ts'), 'utf8');
    // il codice si prende dalla sessione dell'utente, mai dal corpo della richiesta
    expect(src).toMatch(/codicePerUtente/);
    expect(src).not.toMatch(/body\.client_code|body\.clientId|corpo\.client_code/);
  });
});
  • Step 2: Run test to verify it fails

Run: npm test -- tests/longevity/questionario-pagina.test.ts Expected: FAIL — src/lib/longevity/vista.ts non esiste

  • Step 3: Write minimal implementation

vista.ts espone campiDelQuestionario(db, alla) — che legge testAttivi e tiene i soli test_id che iniziano per q_ — e salvaDalForm(db, input), che controlla il consenso prima di scrivere qualsiasi cosa, registra il consenso fra le note col suo esito e la versione del questionario, e delega a salvaCompilazione.

L'isola React mostra i quattro blocchi uno per schermata, con avanti e indietro, e invia in fondo. L'endpoint ricava il codice cliente dalla sessione con codicePerUtente e passa a salvaDalForm.

⚠️ Il consenso rifiutato va scritto comunque fra le note: apri la sessione solo se il consenso è affermativo, ma la nota del consenso si scrive in entrambi i casi, legata al cliente.

  • Step 4: Run test to verify it passes

Run: npm test — atteso 1 fallito pre-esistente, il resto verde. Poi npx tsc --noEmit pulito.

  • Step 5: Commit
git add src/lib/longevity/vista.ts src/components/longevity/Questionario.tsx \
        src/pages/longevity/questionario.astro src/pages/api/longevity/questionario.ts \
        tests/longevity/questionario-pagina.test.ts
git commit -m "longevity: il questionario in pagina, e il consenso che si puo negare davvero"