HormoneTrack-web/tests/estrannaise-cloud.test.js
Siphonight 174928bb6b v1.9.0 : modèle Estrannaise ANALYTIQUE (abandon de l'ODS) + nuage d'incertitude MCMC
Miroir fidèle du Kotlin (doc Android §7.12) :
- estrannaise-models.js : forme close 3C de estrannaise.js (paramètres
  publiés EV/EU/EEn/EC/EB/EUCS), posterior MCMC (313 échantillons/ester,
  asset mcmc_samples.json ≈ 48 Ko fetché au démarrage — remplace le fetch
  des tables ODS, 550 Ko), t½ terminale analytique, mapping en objets
  {d,k1,k2,k3} (miroir du Param Kotlin).
- estrannaise-cloud.js : nuage d'incertitude — 32 courbes du posterior
  (échelonnées, déterministes) superposant les doses des porteurs ESE ;
  exclusif ESE (TFS/WHSAH sans posterior publié), gardes (sans dose,
  nbCurves<2, MCMC non chargé → vide).
- pk-engine.js : dispatch ESE → forme close (plus de fallback PKProfileStore
  au runtime) ; cutoffHours ESE → 10 × t½ analytique ; lab-timing.js t½ ESE
  analytique ; app.js fetch MCMC remplace le fetch des tables ODS
  (démarrage plus rapide).
- chart.js/chart-canvas.js : chip « Nuage » (off, activable si ESE affiché,
  coupure auto si ESE off), clé 'CLOUD' skippée des légendes (leçon #63),
  nuage dessiné en alpha faible, légende dédiée ; seed démo + traitement
  EV/ESE (2 doses) pour démontrer le nuage ; choicesForModel ESE=6.
- Tests : estrannaise-models.test.js (6 : fidélité RMS/pics, dégénérés,
  MCMC 313×6, t½ analytique, gardes) + estrannaise-cloud.test.js (5 :
  32 courbes à dispersion réelle, fenêtre, exclusivité ESE, gardes)
  → 166 verts + E2E (chip/légende/exclusivité nuage) + check --release.
- WEB_VERSION 1.9.0 (versions sync Android). CHANGELOG [1.9.0] web.
2026-09-19 20:45:53 +02:00

70 lines
2.7 KiB
JavaScript

/**
* 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);
});
});