Corrigé (audit) : - cutoffHours : garde hasModel pour ESE (parité Kotlin) — un backup importé ESE + ester non couvert levait TypeError → écran vide - min/max en une passe : les spreads Math.min(...allPoints) levaient RangeError au-delà de ~1e5 arguments (Nuage × zoom max) et allouaient par frame - init MCMC non bloquante : data-ready garantit désormais « asset chargé » Perf (portage v1.8.2 Android, fin de la dérive de miroir) : - prepareE2Context/e2AtCtx : doses groupées, cutoffs précalculés, Bateman paresseux ; computeCurve ne recalcule plus cutoffHours par point - résultat identique (175 tests verts) E2E : version lue via data-version (anti-bug v1.9.3) Docs/commentaires : pk-engine (ESE analytique), pk-calibration (JSDoc arrondi), dialogs (ESE=6), lab-timing (import mort, JSDoc orpheline), home (COLORS), pk-profile-store (test-only), pk_profiles.json exclu du zip (export-ignore, −548 Ko)
93 lines
4.0 KiB
JavaScript
93 lines
4.0 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);
|
||
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);
|
||
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);
|
||
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).length, 0);
|
||
});
|
||
|
||
test('aucune dose sur le porteur → nuage vide', () => {
|
||
assert.equal(compute([ese()], [], NOW - 30 * DAY_MS, NOW, 3_600_000).length, 0);
|
||
});
|
||
|
||
test('nbCurves < 2 → nuage vide', () => {
|
||
assert.equal(compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000, 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, 32, scaled);
|
||
const raw = compute([ese()], doses(), NOW - 30 * DAY_MS, NOW, 3_600_000);
|
||
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})`,
|
||
);
|
||
});
|
||
});
|