/** * Tests du NUAGE D'INCERTITUDE (v1.9.0 — miroir de `EstrannaiseCloudTest.kt`, * `js/pk/estrannaise-cloud.js`) : 32 courbes par défaut, dispersion réelle * entre courbes, fenêtre respectée, exclusivité ESE (TFS/oral → vide). */ import { test, describe, before } from 'node:test'; import { join } from 'node:path'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { initProfiles, makeTreatment, makeDose, DAY_MS, WEB_ROOT } from './helpers.js'; import { compute } from '../js/pk/estrannaise-cloud.js'; import { initWithJson as initMcmc } from '../js/pk/estrannaise-models.js'; const NOW = 1_790_000_000_000; function ese() { return makeTreatment({ name: 'EEn ESE', type: 'ESTRADIOL', route: 'INJECTION_IM', doseAmount: 5.0, doseUnit: 'mg', esterType: 'EEN', pkModel: 'ESE', forecastIntervalDays: 7.0, isActive: true, }); } function doses() { return [4, 3, 2, 1].map((k) => makeDose(NOW - k * DAY_MS, 5.0)); } describe('EstrannaiseCloud web (miroir Android v1.9.0)', () => { before(() => { initProfiles(); // Le nuage exige le posterior MCMC chargé (313 échantillons/ester) initMcmc(readFileSync(join(WEB_ROOT, 'assets/mcmc_samples.json'), 'utf8')); }); test('32 courbes avec dispersion réelle entre elles', () => { const cloud = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW); assert.equal(cloud.length, 32); assert.ok(cloud.every((c) => c.length >= 2)); const peaks = [...new Set(cloud.map((c) => Math.max(...c.map((p) => p.e2))))]; assert.ok(peaks.length >= 8, `les courbes divergent (${peaks.length} maxima distincts)`); }); test('le nuage reste dans la fenêtre demandée', () => { const start = NOW - 10 * DAY_MS; const end = NOW; const cloud = compute([ese()], doses(), start, end, 3_600_000, NOW); assert.ok(cloud.every((c) => c.every((p) => p.timestamp >= start && p.timestamp <= end))); }); test('exclusivité ESE : TFS et oral produisent un nuage vide', () => { const tfs = { ...ese(), pkModel: 'TFS' }; assert.equal(compute([tfs], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW).length, 0); const oral = makeTreatment({ name: 'CPA', type: 'ANTI_ANDROGEN', route: 'ORAL', doseAmount: 12.5, doseUnit: 'mg', esterType: 'NONE', forecastIntervalDays: 2.0, isActive: true, }); assert.equal(compute([oral], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW).length, 0); }); test('aucune dose sur le porteur → nuage vide', () => { assert.equal(compute([ese()], [], NOW - 30 * DAY_MS, NOW, 3_600_000, NOW).length, 0); }); test('nbCurves < 2 → nuage vide', () => { assert.equal(compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 1).length, 0); }); });