- lab-timing.js : computeRegimeStartMs — le régime courant = séquence terminale de doses à (ester effectif, dose, écart inter-doses) constants (écart comparé EXACTEMENT) : tout changement récent de dose/ester/ posologie réinitialise la stabilisation. L'ancien proxy (1ʳᵉ dose du traitement) disait « stabilisée depuis février » malgré des changements récents (remontée v1.8.0). L'encart porte l'ESTER EFFECTIF de la dernière dose (override compris). - home.js : encart « Prochaine prise de sang (suggestion) » sur l'ACCUEIL (demande) — creux daté + créneau associé + mention de stabilisation seulement si le régime n'était pas déjà stable. - 3 nouveaux tests miroirs (155 verts) : dose/intervalle/ester → reset épinglé. WEB_VERSION 1.8.1. CHANGELOG [1.8.1], README.
137 lines
6.0 KiB
JavaScript
137 lines
6.0 KiB
JavaScript
/**
|
||
* 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);
|
||
});
|
||
|
||
// ── v1.8.1 : le régime courant voit les CHANGEMENTS RÉCENTS ──────────────
|
||
// Critique v1.8.0 : « changé d'ester, de dosage ET de posologie, et l'app
|
||
// me disait stabilisée depuis février » — l'ancien proxy (1ʳᵉ dose du
|
||
// traitement) ne détectait rien. NOUVELLE règle : le régime = séquence
|
||
// terminale à (ester, dose, écart inter-doses) constants ; tout changement
|
||
// récent réinitialise la stabilisation.
|
||
|
||
test('changement de dose récent réinitialise la stabilisation', () => {
|
||
// 9 semaines à 5 mg, puis 3 mg il y a 2 j : le régime (3 mg) n'a que
|
||
// 2 j → non stabilisé → créneau ≥ NOW−2 j + ~22,7 j ≈ NOW+26 j (le 1ᵉʳ
|
||
// créneau part de la DERNIÈRE dose réelle, pas de NOW)
|
||
const doses = weeklyDoses(9).concat([makeDose(NOW - 2 * DAY_MS, 3.0)]);
|
||
const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW);
|
||
assert.equal(rec.injectionMs, NOW + 26 * DAY_MS);
|
||
assert.equal(rec.wasAlreadyStabilized, false);
|
||
});
|
||
|
||
test('changement d\'intervalle récent réinitialise la stabilisation', () => {
|
||
// 9 semaines à 7 j, puis une dose 2 j après la plus ancienne des deux
|
||
// dernières : l'écart terminal devient 2 j → régime fondé sur cette dose
|
||
// (celle à NOW−14 j avait un écart de 5 j ≠ 2 j → hors régime)
|
||
const doses = weeklyDoses(9).concat([makeDose(NOW - 9 * DAY_MS, 5.0)]);
|
||
const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW);
|
||
assert.equal(rec.injectionMs, NOW + 14 * DAY_MS);
|
||
assert.equal(rec.wasAlreadyStabilized, false);
|
||
});
|
||
|
||
test('override d\'ester récent réinitialise la stabilisation (ester effectif)', () => {
|
||
// Même dose (5 mg), même écart (7 j), mais la dernière dose est un
|
||
// override EV : switch d'ester = nouveau régime + l'ester EFFECTIF
|
||
// (EV, pas EEN) alimente la carte
|
||
const doses = weeklyDoses(9).concat([
|
||
Object.assign(makeDose(NOW, 5.0), { esterType: 'EV' }),
|
||
]);
|
||
const rec = nextBloodDrawRecommendation([eenTfs()], doses, [], NOW);
|
||
assert.equal(rec.ester, 'EV');
|
||
assert.equal(rec.wasAlreadyStabilized, false);
|
||
});
|
||
});
|