HormoneTrack-web/tests/lab-timing.test.js
Siphonight f5951e1806 v1.8.0 : recommandation de prochaine prise de sang (portage Android, miroir)
- js/pk/lab-timing.js : miroir fidèle de pk/LabTiming.kt — creux prévisionnel
  (minimum de la courbe E2 entre 2 injections), saut au premier creux
  STABILISÉ (5 × t½ terminale : TFS/WHS analytiques, Estrannaise lue dans
  la table via le nouveau pkProfileStore.terminalHalfLifeDays — refactor
  de la pente d'extrapolation de sample(), zéro duplication), filtres
  honnêtes (injectable actif + Posologie requis, creux ≥ now+6 h et jamais
  déjà mesuré, horizon borné → null), shouldSuggestPosology (invite quand
  un injectable actif est sans Posologie — demande v1.8.0).
- labs.js : encart « Prochaine prise de sang (suggestion) » ou invite,
  mutuellement exclusifs (miroir carte Android).
- i18n FR/EN (labrec_*), WEB_VERSION 1.8.0, assertions E2E.
- tests/lab-timing.test.js : 7 tests miroirs (152 verts au total) + E2E
  (encart rendu : titre, creux daté, créneau, stabilisation).
- Doc : §4 arbre, §8 suite + E2E, CHANGELOG [1.8.0], README.
2026-09-17 18:46:51 +02:00

98 lines
3.9 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 de la RECOMMANDATION DE PROCHAINE PRISE DE SANG (v1.8.0 — miroir de
* `LabTimingTest.kt` Android, `js/pk/lab-timing.js`) :
* - creux recommandé JUSTE AVANT le créneau d'injection associé ;
* - régime non stabilisé → saut au premier creux stabilisé (5 × t½) ;
* - régime stabilisé → premier creux (`wasAlreadyStabilized = true`) ;
* - sans Posologie / sans injectable E2 → null (+ invite Posologie) ;
* - prise récente → creux SUIVANT (jamais un creux déjà mesuré).
*/
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { nextBloodDrawRecommendation, shouldSuggestPosology } from '../js/pk/lab-timing.js';
import { makeTreatment, makeDose, makeLab } from './helpers.js';
const DAY_MS = 86_400_000;
const HOUR_MS = 3_600_000;
/** Maintenant FIXE : les tests sont déterministes. */
const NOW = 1_790_000_000_000;
/** Traitement EEn TFS actif, Posologie 7 j (fixture de l'utilisatrice). */
function eenTfs(posologyDays = 7.0) {
return makeTreatment({
name: 'EEn', type: 'ESTRADIOL', route: 'INJECTION_IM',
doseAmount: 5.0, doseUnit: 'mg', esterType: 'EEN', pkModel: 'TFS',
forecastIntervalDays: posologyDays, isActive: true,
});
}
/** Doses hebdomadaires sur [weeks] semaines (la plus récente à NOW − 7 j). */
function weeklyDoses(weeks) {
const out = [];
for (let k = weeks; k >= 1; k--) {
out.push(makeDose(NOW - k * 7 * DAY_MS, 5.0));
}
return out;
}
const lab = (ts) => makeLab('E2', 200, 'pg/mL', ts);
describe('LabTiming (miroir Android v1.8.0)', () => {
test('le creux est trouvé juste avant le créneau d injection associé', () => {
const rec = nextBloodDrawRecommendation(
[eenTfs()], weeklyDoses(9), [], NOW,
);
assert.ok(rec, 'une recommandation est calculable');
assert.equal(rec.injectionMs, NOW + 7 * DAY_MS);
const gap = rec.injectionMs - rec.troughMs;
assert.ok(gap > 0 && gap <= 2 * HOUR_MS, `creux juste avant l'injection (écart ${gap} ms)`);
assert.equal(rec.wasAlreadyStabilized, true);
});
test('non stabilisé - saute au premier creux STABILISÉ', () => {
// Une seule dose il y a 7 j : EEn TFS t½ = ln2/0.15291485 ≈ 4,53 j →
// stabilisation ≈ NOW + 15,7 j → créneaux +7/+14 trop tôt, +21 premier stable
const rec = nextBloodDrawRecommendation([eenTfs()], weeklyDoses(1), [], NOW);
assert.equal(rec.injectionMs, NOW + 21 * DAY_MS);
assert.equal(rec.wasAlreadyStabilized, false);
});
test('déjà stabilisé - premier creux, wasAlreadyStabilized à true', () => {
const rec = nextBloodDrawRecommendation([eenTfs()], weeklyDoses(9), [], NOW);
assert.equal(rec.injectionMs, NOW + 7 * DAY_MS);
assert.equal(rec.wasAlreadyStabilized, true);
assert.ok(rec.stabilizedAtMs < NOW);
});
test('prise de sang récente → recommande le creux SUIVANT', () => {
const recentLab = lab(NOW + 7 * DAY_MS - 30 * 60_000); // 30 min avant le créneau
const rec = nextBloodDrawRecommendation(
[eenTfs()], weeklyDoses(9), [recentLab], NOW,
);
assert.equal(rec.injectionMs, NOW + 14 * DAY_MS);
});
test('sans Posologie → null', () => {
assert.equal(
nextBloodDrawRecommendation([eenTfs(null)], weeklyDoses(9), [], NOW),
null,
);
});
test('oral seul → null (et pas d\'invite Posologie)', () => {
const oral = makeTreatment({
name: 'CPA', type: 'ANTI_ANDROGEN', route: 'ORAL',
doseAmount: 12.5, doseUnit: 'mg', esterType: 'NONE',
forecastIntervalDays: 2.0, isActive: true,
});
assert.equal(nextBloodDrawRecommendation([oral], weeklyDoses(9), [], NOW), null);
assert.equal(shouldSuggestPosology([oral]), false);
});
test('invite Posologie seulement pour un injectable E2 sans Posologie', () => {
assert.equal(shouldSuggestPosology([eenTfs(null)]), true);
assert.equal(shouldSuggestPosology([eenTfs(7.0)]), false);
});
});