Files
EVCS/tools/ab-20260807-r1.mjs
Tallagrand Thierry 9c3a0a9c6e R1: déduplication du moteur nuit — WINDOWS_SOURCEV2 unique calculateur
- Le moteur nuit (161 lignes) était calculé 2x (WINDOWS + BCL), chaque patch
  nuit devait être appliqué 2x (P2, 04/08, P7) -> risque de divergence.
- WINDOWS calcule tout (+ startMs, needsCapture strict, recoveryAllowedNow,
  targetReached) et exporte nightEvcsPreview étendu.
- BCL consomme prev.nightEvcsPreview avec fallbacks sûrs (preview absent ->
  'blocked'). Export nightEvcs inchangé pour DECISION/HA_SERVICE.
- A/B git HEAD vs patché : 7 scénarios + 2 cas limites, sorties strictement
  identiques. Déployé en production, exécutions vérifiées success.
2026-08-07 19:05:21 +02:00

77 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
// A/B R1 : compare la sortie de BATTERY_CLOUD_LOCKS avant/après déduplication.
// AVANT = git HEAD ; APRÈS = working tree. Exigence : sorties identiques
// (hors nightEvcsPreview, volontairement étendu) sur tous les scénarios.
import fs from 'fs';
import vm from 'vm';
import { execSync } from 'child_process';
const OLD = JSON.parse(execSync('git show HEAD:workflows/evcs-surplus-controller-v2.6.json', { maxBuffer: 64 * 1024 * 1024 }).toString());
const NEW = JSON.parse(fs.readFileSync('workflows/evcs-surplus-controller-v2.6.json', 'utf8'));
const code = wf => name => wf.nodes.find(n => n.name === name).parameters.jsCode;
const oldW = code(OLD)('WINDOWS_SOURCEV2'), oldB = code(OLD)('BATTERY_CLOUD_LOCKS');
const newW = code(NEW)('WINDOWS_SOURCEV2'), newB = code(NEW)('BATTERY_CLOUD_LOCKS');
// Réutilise buildPrev + scénarios du harnais dry-run existant
const harness = fs.readFileSync('tools/dryrun-20260807-night-hc.mjs', 'utf8');
const buildPrev = eval('(' + harness.slice(harness.indexOf('function buildPrev'), harness.indexOf('function run')).replace('function buildPrev', 'function') + ')');
const scenarios = eval(harness.slice(harness.indexOf('const scenarios = ') + 'const scenarios = '.length, harness.indexOf('];') + 1));
function run(jsCode, prev) {
const ctx = { $input: { first: () => ({ json: prev }) }, console, Date, Intl, Math, Number, Object, String, JSON };
vm.createContext(ctx);
return vm.runInContext(`(function(){\n${jsCode}\n})()`, ctx)[0].json;
}
// Diff récursif (chemins des différences)
function diff(a, b, path = '', out = []) {
if (a === b) return out;
if (typeof a !== typeof b || a === null || b === null || typeof a !== 'object') {
if (!(Number.isNaN(a) && Number.isNaN(b))) out.push(`${path}: ${JSON.stringify(a)} != ${JSON.stringify(b)}`);
return out;
}
for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) diff(a[k], b[k], path ? `${path}.${k}` : k, out);
return out;
}
let failures = 0;
for (const sc of scenarios) {
// Snapshot construit UNE fois puis cloné : élimine la non-déterminisme Date.now()
const basePrev = buildPrev(sc);
const clone = () => JSON.parse(JSON.stringify(basePrev));
const bOld = run(oldB, run(oldW, clone()));
const bNew = run(newB, run(newW, clone()));
// nightEvcsPreview est volontairement étendu (champs additionnels) : exclu du diff strict
const strip = o => { const c = JSON.parse(JSON.stringify(o)); delete c.nightEvcsPreview; return c; };
const diffs = diff(strip(bOld), strip(bNew));
// nightEvcs (consommé par DECISION/HA_SERVICE) doit être STRICTEMENT identique
const nightDiffs = diff(bOld.nightEvcs, bNew.nightEvcs, 'nightEvcs');
const all = [...diffs, ...nightDiffs.filter(d => !diffs.includes(d))];
if (all.length) {
failures++;
console.log(`❌ ${sc.label}`);
for (const d of all.slice(0, 10)) console.log(` ${d}`);
} else {
console.log(`✅ identique | ${sc.label}`);
}
}
// Test fallback : preview absent => pas de crash, mode 'blocked'
const prevNoPreview = run(newW, buildPrev(scenarios[0]));
delete prevNoPreview.nightEvcsPreview;
try {
const out = run(newB, prevNoPreview);
const n = out.nightEvcs;
const ok = n.nightSessionActive === false && n.nightPreferredSourceMode === 'blocked';
console.log(`${ok ? '✅' : '❌'} fallback preview absent : sessionActive=${n.nightSessionActive}, preferred=${n.nightPreferredSourceMode}`);
if (!ok) failures++;
} catch (e) {
failures++;
console.log(`❌ fallback preview absent : CRASH ${e.message}`);
}
console.log(failures ? `\n${failures} ÉCHEC(S) — NE PAS POUSSER` : '\nA/B PARFAIT — comportement inchangé, prêt à pousser');
process.exit(failures ? 1 : 0);