/** * Tests des seuils d'alerte (miroir de AlertsTest.kt + AlertsEngineTest.kt) : * comparaison stricte, cohérence, codec d'état, décision de notification. */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import * as Alerts from '../js/pk/alerts.js'; describe('évaluation des seuils', () => { const t = new Alerts.Thresholds(400, 50, 1.2, 0.1); test('strict : la valeur EXACTEMENT à la limite ne déclenche rien', () => { assert.equal(Alerts.evaluate(400, 50, 400), null, '== high → rien'); assert.equal(Alerts.evaluate(50, 50, 400), null, '== low → rien'); assert.equal(Alerts.evaluate(400.01, 50, 400)[0], 'HIGH'); assert.equal(Alerts.evaluate(49.99, 50, 400)[0], 'LOW'); }); test('null = jamais d\'alerte sur ce côté', () => { assert.equal(Alerts.evaluate(1000, null, null), null); assert.equal(Alerts.evaluate(1, null, null), null); assert.equal(Alerts.evaluate(500, null, 400)[0], 'HIGH'); }); test('HIGH prime LOW en configuration incohérente (défensif)', () => { const [level, limit] = Alerts.evaluate(50, 100, 40); // haut < bas (interdit à la saisie) assert.equal(level, 'HIGH'); assert.equal(limit, 40); }); test('evaluateAll : ordre stable E2 puis T, unités correctes', () => { const alerts = Alerts.evaluateAll(500, 0.05, t); assert.equal(alerts.length, 2); assert.deepEqual(alerts.map((a) => a.marker), ['E2', 'T']); assert.deepEqual(alerts.map((a) => a.unit), ['pg/mL', 'ng/mL']); assert.equal(alerts[0].level, 'HIGH'); assert.equal(alerts[1].level, 'LOW'); }); test('isCoherent : haut > bas requis (les deux marqueurs indépendants)', () => { assert.equal(new Alerts.Thresholds(400, 50, 1.2, 0.1).isCoherent(), true); assert.equal(new Alerts.Thresholds(50, 400, null, null).isCoherent(), false); assert.equal(new Alerts.Thresholds(null, null, 0.1, 1.2).isCoherent(), false); // un seul côté défini → cohérent assert.equal(new Alerts.Thresholds(400, null, null, 0.1).isCoherent(), true); }); }); describe('codec d\'état (anti-spam) — rétro-résistant aux entrées malformées', () => { test('encode → parse round-trip', () => { const alerts = [ { marker: 'E2', level: 'HIGH' }, { marker: 'T', level: 'LOW' }, ]; const encoded = Alerts.encodeState(alerts); assert.equal(encoded, 'E2:HIGH;T:LOW'); assert.deepEqual(Alerts.parseState(encoded), { E2: 'HIGH', T: 'LOW' }); }); test('parse : résistant aux segments cassés', () => { assert.deepEqual(Alerts.parseState('E2:HIGH;garbage;T:BOGUS;:LOW'), { E2: 'HIGH' }); assert.deepEqual(Alerts.parseState(''), {}); assert.deepEqual(Alerts.parseState(null), {}); assert.deepEqual(Alerts.parseState(undefined), {}); }); test('shouldNotify : nouveau franchissement → true, même état → false', () => { const current = { E2: 'HIGH' }; assert.equal(Alerts.shouldNotify(current, null), true, 'première notification'); assert.equal(Alerts.shouldNotify(current, { E2: 'HIGH' }), false, 'pas de spam'); assert.equal(Alerts.shouldNotify(current, { E2: 'LOW' }), true, 'changement H↔L'); assert.equal(Alerts.shouldNotify({ E2: 'HIGH', T: 'LOW' }, { E2: 'HIGH' }), true, 'nouveau marqueur'); }); test('retour à la normale → false (pas de notif) — l\'appelant efface l\'état', () => { assert.equal(Alerts.shouldNotify({}, { E2: 'HIGH' }), false); }); });