Files
EVCS/tests/energy-balance/run.mjs
Tallagrand Thierry 4b7cc3ae18 Chantier etape 2 : profil horaire de consommation mesure
CONSUMPTION_PROFILE.js : compteurs d'energie cumules -> deltas horaires ->
distribution par heure de la journee -> percentiles. La consommation du
vehicule est retiree : c'est une charge pilotable, elle ne doit pas polluer le
profil de la charge subie. Les remises a zero de compteur et les deltas
implausibles sont jetes, jamais rattrapes par une valeur inventee, et une heure
sans assez de jours observes est marquee non fiable au lieu d'etre publiee en
silence.

Valide sur 28 jours reels (639 heures, 0 reset) : 31,9 kWh/jour typique hors
vehicule, 42,8 kWh au p80. Le profil tient dans un helper input_text
(119 caracteres sur 255).

Verification prealable de la methode : seulement 7,8 % des heures de deficit
sont censurees. Censure et deficit sont physiquement presque exclusifs, donc la
grandeur qui pilote la decision EDF est mesuree sur des donnees propres.

Suite tests/energy-balance/ etendue aux deux noeuds : 14 scenarios.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-08-24 14:43:26 +02:00

90 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
// Suite de tests du chantier bilan énergétique.
// Exécute le VRAI code des nœuds de src/energy-balance/ dans une sandbox vm,
// avec le shim $input de n8n, sur des snapshots HA (réels + cas construits).
//
// Usage : node tests/energy-balance/run.mjs [--print] [--only=<motif>]
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import { fileURLToPath } from 'node:url';
import { scenarios as censorshipScenarios } from './scenarios.mjs';
import { profileScenarios } from './scenarios-profile.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const PRINT = process.argv.includes('--print');
const ONLY = (process.argv.find(a => a.startsWith('--only=')) || '').split('=')[1];
const codeCache = new Map();
function runNode(nodeName, snapshot) {
if (!codeCache.has(nodeName)) {
codeCache.set(nodeName, fs.readFileSync(path.join(ROOT, `src/energy-balance/${nodeName}.js`), 'utf8'));
}
const sandbox = { $input: { first: () => ({ json: snapshot }) }, console };
vm.createContext(sandbox);
return vm.runInContext(`(function(){ ${codeCache.get(nodeName)} })()`, sandbox, { timeout: 15000 })[0].json;
}
const SUMMARIZE = {
CENSORSHIP: o => ({
pvCensored: o.censorship.pvCensored,
reasons: o.censorship.reasons.join('|'),
sampleExploitable: o.censorship.sampleExploitable,
healthOk: o.health.ok,
alerts: o.alerts.join('|'),
}),
CONSUMPTION_PROFILE: o => ({
days: o.days,
complete: o.complete,
hours: o.profile.length,
countersReset: o.quality.countersReset,
unreliableHours: o.quality.unreliableHours.join(','),
csvFitsHelper: o.csv.p80.length <= 255 && o.csv.p50.length <= 255,
p50AtHour14: o.profile[14].p50,
p80AtHour14: o.profile[14].p80,
}),
};
const all = [
...censorshipScenarios.map(s => ({ node: 'CENSORSHIP', ...s })),
...profileScenarios,
];
let failed = 0;
let run = 0;
for (const sc of all) {
if (ONLY && !sc.id.includes(ONLY)) continue;
run++;
const got = SUMMARIZE[sc.node](runNode(sc.node, sc.snapshot));
if (PRINT) {
console.log(`─── [${sc.node}] ${sc.id} | ${sc.label}`);
console.log(' ' + JSON.stringify(got));
continue;
}
const diffs = [];
for (const [k, want] of Object.entries(sc.expect)) {
if (JSON.stringify(got[k]) !== JSON.stringify(want)) {
diffs.push(` ${k}: attendu ${JSON.stringify(want)}, obtenu ${JSON.stringify(got[k])}`);
}
}
if (diffs.length) {
failed++;
console.log(`❌ ${sc.id} | ${sc.label}`);
diffs.forEach(d => console.log(d));
} else {
console.log(`✅ ${sc.id}`);
}
}
if (PRINT) process.exit(0);
console.log('');
if (failed) {
console.log(`${failed} ÉCHEC(S) — NE PAS POUSSER EN PRODUCTION`);
process.exit(1);
}
console.log(`ENERGY BALANCE OK — ${run}/${run} scénarios conformes`);