import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { clamp, lerp, curvaCampana, curvaDecrescente, curvaCrescenteConPlateau, curvaDirettaX10, curvaDirettaX10Invertita, curvaGradini, } from '../../src/lib/longevity/motore/curve'; const RIF = JSON.parse( readFileSync(join(process.cwd(), 'tests/longevity/riferimento/riferimento.json'), 'utf8') ) as { casi: { fn: string; args: unknown[]; atteso: number }[] }; const casiDi = (fn: string) => RIF.casi.filter((c) => c.fn === fn); describe('curve del questionario, confrontate con l oracolo', () => { it('la campana combacia su tutto il dominio', () => { const casi = casiDi('score_bell_curve'); expect(casi.length).toBeGreaterThan(20); for (const c of casi) { const [v, low, pl, ph, high] = c.args as number[]; expect(curvaCampana(v, low, pl, ph, high)).toBeCloseTo(c.atteso, 1); } }); it('la decrescente combacia, inclusa la soglia 7-14 dell alcol', () => { const casi = casiDi('score_decreasing'); expect(casi.length).toBeGreaterThan(40); for (const c of casi) { const [v, best, worst] = c.args as number[]; expect(curvaDecrescente(v, best, worst)).toBeCloseTo(c.atteso, 1); } }); it('la crescente con plateau combacia', () => { const casi = casiDi('score_increasing_plateau'); expect(casi.length).toBeGreaterThan(20); for (const c of casi) { const [v, worst, plateau] = c.args as number[]; expect(curvaCrescenteConPlateau(v, worst, plateau)).toBeCloseTo(c.atteso, 1); } }); it('la diretta per dieci combacia', () => { const casi = casiDi('score_direct_x10'); expect(casi.length).toBeGreaterThan(10); for (const c of casi) { expect(curvaDirettaX10((c.args as number[])[0])).toBeCloseTo(c.atteso, 1); } }); // Questa curva NON esiste nell'oracolo: è la quinta, che al motore del cliente manca. // Serve a q_calo_pomeridiano e nel prototipo HTML è `scale10_inv`: (10 - v) * 10. it('la diretta invertita e il complemento della diretta', () => { for (const v of [0, 2.5, 5, 7.5, 10]) { expect(curvaDirettaX10Invertita(v)).toBeCloseTo(curvaDirettaX10(10 - v), 6); } expect(curvaDirettaX10Invertita(0)).toBe(100); expect(curvaDirettaX10Invertita(10)).toBe(0); }); it('i gradini decrescenti riproducono il prototipo: q_sigarette', () => { const steps: [number, number][] = [[0, 100], [5, 60], [10, 40], [20, 20], [999, 0]]; expect(curvaGradini(0, steps, undefined, true)).toBe(100); expect(curvaGradini(5, steps, undefined, true)).toBe(60); expect(curvaGradini(10, steps, undefined, true)).toBe(40); expect(curvaGradini(20, steps, undefined, true)).toBe(20); expect(curvaGradini(40, steps, undefined, true)).toBeLessThan(20); }); // Il ramo crescente non era esercitato da nessun test: l'ancoraggio (0, 20) // — invece di (0, 100) — poteva essere invertito o sbagliato senza che // niente diventasse rosso. Parametri veri da src/lib/longevity/registro.ts. it('i gradini crescenti riproducono il prototipo: q_attivita e q_luce', () => { // q_attivita: nessun zeroVal, i gradini partono già da (0, 20). const stepsAttivita: [number, number][] = [[0, 20], [2, 50], [4, 80], [999, 100]]; expect(curvaGradini(0, stepsAttivita, undefined, false)).toBe(20); expect(curvaGradini(2, stepsAttivita, undefined, false)).toBe(50); expect(curvaGradini(4, stepsAttivita, undefined, false)).toBe(80); expect(curvaGradini(999, stepsAttivita, undefined, false)).toBe(100); expect(curvaGradini(1, stepsAttivita, undefined, false)).toBe(35); // interpolato fra (0,20) e (2,50) expect(curvaGradini(3, stepsAttivita, undefined, false)).toBe(65); // interpolato fra (2,50) e (4,80) // q_luce: zeroVal 20, gradini che partono da 0.5 — l'ancoraggio (0, 20) // serve davvero per interpolare fra 0 (escluso, coperto da zeroVal) e 0.5. const stepsLuce: [number, number][] = [[0.5, 60], [1, 90], [999, 100]]; expect(curvaGradini(0.5, stepsLuce, 20, false)).toBe(60); expect(curvaGradini(1, stepsLuce, 20, false)).toBe(90); expect(curvaGradini(999, stepsLuce, 20, false)).toBe(100); expect(curvaGradini(0.75, stepsLuce, 20, false)).toBe(75); // interpolato fra (0.5,60) e (1,90) expect(curvaGradini(0.25, stepsLuce, 20, false)).toBe(40); // interpolato fra l'ancoraggio (0,20) e (0.5,60) }); // Non basta controllare che a zero il risultato sia il valore atteso: se // zeroVal COINCIDE con l'ancoraggio di default (100 per il decrescente, // 20 per il crescente, come in q_schermi e q_luce) il test passa anche // cancellando dal codice la riga che gestisce lo zero. Qui zeroVal è // deliberatamente diverso dall'ancoraggio, in entrambe le direzioni: se // la riga sparisse, l'interpolazione a v=0 darebbe l'ancoraggio (100 o 20), // non questi valori, e il test diventerebbe rosso. it('zeroVal vince sui gradini anche quando NON coincide con l ancoraggio', () => { // q_schermi, il caso reale: qui zeroVal (100) coincide con l'ancoraggio // decrescente (100), quindi da solo NON è discriminante — resta utile // come controllo del valore vero, non come prova dello zero speciale. const stepsSchermi: [number, number][] = [[15, 85], [30, 70], [60, 40], [999, 10]]; expect(curvaGradini(0, stepsSchermi, 100, true)).toBe(100); expect(curvaGradini(15, stepsSchermi, 100, true)).toBe(85); // Discriminante, decrescente: ancoraggio di default sarebbe 100, // zeroVal è 42. Senza la riga dello zero speciale risulterebbe 100. const stepsDecrescenteDiscriminante: [number, number][] = [[10, 80], [30, 50], [999, 10]]; expect(curvaGradini(0, stepsDecrescenteDiscriminante, 42, true)).toBe(42); // Discriminante, crescente: ancoraggio di default sarebbe 20, // zeroVal è 55. Senza la riga dello zero speciale risulterebbe 20. const stepsCrescenteDiscriminante: [number, number][] = [[5, 40], [20, 70], [999, 100]]; expect(curvaGradini(0, stepsCrescenteDiscriminante, 55, false)).toBe(55); }); it('clamp e lerp si comportano come nell oracolo', () => { expect(clamp(150)).toBe(100); expect(clamp(-5)).toBe(0); expect(lerp(5, 0, 10, 0, 100)).toBe(50); expect(lerp(-1, 0, 10, 0, 100)).toBe(0); // t viene limitato a [0,1] expect(lerp(11, 0, 10, 0, 100)).toBe(100); }); });