- 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.
77 lines
3.7 KiB
JavaScript
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);
|