#!/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=] 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, resourceScenarios } from './scenarios-profile.mjs'; import { balanceScenarios } from './scenarios-balance.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, }), RESOURCE_ESTIMATE: o => ({ usable: o.usable, todayExpected: o.todayExpectedRemainingKwh, tomorrowExpected: o.tomorrowExpectedKwh, shapeConfidenceBelow: o.source.shapeConfidence != null && o.source.shapeConfidence < 0.5, }), BALANCE: o => o.usable ? ({ usable: true, crossingFound: o.crossing.found, crossingHour: o.crossing.hour, energyToCoverKwh: o.coverage.energyToCoverKwh, requiredSocPct: o.coverage.requiredSocPct, gridNeeded: o.coverage.gridNeeded, prudentRequiredSocPct: o.coverage.prudent.requiredSocPct, deficitTrustworthy: o.confidence.deficitTrustworthy, }) : ({ usable: false, missing: o.missing.join(',') }), }; const all = [ ...censorshipScenarios.map(s => ({ node: 'CENSORSHIP', ...s })), ...profileScenarios, ...resourceScenarios, ...balanceScenarios, ]; 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`);