R3: suite de tests golden — 16 scénarios, bloquante avant push

- tests/golden/run.mjs : chaîne complète des 5 nodes (code réel) en sandbox vm,
  horloge simulée par scénario, snapshot HA réel comme fixture.
- 16 scénarios attendus figés après revue : nuit HC (P7), solaire, abondance P6,
  provocation P3, gardes SOC, force_grid/force_charge, stop, overload.
- n8n-push.mjs exécute la suite avant tout PUT du workflow EVCS :
  golden rouge = push refusé (--skip-golden en échappatoire consciente).
- Validé par mutation : P7 cassé volontairement -> 2 échecs détectés + push bloqué.
- tools/fetch-snapshot-fixture.mjs : recapture d'un snapshot production.
This commit is contained in:
Tallagrand Thierry
2026-08-07 19:16:03 +02:00
parent 9c3a0a9c6e
commit d1efeac44c
6 changed files with 838 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
#!/usr/bin/env node
// Capture un snapshot HA RÉEL depuis une exécution n8n récente du workflow EVCS
// → tests/golden/fixture-snapshot.json (base des scénarios golden).
// Usage: node tools/fetch-snapshot-fixture.mjs [executionId]
import fs from 'fs';
import os from 'os';
import path from 'path';
const WORKFLOW_ID = 'ATswvGfG4KJUls0K';
const SNAPSHOT_NODE = 'HA Snapshot EVCS Surplus Controller V2.2';
const OUT = 'tests/golden/fixture-snapshot.json';
function loadApiConfig() {
const cfgPath = path.join(os.homedir(), '.config/opencode/opencode.json');
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
const env = cfg?.mcp?.['n8n-mcp']?.environment || {};
return {
url: (env.N8N_API_URL || '').replace(/\/api\/v1\/?$/, ''),
key: env.N8N_API_KEY
};
}
const { url, key } = loadApiConfig();
const headers = { 'X-N8N-API-KEY': key };
let execId = process.argv[2];
if (!execId) {
const res = await fetch(`${url}/api/v1/executions?workflowId=${WORKFLOW_ID}&status=success&limit=1`, { headers });
const list = await res.json();
execId = list.data?.[0]?.id;
if (!execId) { console.error('Aucune exécution trouvée'); process.exit(1); }
}
const res = await fetch(`${url}/api/v1/executions/${execId}?includeData=true`, { headers });
if (!res.ok) { console.error(`GET exécution ${execId}: ${res.status}`); process.exit(1); }
const exec = await res.json();
const runData = exec.data?.resultData?.runData;
if (!runData?.[SNAPSHOT_NODE]) {
console.error(`Node "${SNAPSHOT_NODE}" absent des données. Nodes dispo: ${Object.keys(runData || {}).join(', ')}`);
process.exit(1);
}
const items = runData[SNAPSHOT_NODE][0]?.data?.main?.[0];
if (!items?.length) { console.error('Pas de données de sortie sur le node snapshot'); process.exit(1); }
const snapshot = items[0].json;
fs.mkdirSync(path.dirname(OUT), { recursive: true });
fs.writeFileSync(OUT, JSON.stringify(snapshot, null, 2) + '\n');
console.log(`Fixture écrite: ${OUT} (exécution ${execId}, ${Object.keys(snapshot).length} champs)`);
console.log(`Champs: ${Object.keys(snapshot).sort().join(', ')}`);

View File

@@ -86,6 +86,25 @@ if (syntaxErrors) {
}
console.log('Syntaxe OK sur tous les nodes jsCode.');
// 3bis. Suite golden (R3) : obligatoire pour le workflow EVCS, sauf --skip-golden
const isEvcsWorkflow = path.basename(INPUT) === 'evcs-surplus-controller-v2.6.json';
const skipGolden = process.argv.includes('--skip-golden');
if (isEvcsWorkflow && fs.existsSync('tests/golden/run.mjs')) {
if (skipGolden) {
console.log('⚠️ Suite golden SAUTÉE (--skip-golden) — à n\'utiliser qu\'en connaissance de cause.');
} else {
console.log('Exécution de la suite golden (tests/golden/run.mjs)...');
const { spawnSync } = await import('child_process');
const g = spawnSync('node', ['tests/golden/run.mjs'], { stdio: 'inherit' });
if (g.status !== 0) {
console.error('ABANDON: la suite golden échoue. Rien n\'a été poussé.');
console.error('(Évolution volontaire du comportement ? Mettre à jour les attendus');
console.error(' dans tests/golden/scenarios.mjs dans le même commit, puis re-pousser.)');
process.exit(1);
}
}
}
// 4. PUT
const putRes = await fetch(`${url}/api/v1/workflows/${WORKFLOW_ID}`, {
method: 'PUT',