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:
52
HANDOFF.md
52
HANDOFF.md
@@ -520,3 +520,55 @@ divergence silencieuse entre sourceMode (WINDOWS) et conditions (BCL).
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Document mis à jour le : 07/08/2026 (~19:10) — R1 déployé, comportement inchangé (A/B), production stable*
|
*Document mis à jour le : 07/08/2026 (~19:10) — R1 déployé, comportement inchangé (A/B), production stable*
|
||||||
|
|
||||||
|
## 13. Session du 07/08/2026 (soir) — R3 : Suite de tests golden
|
||||||
|
|
||||||
|
### Ce qui existe désormais (`tests/golden/`)
|
||||||
|
- **`run.mjs`** : exécute le VRAI code des nodes en chaîne complète
|
||||||
|
(INPUT_PARSE → WINDOWS_SOURCEV2 → BATTERY_CLOUD_LOCKS → DECISION_ENGINEV2
|
||||||
|
→ HA_SERVICE_CALLS_OUTPUT) dans une sandbox vm avec **horloge simulée**
|
||||||
|
(Date épinglée par scénario) sur un **snapshot HA réel** modifié par overrides.
|
||||||
|
- **`fixture-snapshot.json`** : snapshot production capturé depuis l'exécution
|
||||||
|
n8n 975833 (173 champs, re-capturable via `tools/fetch-snapshot-fixture.mjs`).
|
||||||
|
- **`scenarios.mjs`** : 16 scénarios avec attendus FIGÉS après revue manuelle :
|
||||||
|
6 nuit HC (P7 ON/OFF, sous-cible, hard floor, sans véhicule, EDF ON→start),
|
||||||
|
4 solaire (matin fort, matin nuageux, abondance P6, provocation P3),
|
||||||
|
garde SOC midi HC, force_grid, force_charge nuit, stop, overload, débranché.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
- `node tests/golden/run.mjs` → 16/16 ✅ ou liste des diffs + exit 1
|
||||||
|
- `--print` : affiche les résumés obtenus (pour autoriser une évolution)
|
||||||
|
- `--json` : sortie machine (utilisée pour figer les attendus)
|
||||||
|
- `--only=<motif>` : filtre par id de scénario
|
||||||
|
|
||||||
|
### Intégration au déploiement (le point clé)
|
||||||
|
`tools/n8n-push.mjs` exécute la suite AVANT tout PUT du workflow EVCS :
|
||||||
|
**golden rouge = rien n'est poussé**. Échappatoire consciente : `--skip-golden`.
|
||||||
|
Évolution volontaire du comportement → mettre à jour les attendus dans
|
||||||
|
`scenarios.mjs` DANS LE MÊME COMMIT que le patch.
|
||||||
|
|
||||||
|
### Validation par mutation
|
||||||
|
P7 volontairement cassé (`nightGridHcPreferred = false`) → la suite détecte
|
||||||
|
2 échecs avec diffs précis (sourceMode, takeover, EDF) et bloque le push ;
|
||||||
|
restauration → 16/16. Le filet fonctionne.
|
||||||
|
|
||||||
|
### Résumé testé par scénario
|
||||||
|
profile, sourceMode, state, decision, desiredCharging, currentA,
|
||||||
|
takeoverNeeded, action EDF ON, service helper takeover.
|
||||||
|
|
||||||
|
### Notes de comportement figées (découvertes en écrivant les attendus)
|
||||||
|
- SOC 20 la nuit avec maison sur batterie (−14 A) → `battery_current_hard_stop`
|
||||||
|
(bande lt25 : hard −8 A) + demande réseau simultanée. Protection cohérente.
|
||||||
|
- `force_charge_now` la nuit (toggle HC ON) → `start_manual_local_min_current`
|
||||||
|
ET takeover helper ON (session nuit active) : EDF peut venir en renfort. Voulu.
|
||||||
|
- Garde SOC>35 midi : sourceMode `grid_hc_allowed` mais `idle_wait_conditions`
|
||||||
|
(le grid HC midi ne s'active que SOC ≤ 35) — comportement épinglé par le test.
|
||||||
|
|
||||||
|
### Prochaines étapes du plan
|
||||||
|
- R2 : git source de vérité (src/nodes/*.js + assemblage dans n8n-push).
|
||||||
|
- Agent IA planificateur en mode ombre (après R2).
|
||||||
|
- Enrichir la suite au fil des incidents : chaque bug corrigé = +1 scénario.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis à jour le : 07/08/2026 (~19:20) — R3 en place, 16/16, push bloquant*
|
||||||
|
|
||||||
|
|||||||
175
tests/golden/fixture-snapshot.json
Normal file
175
tests/golden/fixture-snapshot.json
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
{
|
||||||
|
"ac_loads_max_safe_w": "9000.0",
|
||||||
|
"afternoon_battery_max_discharge_a": "unknown",
|
||||||
|
"afternoon_probe_pv_avg_min_w": "1000.0",
|
||||||
|
"afternoon_probe_soc_min": "85.0",
|
||||||
|
"allow_grid_hc_midday": "on",
|
||||||
|
"allow_grid_hc_night": "on",
|
||||||
|
"base_load_remaining_kwh": "2.2",
|
||||||
|
"batt_charge_reserve_25_40_a": "110.0",
|
||||||
|
"batt_charge_reserve_40_60_a": "70.0",
|
||||||
|
"batt_charge_reserve_60_80_a": "40.0",
|
||||||
|
"batt_charge_reserve_80_90_a": "10.0",
|
||||||
|
"batt_charge_reserve_gt90_a": "5.0",
|
||||||
|
"batt_charge_reserve_lt25_a": "150.0",
|
||||||
|
"batt_discharge_hard_25_40_a": "-15.0",
|
||||||
|
"batt_discharge_hard_40_60_a": "-30.0",
|
||||||
|
"batt_discharge_hard_60_80_a": "-50.0",
|
||||||
|
"batt_discharge_hard_80_90_a": "-80.0",
|
||||||
|
"batt_discharge_hard_gt90_a": "-100.0",
|
||||||
|
"batt_discharge_hard_lt25_a": "-8.0",
|
||||||
|
"batt_discharge_soft_25_40_a": "-5.0",
|
||||||
|
"batt_discharge_soft_40_60_a": "-12.0",
|
||||||
|
"batt_discharge_soft_60_80_a": "-20.0",
|
||||||
|
"batt_discharge_soft_80_90_a": "-40.0",
|
||||||
|
"batt_discharge_soft_gt90_a": "-50.0",
|
||||||
|
"batt_discharge_soft_lt25_a": "-3.0",
|
||||||
|
"batt_hard_tol_25_40_sec": "5.0",
|
||||||
|
"batt_hard_tol_40_60_sec": "10.0",
|
||||||
|
"batt_hard_tol_60_80_sec": "15.0",
|
||||||
|
"batt_hard_tol_80_90_sec": "20.0",
|
||||||
|
"batt_hard_tol_gt90_sec": "20.0",
|
||||||
|
"batt_hard_tol_lt25_sec": "2.0",
|
||||||
|
"batt_recovery_25_40_min": "12.0",
|
||||||
|
"batt_recovery_40_60_min": "10.0",
|
||||||
|
"batt_recovery_60_80_min": "8.0",
|
||||||
|
"batt_recovery_80_90_min": "6.0",
|
||||||
|
"batt_recovery_gt90_min": "6.0",
|
||||||
|
"batt_recovery_lt25_min": "15.0",
|
||||||
|
"batt_soft_tol_25_40_min": "2.0",
|
||||||
|
"batt_soft_tol_40_60_min": "4.0",
|
||||||
|
"batt_soft_tol_60_80_min": "6.0",
|
||||||
|
"batt_soft_tol_80_90_min": "10.0",
|
||||||
|
"batt_soft_tol_gt90_min": "12.0",
|
||||||
|
"batt_soft_tol_lt25_min": "1.0",
|
||||||
|
"battery_current_a": "2.799999952316284",
|
||||||
|
"battery_discharge_hard_a": "-25.0",
|
||||||
|
"battery_discharge_soft_a": "0.0",
|
||||||
|
"battery_full_forecast_min_kwh": "4.5",
|
||||||
|
"battery_full_soc": "90.0",
|
||||||
|
"battery_neutral_band_a": "0.0",
|
||||||
|
"battery_reserve_up_a": "8.0",
|
||||||
|
"blocked_until": "2026-08-07 19:08:30",
|
||||||
|
"budget_remaining_kwh": "-10.11",
|
||||||
|
"charge_permission_now": "off",
|
||||||
|
"cloud_debt_kwh": "0.0",
|
||||||
|
"cloud_debt_max_kwh": "1.2",
|
||||||
|
"cloud_debt_started_at": "2026-08-06 19:41:30",
|
||||||
|
"cloud_tolerance_minutes": "20.0",
|
||||||
|
"control_latch": "off",
|
||||||
|
"controller_enabled": "on",
|
||||||
|
"current_change_cooldown_seconds": "30.0",
|
||||||
|
"current_max_auto_a": "22.0",
|
||||||
|
"current_min_a": "6.0",
|
||||||
|
"current_step_down_a": "2.0",
|
||||||
|
"current_step_up_a": "1.0",
|
||||||
|
"day_start_time": "08:30:00",
|
||||||
|
"day_stop_time": "20:15:00",
|
||||||
|
"departure_priority_enabled": "on",
|
||||||
|
"departure_time": "08:50:00",
|
||||||
|
"end_of_day_soc_guard": "85.0",
|
||||||
|
"end_of_day_soc_target": "85.0",
|
||||||
|
"evcs_ac_power_w": "0.0",
|
||||||
|
"evcs_auto_start_switch": "off",
|
||||||
|
"evcs_charging_switch": "off",
|
||||||
|
"evcs_command_current_a": "6",
|
||||||
|
"evcs_current_a": "0.0",
|
||||||
|
"evcs_delivery_noise_ema": "0.003",
|
||||||
|
"evcs_delivery_ratio_ema": "0.001",
|
||||||
|
"evcs_device_connected": "on",
|
||||||
|
"evcs_mode_fallback": "unknown",
|
||||||
|
"evcs_mode_sensor": "Manuel",
|
||||||
|
"evcs_no_demand_filter_eval_at": "2026-08-07 19:08:30",
|
||||||
|
"evcs_no_demand_score_ema": "0.002",
|
||||||
|
"evcs_no_demand_semantic_active": "off",
|
||||||
|
"evcs_no_demand_since_at": "2026-08-07 18:26:00",
|
||||||
|
"evcs_priority_manual_mode": "normal",
|
||||||
|
"evcs_priority_manual_override": "off",
|
||||||
|
"evcs_priority_manual_reason": "unknown",
|
||||||
|
"evcs_priority_manual_until": "2026-04-14 00:00:00",
|
||||||
|
"evcs_set_current_a": "6",
|
||||||
|
"evcs_status_raw": "0",
|
||||||
|
"evcs_vehicle_connected": "off",
|
||||||
|
"evening_battery_max_discharge_a": "-30.0",
|
||||||
|
"evening_soc_start_min": "85.0",
|
||||||
|
"evening_soc_stop_min": "80.0",
|
||||||
|
"evening_start_time": "17:00:00",
|
||||||
|
"evening_stop_time": "20:30:00",
|
||||||
|
"flexible_safety_margin_kwh": "0.9",
|
||||||
|
"force_mode_auto_revert_minutes": "120.0",
|
||||||
|
"forecast_commit_min_kwh": "10.0",
|
||||||
|
"forecast_commit_soc_floor": "35.0",
|
||||||
|
"forecast_mode_enabled": "off",
|
||||||
|
"grid_battery_hard_a": "-20.0",
|
||||||
|
"grid_battery_soft_a": "-10.0",
|
||||||
|
"grid_current_max_a": "20.0",
|
||||||
|
"grid_power_w": "0",
|
||||||
|
"grid_state": "off",
|
||||||
|
"hard_stop_enabled": "on",
|
||||||
|
"hc_midday_start_time": "12:24:00",
|
||||||
|
"hc_midday_stop_time": "14:24:00",
|
||||||
|
"hc_night_start_time": "01:24:00",
|
||||||
|
"hc_night_stop_time": "07:24:00",
|
||||||
|
"house_ac_headroom_w": "5000.0",
|
||||||
|
"house_ac_power_prev_w": "1842.0",
|
||||||
|
"house_ac_power_w": "1842.0",
|
||||||
|
"house_spike_grace_minutes": "10.0",
|
||||||
|
"last_block_reason": "vehicle_not_connected",
|
||||||
|
"last_current_change_at": "2026-08-07 18:26:30",
|
||||||
|
"last_signature": "vehicle_not_connected|profile=evening_high_soc|source=solar_only|chg=0|perm=0|l=0|cur=6|block=vehicle_not_connected|stop=0|prio=0|night=0|ntrg=85|nurg=0|vdf=0|vdfs=0.002",
|
||||||
|
"last_start_at": "2026-08-07 18:26:30",
|
||||||
|
"last_stop_at": "2026-08-07 18:26:00",
|
||||||
|
"midday_battery_max_discharge_a": "-40.0",
|
||||||
|
"midday_probe_pv_avg_min_w": "0.0",
|
||||||
|
"midday_soc_min": "75.0",
|
||||||
|
"min_run_minutes": "29.0",
|
||||||
|
"min_stop_minutes": "0.0",
|
||||||
|
"morning_battery_max_discharge_a": "-20.0",
|
||||||
|
"morning_probe_pv_avg_min_w": "1500.0",
|
||||||
|
"morning_probe_pv_trend_min_w": "-2000.0",
|
||||||
|
"morning_soc_min": "35.0",
|
||||||
|
"morning_start_time": "07:30:00",
|
||||||
|
"morning_stop_time": "11:00:00",
|
||||||
|
"night_takeover_prev": "off",
|
||||||
|
"notifications_enabled": "on",
|
||||||
|
"operating_mode_helper": "evening_high_soc",
|
||||||
|
"overload_hold_seconds": "30.0",
|
||||||
|
"overload_step_down_a": "2.0",
|
||||||
|
"overload_stop_after_seconds": "120.0",
|
||||||
|
"probe_current_a": "6.0",
|
||||||
|
"probe_enabled": "on",
|
||||||
|
"probe_minutes": "3.0",
|
||||||
|
"probe_retry_minutes": "45.0",
|
||||||
|
"probe_started_at": "2026-03-31 00:00:00",
|
||||||
|
"projected_end_soc_after_charge": "35.2",
|
||||||
|
"pv_hold_min_w": "500.0",
|
||||||
|
"pv_power_10m_avg_w": "1778",
|
||||||
|
"pv_power_w": "1851.9319741067588",
|
||||||
|
"pv_start_min_w": "700.0",
|
||||||
|
"pv_trend_w": "unknown",
|
||||||
|
"required_energy_kwh": "10.5",
|
||||||
|
"soc_comfort_target": "85.0",
|
||||||
|
"soc_percent": "93.0",
|
||||||
|
"soc_start_min": "80.0",
|
||||||
|
"soc_stop_min": "29.0",
|
||||||
|
"solar_potential_grace_minutes": "12.0",
|
||||||
|
"step_down_cooldown_seconds": "60.0",
|
||||||
|
"step_up_cooldown_seconds": "30.0",
|
||||||
|
"target_current_auto_a": "6.0",
|
||||||
|
"today_remaining_kwh": "3.486",
|
||||||
|
"tomorrow_kwh": "57.263",
|
||||||
|
"vebus_ac_out_l1_current_a": "8.0",
|
||||||
|
"vebus_ac_out_l1_power_w": "1846",
|
||||||
|
"vebus_ac_out_l1_voltage_v": "229.75999450683594",
|
||||||
|
"vebus_dc_current_a": "-38.400001525878906",
|
||||||
|
"vebus_dc_power_w": "-1842.0",
|
||||||
|
"vebus_dc_voltage_v": "51.279998779296875",
|
||||||
|
"vebus_l1_low_battery": "off",
|
||||||
|
"vebus_l1_overload": "off",
|
||||||
|
"vebus_low_battery": "off",
|
||||||
|
"vebus_main_state": "4",
|
||||||
|
"vebus_overload": "off",
|
||||||
|
"vebus_state": "9",
|
||||||
|
"victron_acin1_available": "0",
|
||||||
|
"victron_evcs_strategy": "auto_solar_plus"
|
||||||
|
}
|
||||||
176
tests/golden/run.mjs
Normal file
176
tests/golden/run.mjs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// R3 — Suite de tests GOLDEN du workflow EVCS Surplus Controller V2.6.
|
||||||
|
// Exécute le VRAI code des nodes (chaîne complète INPUT_PARSE → WINDOWS_SOURCEV2
|
||||||
|
// → BATTERY_CLOUD_LOCKS → DECISION_ENGINEV2 → HA_SERVICE_CALLS_OUTPUT → Build)
|
||||||
|
// sur un snapshot HA réel modifié par scénario, avec horloge simulée.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// node tests/golden/run.mjs # assertions (exit 1 si échec)
|
||||||
|
// node tests/golden/run.mjs --print # affiche les résumés (pour figer les attendus)
|
||||||
|
// node tests/golden/run.mjs --only=night-hc # filtre par label
|
||||||
|
//
|
||||||
|
// RÈGLE : cette suite DOIT passer avant tout push (intégrée à tools/n8n-push.mjs).
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import vm from 'vm';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { scenarios } from './scenarios.mjs';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = path.resolve(__dirname, '../..');
|
||||||
|
const WORKFLOW_FILE = path.join(ROOT, 'workflows/evcs-surplus-controller-v2.6.json');
|
||||||
|
const FIXTURE_FILE = path.join(__dirname, 'fixture-snapshot.json');
|
||||||
|
|
||||||
|
const PRINT = process.argv.includes('--print');
|
||||||
|
const JSON_OUT = process.argv.includes('--json');
|
||||||
|
const only = (process.argv.find(a => a.startsWith('--only=')) || '').slice(7);
|
||||||
|
|
||||||
|
const wf = JSON.parse(fs.readFileSync(WORKFLOW_FILE, 'utf8'));
|
||||||
|
const fixture = JSON.parse(fs.readFileSync(FIXTURE_FILE, 'utf8'));
|
||||||
|
|
||||||
|
const CHAIN = [
|
||||||
|
'INPUT_PARSE',
|
||||||
|
'WINDOWS_SOURCEV2',
|
||||||
|
'BATTERY_CLOUD_LOCKS',
|
||||||
|
'DECISION_ENGINEV2',
|
||||||
|
'HA_SERVICE_CALLS_OUTPUT',
|
||||||
|
];
|
||||||
|
const nodeCode = Object.fromEntries(
|
||||||
|
CHAIN.map(n => [n, wf.nodes.find(x => x.name === n).parameters.jsCode])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Horloge simulée : Date épinglée à l'instant du scénario dans la sandbox.
|
||||||
|
function makeFakeDate(fixedMs) {
|
||||||
|
return class FakeDate extends Date {
|
||||||
|
constructor(...args) {
|
||||||
|
if (args.length === 0) super(fixedMs);
|
||||||
|
else super(...args);
|
||||||
|
}
|
||||||
|
static now() { return fixedMs; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function runNode(jsCode, inputJson, fixedMs) {
|
||||||
|
const ctx = {
|
||||||
|
$input: { first: () => ({ json: inputJson }) },
|
||||||
|
console, Intl, Math, Number, Object, String, JSON, Array, Boolean,
|
||||||
|
Date: makeFakeDate(fixedMs),
|
||||||
|
};
|
||||||
|
vm.createContext(ctx);
|
||||||
|
const out = vm.runInContext(`(function(){\n${jsCode}\n})()`, ctx);
|
||||||
|
return out[0].json;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overrides par défaut : timers neutres relatifs à l'instant du scénario
|
||||||
|
// (min_stop écoulé, pas de blocage anti-cliquetis, filtre no-demand au repos).
|
||||||
|
function stamp(ms) {
|
||||||
|
const p = new Intl.DateTimeFormat('fr-FR', {
|
||||||
|
timeZone: 'Europe/Paris', year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
||||||
|
}).formatToParts(new Date(ms));
|
||||||
|
const g = t => p.find(x => x.type === t).value;
|
||||||
|
return `${g('year')}-${g('month')}-${g('day')} ${g('hour')}:${g('minute')}:${g('second')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseOverrides(fixedMs) {
|
||||||
|
return {
|
||||||
|
last_start_at: stamp(fixedMs - 2 * 3600000),
|
||||||
|
last_stop_at: stamp(fixedMs - 2 * 3600000 + 30000),
|
||||||
|
last_current_change_at: stamp(fixedMs - 2 * 3600000),
|
||||||
|
blocked_until: stamp(fixedMs - 3600000),
|
||||||
|
last_block_reason: 'none',
|
||||||
|
evcs_no_demand_filter_eval_at: stamp(fixedMs - 30000),
|
||||||
|
evcs_no_demand_semantic_active: 'off',
|
||||||
|
evcs_delivery_ratio_ema: '0.9',
|
||||||
|
evcs_delivery_noise_ema: '0.01',
|
||||||
|
evcs_no_demand_score_ema: '0.0',
|
||||||
|
probe_started_at: '',
|
||||||
|
cloud_debt_kwh: '0.0',
|
||||||
|
night_takeover_prev: 'off',
|
||||||
|
grid_state: 'off',
|
||||||
|
victron_acin1_available: '0',
|
||||||
|
evcs_charging_switch: 'off',
|
||||||
|
control_latch: 'off',
|
||||||
|
charge_permission_now: 'off',
|
||||||
|
controller_enabled: 'on',
|
||||||
|
vebus_overload: 'off',
|
||||||
|
vebus_l1_overload: 'off',
|
||||||
|
vebus_low_battery: 'off',
|
||||||
|
vebus_l1_low_battery: 'off',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(out) {
|
||||||
|
const c = out.computed || {};
|
||||||
|
const d = out.decision || {};
|
||||||
|
const calls = out.ha_service_calls || {};
|
||||||
|
return {
|
||||||
|
profile: c.profile,
|
||||||
|
sourceMode: c.source_mode,
|
||||||
|
state: d.state,
|
||||||
|
decision: d.current_decision,
|
||||||
|
desiredCharging: d.desired_charging,
|
||||||
|
currentA: d.desired_current_a,
|
||||||
|
takeoverNeeded: !!c.night_grid_takeover_needed,
|
||||||
|
edfOnAction: !!calls.edf_general_ok_on?.execute,
|
||||||
|
takeoverHelperService: calls.helper_night_grid_takeover_needed?.service || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonResults = {};
|
||||||
|
let failures = 0, ran = 0;
|
||||||
|
for (const sc of scenarios) {
|
||||||
|
if (only && !sc.id.includes(only)) continue;
|
||||||
|
ran++;
|
||||||
|
const fixedMs = new Date(sc.time).getTime();
|
||||||
|
if (!Number.isFinite(fixedMs)) throw new Error(`${sc.id}: time invalide`);
|
||||||
|
|
||||||
|
const raw = { ...fixture, ...baseOverrides(fixedMs), ...sc.overrides };
|
||||||
|
let payload = { data: JSON.stringify(raw) };
|
||||||
|
try {
|
||||||
|
for (const nodeName of CHAIN) payload = runNode(nodeCode[nodeName], payload, fixedMs);
|
||||||
|
} catch (e) {
|
||||||
|
failures++;
|
||||||
|
console.log(`💥 ${sc.id} : CRASH dans la chaîne — ${e.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const got = summarize(payload);
|
||||||
|
|
||||||
|
if (JSON_OUT) {
|
||||||
|
jsonResults[sc.id] = got;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PRINT) {
|
||||||
|
console.log(`\n─── ${sc.id} | ${sc.label}`);
|
||||||
|
console.log(' expect:', JSON.stringify(got));
|
||||||
|
if (payload.decision?.short_reason) console.log(' raison:', payload.decision.short_reason);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffs = [];
|
||||||
|
for (const [k, v] of Object.entries(sc.expect || {})) {
|
||||||
|
if (JSON.stringify(got[k]) !== JSON.stringify(v)) diffs.push(`${k}: attendu ${JSON.stringify(v)}, obtenu ${JSON.stringify(got[k])}`);
|
||||||
|
}
|
||||||
|
if (diffs.length) {
|
||||||
|
failures++;
|
||||||
|
console.log(`❌ ${sc.id} | ${sc.label}`);
|
||||||
|
for (const d of diffs) console.log(` ${d}`);
|
||||||
|
if (payload.decision?.short_reason) console.log(` raison: ${payload.decision.short_reason}`);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ ${sc.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JSON_OUT) {
|
||||||
|
console.log(JSON.stringify(jsonResults, null, 2));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PRINT) {
|
||||||
|
console.log(failures
|
||||||
|
? `\n${failures}/${ran} ÉCHEC(S) — NE PAS POUSSER EN PRODUCTION`
|
||||||
|
: `\nGOLDEN OK — ${ran}/${ran} scénarios conformes`);
|
||||||
|
process.exit(failures ? 1 : 0);
|
||||||
|
}
|
||||||
366
tests/golden/scenarios.mjs
Normal file
366
tests/golden/scenarios.mjs
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
// R3 — Scénarios golden EVCS. Chaque scénario = instant simulé + overrides du
|
||||||
|
// snapshot HA réel (tests/golden/fixture-snapshot.json) + résumé attendu.
|
||||||
|
// Les attendus sont FIGÉS après vérification manuelle : un diff = régression
|
||||||
|
// (ou évolution volontaire → mettre à jour l'attendu dans le même commit).
|
||||||
|
//
|
||||||
|
// Fenêtres (helpers réels) : night_hc 01:24-07:24 | morning 07:30-11:00 |
|
||||||
|
// midday 12:24-14:24 | evening 17:00-20:30 | sinon outside_window.
|
||||||
|
// Seuils : hard floor nuit 22 | cible matin forecast fort 35 (tomorrow_kwh 57) |
|
||||||
|
// soc_start_min 80 | morning_soc_min 35 | midday_soc_min 75.
|
||||||
|
|
||||||
|
const NIGHT = '2026-08-08T01:30:00+02:00';
|
||||||
|
const MORNING = '2026-08-08T09:00:00+02:00';
|
||||||
|
const MIDDAY = '2026-08-08T13:00:00+02:00';
|
||||||
|
const AFTERNOON = '2026-08-08T15:30:00+02:00';
|
||||||
|
const EVENING = '2026-08-08T18:00:00+02:00';
|
||||||
|
|
||||||
|
const VEHICLE_NIGHT = {
|
||||||
|
evcs_vehicle_connected: 'on',
|
||||||
|
evcs_device_connected: 'on',
|
||||||
|
pv_power_w: '0',
|
||||||
|
pv_power_10m_avg_w: '0',
|
||||||
|
pv_trend_w: '0',
|
||||||
|
house_ac_power_w: '700',
|
||||||
|
vebus_ac_out_l1_power_w: '700',
|
||||||
|
battery_current_a: '-14',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scenarios = [
|
||||||
|
|
||||||
|
// ================= NUIT HC (P7 + historique) =================
|
||||||
|
{
|
||||||
|
id: 'night-hc-p7-toggle-on',
|
||||||
|
label: 'Nuit HC, VW branchée, toggle HC nuit ON, SOC 53, EDF OFF → demande réseau (P7)',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: { ...VEHICLE_NIGHT, soc_percent: '53', allow_grid_hc_night: 'on' },
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'grid_hc_allowed',
|
||||||
|
state: 'night_wait_grid_takeover',
|
||||||
|
decision: 'hold',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: true,
|
||||||
|
edfOnAction: true,
|
||||||
|
takeoverHelperService: 'turn_on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night-hc-p7-edf-on-start',
|
||||||
|
label: 'Nuit HC, cycle suivant : EDF ON, batterie tenue +2A → démarrage charge HC',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT, soc_percent: '53', allow_grid_hc_night: 'on',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1',
|
||||||
|
battery_current_a: '2', night_takeover_prev: 'on',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'grid_hc_allowed',
|
||||||
|
state: 'start_grid_hc_min_current',
|
||||||
|
decision: 'start',
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: true,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night-hc-toggle-off-historique',
|
||||||
|
label: 'Nuit HC, toggle OFF → comportement historique battery_assist_night (idle)',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: { ...VEHICLE_NIGHT, soc_percent: '53', allow_grid_hc_night: 'off' },
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'battery_assist_night',
|
||||||
|
state: 'idle_wait_conditions',
|
||||||
|
decision: 'hold',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night-soc-sous-cible',
|
||||||
|
label: 'Nuit HC, SOC 30 < cible matin 35 → grid_forced',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: { ...VEHICLE_NIGHT, soc_percent: '30', allow_grid_hc_night: 'on' },
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'grid_forced',
|
||||||
|
state: 'night_wait_grid_takeover',
|
||||||
|
decision: 'hold',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: true,
|
||||||
|
edfOnAction: true,
|
||||||
|
takeoverHelperService: 'turn_on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night-hard-floor',
|
||||||
|
label: 'Nuit HC, SOC 20 ≤ hard floor 22 → hard stop protection batterie + demande réseau',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: { ...VEHICLE_NIGHT, soc_percent: '20', allow_grid_hc_night: 'on' },
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'grid_forced',
|
||||||
|
state: 'battery_current_hard_stop',
|
||||||
|
decision: 'stop_hard',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: true,
|
||||||
|
edfOnAction: true,
|
||||||
|
takeoverHelperService: 'turn_on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night-sans-vehicule',
|
||||||
|
label: 'Nuit HC sans véhicule → bloqué, takeover relâché',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: { ...VEHICLE_NIGHT, evcs_vehicle_connected: 'off', soc_percent: '53' },
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'blocked',
|
||||||
|
state: 'vehicle_not_connected',
|
||||||
|
decision: 'stop_not_connected',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ================= JOUR SOLAIRE =================
|
||||||
|
{
|
||||||
|
id: 'morning-solaire-fort',
|
||||||
|
label: 'Matin, PV 4,5 kW, SOC 60, batterie +10A → charge solaire',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '60', pv_power_w: '4500', pv_power_10m_avg_w: '4200',
|
||||||
|
pv_trend_w: '100', battery_current_a: '10',
|
||||||
|
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'morning_solar',
|
||||||
|
sourceMode: 'solar_only',
|
||||||
|
state: 'start_min_current',
|
||||||
|
decision: 'start',
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'morning-nuageux',
|
||||||
|
label: 'Matin nuageux, PV 300 W, SOC 40 → pas de démarrage',
|
||||||
|
time: MORNING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '40', pv_power_w: '300', pv_power_10m_avg_w: '250',
|
||||||
|
pv_trend_w: '-50', battery_current_a: '-3',
|
||||||
|
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'morning_solar',
|
||||||
|
sourceMode: 'blocked',
|
||||||
|
state: 'idle_wait_source',
|
||||||
|
decision: 'hold',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'midday-abondance-p6',
|
||||||
|
label: 'Midi abondance P6 : PV avg 4,5 kW + batterie +25A, SOC 66 < plancher 75 → démarre quand même',
|
||||||
|
time: MIDDAY,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '66', pv_power_w: '5000', pv_power_10m_avg_w: '4500',
|
||||||
|
pv_trend_w: '200', battery_current_a: '25',
|
||||||
|
house_ac_power_w: '1200', vebus_ac_out_l1_power_w: '1200',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'midday_hc_or_solar',
|
||||||
|
sourceMode: 'solar_only',
|
||||||
|
state: 'start_min_current',
|
||||||
|
decision: 'start',
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'midday-hc-garde-soc',
|
||||||
|
label: 'Midi sans PV, SOC 50 > 35, HC midi autorisé + EDF ON → grid proposé mais gardé (soc>35)',
|
||||||
|
time: MIDDAY,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '50', pv_power_w: '100', pv_power_10m_avg_w: '100',
|
||||||
|
pv_trend_w: '0', battery_current_a: '-4',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1',
|
||||||
|
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'midday_hc_or_solar',
|
||||||
|
sourceMode: 'grid_hc_allowed',
|
||||||
|
state: 'idle_wait_conditions',
|
||||||
|
decision: 'hold',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apresmidi-provocation-p3',
|
||||||
|
label: 'Après-midi hors fenêtre, SOC 95, PV bridé 300 W (< hold 500) → provocation PV (solar_potential)',
|
||||||
|
time: AFTERNOON,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '95', pv_power_w: '300', pv_power_10m_avg_w: '300',
|
||||||
|
pv_trend_w: '0', battery_current_a: '2',
|
||||||
|
house_ac_power_w: '600', vebus_ac_out_l1_power_w: '600',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'outside_window',
|
||||||
|
sourceMode: 'solar_potential',
|
||||||
|
state: 'start_forecast_solar_potential',
|
||||||
|
decision: 'start',
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ================= STRATÉGIES MANUELLES =================
|
||||||
|
{
|
||||||
|
id: 'force-grid-jour',
|
||||||
|
label: 'force_grid_now l\'après-midi, EDF ON, SOC 60 → charge réseau forcée',
|
||||||
|
time: AFTERNOON,
|
||||||
|
overrides: {
|
||||||
|
victron_evcs_strategy: 'force_grid_now',
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '60', pv_power_w: '500', pv_power_10m_avg_w: '500',
|
||||||
|
pv_trend_w: '0', battery_current_a: '2',
|
||||||
|
grid_state: 'on', victron_acin1_available: '1',
|
||||||
|
house_ac_power_w: '900', vebus_ac_out_l1_power_w: '900',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'outside_window',
|
||||||
|
sourceMode: 'grid_forced',
|
||||||
|
state: 'start_grid_forced_min_current',
|
||||||
|
decision: 'start',
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'force-charge-nuit',
|
||||||
|
label: 'force_charge_now la nuit (toggle HC ON) → démarrage manuel local (non-régression P7)',
|
||||||
|
time: NIGHT,
|
||||||
|
overrides: {
|
||||||
|
...VEHICLE_NIGHT, victron_evcs_strategy: 'force_charge_now',
|
||||||
|
soc_percent: '60', allow_grid_hc_night: 'on',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'night_hc',
|
||||||
|
sourceMode: 'solar_potential',
|
||||||
|
state: 'start_manual_local_min_current',
|
||||||
|
decision: 'start',
|
||||||
|
desiredCharging: true,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: true,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_on',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'strategie-stop',
|
||||||
|
label: 'Stratégie stop en journée → tout bloqué',
|
||||||
|
time: AFTERNOON,
|
||||||
|
overrides: {
|
||||||
|
victron_evcs_strategy: 'stop',
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '80', pv_power_w: '3000', pv_power_10m_avg_w: '3000',
|
||||||
|
battery_current_a: '10',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'outside_window',
|
||||||
|
sourceMode: 'blocked',
|
||||||
|
state: 'stopped_user',
|
||||||
|
decision: 'stop_stop_mode',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// ================= SÉCURITÉ =================
|
||||||
|
{
|
||||||
|
id: 'securite-overload',
|
||||||
|
label: 'Charge solaire en cours + surcharge VE.Bus → protection (stop/step-down)',
|
||||||
|
time: EVENING,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'on', evcs_device_connected: 'on',
|
||||||
|
evcs_charging_switch: 'on', control_latch: 'on', charge_permission_now: 'on',
|
||||||
|
last_start_at: '2026-08-08 17:30:00',
|
||||||
|
soc_percent: '90', pv_power_w: '3500', pv_power_10m_avg_w: '3400',
|
||||||
|
battery_current_a: '5', evcs_current_a: '13', evcs_set_current_a: '13',
|
||||||
|
vebus_overload: 'on', vebus_l1_overload: 'on',
|
||||||
|
house_ac_power_w: '4800', vebus_ac_out_l1_power_w: '4800',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'evening_high_soc',
|
||||||
|
sourceMode: 'solar_only',
|
||||||
|
state: 'vebus_overload_stop_min_current',
|
||||||
|
decision: 'stop',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vehicule-debranche-jour',
|
||||||
|
label: 'Après-midi véhicule débranché → stop_not_connected (état de repos)',
|
||||||
|
time: AFTERNOON,
|
||||||
|
overrides: {
|
||||||
|
evcs_vehicle_connected: 'off', evcs_device_connected: 'on',
|
||||||
|
soc_percent: '93', pv_power_w: '1800', pv_power_10m_avg_w: '1800',
|
||||||
|
battery_current_a: '3',
|
||||||
|
},
|
||||||
|
expect: {
|
||||||
|
profile: 'outside_window',
|
||||||
|
sourceMode: 'solar_only',
|
||||||
|
state: 'vehicle_not_connected',
|
||||||
|
decision: 'stop_not_connected',
|
||||||
|
desiredCharging: false,
|
||||||
|
currentA: 6,
|
||||||
|
takeoverNeeded: false,
|
||||||
|
edfOnAction: false,
|
||||||
|
takeoverHelperService: 'turn_off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
50
tools/fetch-snapshot-fixture.mjs
Normal file
50
tools/fetch-snapshot-fixture.mjs
Normal 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(', ')}`);
|
||||||
@@ -86,6 +86,25 @@ if (syntaxErrors) {
|
|||||||
}
|
}
|
||||||
console.log('Syntaxe OK sur tous les nodes jsCode.');
|
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
|
// 4. PUT
|
||||||
const putRes = await fetch(`${url}/api/v1/workflows/${WORKFLOW_ID}`, {
|
const putRes = await fetch(`${url}/api/v1/workflows/${WORKFLOW_ID}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
Reference in New Issue
Block a user