/** * Tests de fidélité de l'asset pk_profiles.json + échantillonnage * (miroir de PKProfileStoreTest.kt côté Android). * * ⚠️ L'asset web est une COPIE de l'asset Android : ces tests garantissent * que les deux apps lisent EXACTEMENT les mêmes tables (toute divergence = * courbes différentes entre Android et web). */ import { test, describe, before } from 'node:test'; import assert from 'node:assert/strict'; import { initProfiles, rawProfiles } from './helpers.js'; import * as PKProfileStore from '../js/pk/pk-profile-store.js'; describe('pk_profiles.json (asset partagé avec Android)', () => { before(() => initProfiles()); test('6 profils présents × 8001 points', () => { const raw = rawProfiles(); const expected = ['EV_ese', 'EU_ese', 'EEn_ese', 'EV_tfs', 'EU_tfs', 'EEn_tfs']; for (const key of expected) { assert.ok(raw.profiles[key], `profil manquant : ${key}`); assert.equal(raw.profiles[key].length, 8001, `${key} doit avoir 8001 points`); } }); test('pics == valeurs ODS de référence (pg/mL par mg)', () => { // Mêmes valeurs épinglées côté Android (doc §7.1 / PKProfileStoreTest) const refs = [ ['EV', 'ESE', 61.12], ['EU', 'ESE', 3.44], ['EEn', 'ESE', 31.35], ['EV', 'TFS', 58.96], ['EU', 'TFS', 10.11], ['EEn', 'TFS', 31.97], ]; for (const [ester, mod, peak] of refs) { let max = 0; for (let h = 0; h <= 8000; h++) { const v = PKProfileStore.sample(ester, mod, h); if (v > max) max = v; } assert.ok(Math.abs(max - peak) < 0.02, `pic ${ester}_${mod} = ${max}, attendu ${peak}`); } }); test('EV_ese : pic à ~45 h', () => { let peakH = 0; let peakV = 0; for (let h = 0; h <= 500; h++) { const v = PKProfileStore.sample('EV', 'ESE', h); if (v > peakV) { peakV = v; peakH = h; } } assert.ok(peakH >= 40 && peakH <= 50, `Tmax EV_ese = ${peakH} h, attendu ~45 h`); }); test('zéro avant injection / à dt=0', () => { assert.equal(PKProfileStore.sample('EV', 'ESE', 0), 0); assert.equal(PKProfileStore.sample('EV', 'ESE', -5), 0); }); test('modèle STRICT : modèle inconnu → 0 (jamais de fallback silencieux, bug §14 #21)', () => { assert.equal(PKProfileStore.sample('EV', 'WHS', 100), 0); // WHS n'a pas de tables ODS assert.equal(PKProfileStore.sample('EV', 'XXX', 100), 0); }); test('lookup INSENSIBLE À LA CASSE (bug #22 — courbes EEn vides)', () => { // Esters.EEN = "EEN" (constante app) vs "EEn" (casse biologique de l'asset) assert.equal(PKProfileStore.sample('EEN', 'ESE', 152), PKProfileStore.sample('EEn', 'ESE', 152)); assert.ok(PKProfileStore.sample('EEN', 'ESE', 152) > 1); }); test('extrapolation terminale : au-delà de la table, décroissante (EV) et jamais croissante (EU)', () => { // La table fait 8001 h : l'extrapolation ne s'applique qu'AU-DELÀ. // EV (t½ ~4 j ODS) : décroissance exponentielle mesurable après 8000 h. const ev8500 = PKProfileStore.sample('EV', 'ESE', 8500); const ev9000 = PKProfileStore.sample('EV', 'ESE', 9000); assert.ok(ev8500 > 0, 'queue extrapolée mesurable'); assert.ok(ev9000 < ev8500, `décroissance attendue (${ev8500} → ${ev9000})`); // EU (ester ultra-lent, tail plate au plancher 0,04) : taux log ≈ 0 → // plateau constant — en tout cas JAMAIS croissante (bug #20 : extrapoler // depuis le plancher 0,01/0,00 donnait 0 à vie ou une constante plate). const eu8000 = PKProfileStore.sample('EU', 'ESE', 8000); const eu8500 = PKProfileStore.sample('EU', 'ESE', 8500); assert.ok(eu8000 > 0, 'EU mesurable au-delà de la table'); assert.ok(eu8500 <= eu8000, 'jamais croissante'); }); test('interpolation linéaire entre points entiers', () => { // Le plateau 45/46 h d'EV_ese est plat (piège du test Android) — prendre // une heure en montée : la valeur à 100.5 h = moyenne de 100 et 101 const raw = rawProfiles(); const arr = raw.profiles.EV_ese; const expected = (arr[100] + arr[101]) / 2; assert.ok(Math.abs(PKProfileStore.sample('EV', 'ESE', 100.5) - expected) < 1e-9); }); });