HormoneTrack-web/tests/estrannaise-cloud.test.js
Siphonight fab5a2abfb v1.9.2 : fix nuage vide sans traitement stocké ESE (filtre par ester effectif) — miroir Android
- estrannaise-cloud.js : le filtre des doses ne teste PLUS le pkModel
  STOCKÉ du traitement — le nuage couvre toutes les doses E2 à profil
  injectable dont l'ESTER EFFECTIF (override compris) est couvert par le
  fit Estrannaise (identique à l'ensemble redessiné par
  computeCurve(modelOverride = "ESE")). L'exclusivité ESE reste portée
  par le chip ; l'oral Bateman reste hors nuage ; les traitements
  inactifs sont inclus (§6.bis).
- Test réécrit : « les doses d'un traitement TFS sont couvertes quand ESE
  est affiché » (épinglé des deux plateformes) ; oral seul → vide.
- WEB_VERSION 1.9.2. CHANGELOG [1.9.2], README statut.
2026-09-19 22:16:13 +02:00

93 lines
4.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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('les doses d un traitement TFS sont couvertes quand ESE est affiché (fix)', () => {
// Fix v1.9.2 : le pkModel STOCKÉ n\'est pas un prérequis — quand ESE
// est affiché, la courbe redessine toutes les doses E2 avec le modèle
// Estrannaise, donc le nuage les couvre aussi (l\'exclusivité ESE est
// portée par le CHIP, pas par le filtre des doses)
const tfs = { ...ese(), pkModel: 'TFS' };
const cloud = compute([tfs], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW);
assert.equal(cloud.length, 32);
assert.ok(cloud.every((c) => c.length >= 2));
const oral = makeTreatment({
name: 'CPA', type: 'ANTI_ANDROGEN', route: 'ORAL',
doseAmount: 12.5, doseUnit: 'mg', esterType: 'NONE',
forecastIntervalDays: 2.0, isActive: true,
});
// L'oral Bateman reste hors nuage (pas d'ester échantillonnable)
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);
});
test('le nuage suit la calibration (fix v1.9.1, miroir Android)', () => {
// scalePerEster ×2 → nuage calibré = nuage brut ×2 (le nuage ENTOURE
// la courbe ESE calibrée au lieu de flotter à une autre échelle)
const scaled = { EEN: 2.0 };
const calibrated = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW, 32, scaled);
const raw = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, NOW);
assert.equal(calibrated.length, raw.length);
assert.ok(raw[raw.length - 1].length >= 2);
const rawLast = raw[raw.length - 1][raw[raw.length - 1].length - 1].e2;
const calLast = calibrated[calibrated.length - 1][calibrated[calibrated.length - 1].length - 1].e2;
assert.ok(
Math.abs(calLast - rawLast * 2) <= rawLast * 0.01,
`calibré (${calLast}) ≈ 2× brut (${rawLast})`,
);
});
});