Une coupure reseau n arrete pas l installation : le solaire et la batterie continuent, seul le filet de secours disparait. La reserve a tenir est donc la couverture prudente p80 que le bilan calcule deja — aucune constante nouvelle. En deficit c est la fenetre traversee, en surplus la prochaine, car a 15 h la reponse « rien a couvrir » est exacte et inutile. Le seuil d activation est la vigilance orange, echelle officielle de Meteo-France et non un nombre choisi ici. Le snapshot lit l alerte sans connaitre la commune, donc le code marche des l installation de l integration et publie « indisponible » en attendant. Tests 38 -> 42. Le fixture du test de perimetre ombre ne contenait pas storm : les nouvelles ecritures n etaient pas couvertes. Corrige, assertion explicite, preuve par mutation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
204 lines
6.7 KiB
JavaScript
204 lines
6.7 KiB
JavaScript
// Scénarios de BUILD_HA_CALLS — garantie du mode ombre.
|
|
// La suite doit ÉCHOUER si une écriture sort du périmètre `evcs_balance_*`.
|
|
// C'est la seule protection structurelle contre un câblage réel accidentel.
|
|
|
|
const SNAPSHOT = {
|
|
balance_enabled: 'on',
|
|
grid_soc_off: '30.0',
|
|
soc_percent: '21',
|
|
usable_capacity_kwh: '15',
|
|
};
|
|
|
|
const CENSORSHIP_CENSURE = {
|
|
censorship: { pvCensored: true, reasons: ['batterie_pleine_sans_puits'] },
|
|
};
|
|
const CENSORSHIP_LIBRE = {
|
|
censorship: { pvCensored: false, reasons: [] },
|
|
};
|
|
|
|
const BALANCE_OK = {
|
|
usable: true,
|
|
crossing: { found: true, hour: 8 },
|
|
coverage: {
|
|
energyToCoverKwh: 0.82,
|
|
requiredSocPct: 27.5,
|
|
currentSocPct: 21,
|
|
socMarginPct: -6.5,
|
|
gridNeeded: true,
|
|
gridEnergyNeededKwh: 0.97,
|
|
prudent: { requiredSocPct: 38.1 },
|
|
},
|
|
vehicle: { surplusAvailableKwh: 20.17 },
|
|
confidence: { deficitTrustworthy: true },
|
|
// La réserve d'orage fait partie de la sortie normale de BALANCE : elle doit
|
|
// figurer ici, sinon la garantie de périmètre ne couvre pas ses écritures.
|
|
storm: {
|
|
level: 'orange', known: true, active: true, phenomena: ['Orages'],
|
|
windowStartHour: null, reserveKwh: 2.42, reserveSocPct: 38.1,
|
|
socMarginPct: -17.1, covered: false, shortfallKwh: 2.57,
|
|
},
|
|
};
|
|
|
|
// Le même bilan, mais sans croisement trouvé sur l'horizon : le SOC requis
|
|
// n'est plus une exigence datée, seulement la somme d'un horizon entier. Le
|
|
// statut doit le dire.
|
|
const BALANCE_SANS_CROISEMENT = {
|
|
...BALANCE_OK,
|
|
crossing: { found: false, hour: null },
|
|
coverage: { ...BALANCE_OK.coverage, requiredSocPct: 100, energyToCoverKwh: 25.97 },
|
|
};
|
|
|
|
export const shadowScenarios = [
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-perimetre-respecte',
|
|
label: 'Bilan complet : toutes les écritures restent dans evcs_balance_*',
|
|
snapshot: {
|
|
...BALANCE_OK,
|
|
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_CENSURE },
|
|
},
|
|
expect: {
|
|
outOfScope: '',
|
|
wroteRequiredSoc: true,
|
|
wroteGridNeeded: true,
|
|
// Les 5 écritures de la réserve d'orage sont émises et restent dans le
|
|
// périmètre ombre.
|
|
wroteStorm: 5,
|
|
hasLogbook: true,
|
|
status: 'ok',
|
|
},
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-sans-croisement-statut-degrade',
|
|
label:
|
|
'Aucun croisement sur l\'horizon : les chiffres sont publiés mais le statut passe à `degrade`',
|
|
snapshot: {
|
|
...BALANCE_SANS_CROISEMENT,
|
|
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_LIBRE },
|
|
},
|
|
expect: {
|
|
outOfScope: '',
|
|
wroteRequiredSoc: true,
|
|
status: 'degrade',
|
|
},
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-bilan-indisponible',
|
|
label: 'Bilan inutilisable : statut publié, aucune valeur inventée',
|
|
snapshot: {
|
|
usable: false,
|
|
missing: ['usable_capacity_kwh'],
|
|
__nodes: { 'HA Snapshot Balance': SNAPSHOT, CENSORSHIP: CENSORSHIP_LIBRE },
|
|
},
|
|
expect: {
|
|
outOfScope: '',
|
|
wroteRequiredSoc: false,
|
|
hasLogbook: false,
|
|
},
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS',
|
|
id: 'ombre-kill-switch',
|
|
label: 'Kill-switch à OFF : absolument aucune écriture',
|
|
snapshot: {
|
|
...BALANCE_OK,
|
|
__nodes: {
|
|
'HA Snapshot Balance': { ...SNAPSHOT, balance_enabled: 'off' },
|
|
CENSORSHIP: CENSORSHIP_CENSURE,
|
|
},
|
|
},
|
|
expect: {
|
|
callCount: 0,
|
|
outOfScope: '',
|
|
},
|
|
},
|
|
];
|
|
|
|
// --- BUILD_HA_CALLS_PROFILES (workflow quotidien) ------------------------
|
|
const PROFILES_OK = {
|
|
complete: true,
|
|
days: 28,
|
|
csv: { p50: '1,'.repeat(23) + '1', p80: '2,'.repeat(23) + '2',
|
|
envelope: '3,'.repeat(23) + '3', censorshipRate: '0,'.repeat(23) + '0' },
|
|
totals: { typicalDailyKwh: 31.9, prudentDailyKwh: 42.8 },
|
|
production: { demonstratedDailyKwh: 74.4, censoredDayHours: 8 },
|
|
quality: { hoursUsed: 639, pvHoursUsed: 620, countersReset: 0, unreliableHours: [] },
|
|
};
|
|
|
|
export const profilesShadowScenarios = [
|
|
{
|
|
node: 'BUILD_HA_CALLS_PROFILES',
|
|
id: 'profils-perimetre-respecte',
|
|
label: 'Publication des profils : périmètre evcs_balance_* respecté',
|
|
snapshot: PROFILES_OK,
|
|
expect: { outOfScope: '', callCount: 6, hasLogbook: true },
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS_PROFILES',
|
|
id: 'profils-historique-insuffisant',
|
|
label: 'Historique trop court : les profils en place ne sont PAS écrasés',
|
|
snapshot: { ...PROFILES_OK, complete: false, days: 2,
|
|
quality: { ...PROFILES_OK.quality, unreliableHours: [3, 4, 5] } },
|
|
expect: { outOfScope: '', wroteProfiles: false },
|
|
},
|
|
];
|
|
|
|
// --- Prévision solaire (workflow 15 min) ---------------------------------
|
|
const FS_OK = {
|
|
ok: true, days: 6,
|
|
today: { date: '2026-08-25', csv: '0.00,'.repeat(23) + '1.00', totalKwh: 43.5 },
|
|
tomorrow: { date: '2026-08-26', csv: '0.00,'.repeat(23) + '2.00', totalKwh: 30.0 },
|
|
rateLimitRemaining: 293,
|
|
};
|
|
|
|
export const forecastScenarios = [
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-refus-cle-absente',
|
|
label: 'Clé absente : refus explicite, aucun appel gaspillé sur le quota',
|
|
snapshot: { enabled: 'on', api_key: 'unknown', planes: '40/-45/4.03', latitude: 48.7, longitude: -3.5 },
|
|
expect: { urlOk: false, problems: 'clé API absente' },
|
|
},
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-refus-plans-invalides',
|
|
label: 'Plans mal formés (nombre de valeurs non multiple de 3) : refus',
|
|
snapshot: { enabled: 'on', api_key: 'ABCD1234', planes: '40/-45', latitude: 48.7, longitude: -3.5 },
|
|
expect: { urlOk: false, problems: 'plans invalides' },
|
|
},
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-kill-switch',
|
|
label: 'Kill-switch à OFF : aucune URL construite',
|
|
snapshot: { enabled: 'off', api_key: 'ABCD1234', planes: '40/-45/4.03', latitude: 48.7, longitude: -3.5 },
|
|
expect: { urlOk: false },
|
|
},
|
|
{
|
|
node: 'BUILD_FORECAST_URL',
|
|
id: 'url-trois-plans',
|
|
label: 'Trois plans valides : URL construite, triplets préservés',
|
|
snapshot: {
|
|
enabled: 'on', api_key: 'ABCD1234',
|
|
planes: '40/-45/4.03/35/0/9.3/45/90/2.79',
|
|
latitude: 48.7732657, longitude: -3.5187179,
|
|
},
|
|
expect: { urlOk: true, planeCount: 3, urlTail: '40/-45/4.03/35/0/9.3/45/90/2.79' },
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS_FORECAST',
|
|
id: 'forecast-perimetre-respecte',
|
|
label: 'Publication de la prévision : périmètre evcs_balance_* respecté',
|
|
snapshot: FS_OK,
|
|
expect: { outOfScope: '', callCount: 4, hasLogbook: true },
|
|
},
|
|
{
|
|
node: 'BUILD_HA_CALLS_FORECAST',
|
|
id: 'forecast-echec-ne-detruit-rien',
|
|
label: 'Échec de récupération : la prévision précédente n\'est pas écrasée',
|
|
snapshot: { ok: false, problems: ['réponse illisible ou sans résultat'] },
|
|
expect: { outOfScope: '', wroteForecast: false },
|
|
},
|
|
];
|