feat: phase A — backup B2, healthcheck /api/status, vector tiles Tippecanoe
- A1: scripts/backup-postgres.sh — pg_dump quotidien compressé → Backblaze B2 (rclone), rétention 30j - A2: StatusController retourne HTTP 503 + healthy/alertReason si dernière observation > 6h - A4: Dockerfile installe tippecanoe, GenerateTilesCommand génère 5 tilesets (obs + h6/12/24/48), Caddyfile sert /tiles/* sans fallback SPA, SargassesMap.jsx passe en sources vector tiles statiques - chore: backend/public/assets/ ajouté au .gitignore (build artifacts) - chore: setup vitest frontend + ImpactScoreServiceTest (session précédente) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
121
frontend/src/utils/scoring.test.js
Normal file
121
frontend/src/utils/scoring.test.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { scoreLabel, computeEta, etaLabel, ETA_BUFFER_KM } from './scoring';
|
||||
|
||||
// ─── scoreLabel ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('scoreLabel', () => {
|
||||
it('retourne "Impact" pour level high, quel que soit le score', () => {
|
||||
expect(scoreLabel(10, 'high', null)).toBe('Impact');
|
||||
expect(scoreLabel(0, 'high', 200)).toBe('Impact');
|
||||
expect(scoreLabel(100,'high', 0 )).toBe('Impact');
|
||||
});
|
||||
|
||||
it('retourne "Risque" pour level medium', () => {
|
||||
expect(scoreLabel(50, 'medium', 50)).toBe('Risque');
|
||||
expect(scoreLabel(31, 'medium', null)).toBe('Risque');
|
||||
});
|
||||
|
||||
it('retourne "Veille" si score ≥ 15 ET distance < 100km (level low)', () => {
|
||||
expect(scoreLabel(15, 'low', 99)).toBe('Veille');
|
||||
expect(scoreLabel(20, 'low', 0 )).toBe('Veille');
|
||||
expect(scoreLabel(15, 'low', 100)).toBe('OK'); // distance = seuil exact → non inclus
|
||||
});
|
||||
|
||||
it('retourne "OK" si score < 15 même avec distance proche', () => {
|
||||
expect(scoreLabel(14, 'low', 50)).toBe('OK');
|
||||
});
|
||||
|
||||
it('retourne "OK" si distance null même avec score ≥ 15', () => {
|
||||
expect(scoreLabel(20, 'low', null)).toBe('OK');
|
||||
});
|
||||
|
||||
it('retourne "OK" si distance ≥ 100km', () => {
|
||||
expect(scoreLabel(50, 'low', 100)).toBe('OK');
|
||||
expect(scoreLabel(80, 'low', 150)).toBe('OK');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── computeEta ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('computeEta', () => {
|
||||
it('retourne 0 si les sargasses sont déjà à moins de ETA_BUFFER_KM', () => {
|
||||
expect(computeEta({ distance: 0 }, null)).toBe(0);
|
||||
expect(computeEta({ distance: ETA_BUFFER_KM - 0.1 }, null)).toBe(0);
|
||||
});
|
||||
|
||||
it('retourne null si aucun horizon < ETA_BUFFER_KM', () => {
|
||||
const horizons = {
|
||||
h6: { distanceKm: 50 },
|
||||
h12: { distanceKm: 40 },
|
||||
h24: { distanceKm: 30 },
|
||||
h48: { distanceKm: 20 },
|
||||
};
|
||||
expect(computeEta({ distance: 100 }, horizons)).toBeNull();
|
||||
});
|
||||
|
||||
it('retourne le premier horizon qui passe sous ETA_BUFFER_KM', () => {
|
||||
const horizons = {
|
||||
h6: { distanceKm: 50 },
|
||||
h12: { distanceKm: 50 },
|
||||
h24: { distanceKm: 3 }, // < 5km
|
||||
h48: { distanceKm: 0 },
|
||||
};
|
||||
expect(computeEta({ distance: 100 }, horizons)).toBe(24);
|
||||
});
|
||||
|
||||
it('retourne h6 si dès le premier horizon', () => {
|
||||
const horizons = { h6: { distanceKm: 2 } };
|
||||
expect(computeEta({ distance: 100 }, horizons)).toBe(6);
|
||||
});
|
||||
|
||||
it('gère score null / horizons null sans erreur', () => {
|
||||
expect(computeEta(null, null)).toBeNull();
|
||||
expect(computeEta({ distance: null }, null)).toBeNull();
|
||||
});
|
||||
|
||||
it('retourne 0 si distance exactement ETA_BUFFER_KM - epsilon', () => {
|
||||
expect(computeEta({ distance: ETA_BUFFER_KM - 0.001 }, null)).toBe(0);
|
||||
});
|
||||
|
||||
it('retourne null si distance exactement ETA_BUFFER_KM', () => {
|
||||
// distance >= ETA_BUFFER_KM → pas d'impact en cours, on regarde les horizons
|
||||
const horizons = { h6: { distanceKm: 10 }, h12: { distanceKm: 10 }, h24: { distanceKm: 10 }, h48: { distanceKm: 10 } };
|
||||
expect(computeEta({ distance: ETA_BUFFER_KM }, horizons)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── etaLabel ─────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('etaLabel', () => {
|
||||
it('retourne eta--now pour etaHours=0', () => {
|
||||
const r = etaLabel(0, null, null);
|
||||
expect(r.cls).toBe('eta--now');
|
||||
expect(r.label).toBe('Impact en cours');
|
||||
});
|
||||
|
||||
it('retourne eta--soon pour etaHours=6 et 12', () => {
|
||||
expect(etaLabel(6, null, null).cls).toBe('eta--soon');
|
||||
expect(etaLabel(12, null, null).cls).toBe('eta--soon');
|
||||
});
|
||||
|
||||
it('retourne eta--later pour etaHours=24 et 48', () => {
|
||||
expect(etaLabel(24, null, null).cls).toBe('eta--later');
|
||||
expect(etaLabel(48, null, null).cls).toBe('eta--later');
|
||||
});
|
||||
|
||||
it('retourne eta--watch si score ≥ 15 ET distance < 100 ET etaHours null', () => {
|
||||
expect(etaLabel(null, 15, 50).cls).toBe('eta--watch');
|
||||
expect(etaLabel(null, 20, 99).cls).toBe('eta--watch');
|
||||
});
|
||||
|
||||
it('retourne eta--safe si pas d\'impact (fallback)', () => {
|
||||
expect(etaLabel(null, 0, 200).cls).toBe('eta--safe');
|
||||
expect(etaLabel(null, 14, 50 ).cls).toBe('eta--safe'); // score < 15
|
||||
expect(etaLabel(null, 20, 100).cls).toBe('eta--safe'); // distance = 100 → non inclus
|
||||
expect(etaLabel(null, 20, null).cls).toBe('eta--safe');
|
||||
});
|
||||
|
||||
it('retourne eta--safe si score null (considéré comme 0)', () => {
|
||||
expect(etaLabel(null, null, 50).cls).toBe('eta--safe');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user