Plan du planificateur HC ; dialogue avec le conseiller dans HA ; arbitre encadre (question a choix fermes, liste blanche)

This commit is contained in:
Tallagrand Thierry
2026-10-08 07:26:14 +02:00
parent b25770736f
commit dea9c88530
16 changed files with 1114 additions and 3 deletions

24
tests/arbitrage/run.mjs Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
// Tests de l'arbitre encadre (tools/arbitrage.mjs, 08/10/2026).
import { detecterSituation, lireChoix } from '../../tools/arbitrage.mjs';
let f = 0;
const ok = (l, c, d = '') => { if (c) console.log(`✅ ${l}`); else { f++; console.log(`❌ ${l} ${d}`); } };
// Releve reel du 08/10 01:30 : VW bloquee, EDF ferme, session HC.
const nuit0810 = { fenetre: 'nuit', finFenetre: '07:24', contacteurOn: true, voitureBranchee: true, borneW: 0,
sourceMode: 'Bloqué', etat: 'night_no_grid_hc_lock', raison: 'Nuit sans autorisation HC réseau | Nuit EVCS: target=38.8%', vehicule: 'VW', modeNuit: 'sans_edf' };
const s = detecterSituation(nuit0810);
ok('nuit-0810-question', s && s.cas === 'voiture_bloquee_edf_ferme' && /VW est branchee/.test(s.question) && /Nuit sans autorisation HC réseau/.test(s.question), JSON.stringify(s));
ok('nuit-0810-choix-ferme', s && s.choix.length === 2 && s.choix[0].ordre.ordre === 'mode_nuit' && s.choix[0].ordre.valeur === 'complete' && s.choix[1].ordre === null);
const m = detecterSituation({ ...nuit0810, fenetre: 'midi', finFenetre: '14:24' });
ok('midi-force-grid', m && m.choix[0].ordre.ordre === 'strategie' && m.choix[0].ordre.valeur === 'force_grid_now' && /14:24/.test(m.choix[0].libelle));
ok('deja-complete-pas-de-question', detecterSituation({ ...nuit0810, modeNuit: 'complete' }) === null);
ok('hors-hc-rien', detecterSituation({ ...nuit0810, fenetre: null }) === null);
ok('edf-ouvert-rien', detecterSituation({ ...nuit0810, contacteurOn: false }) === null);
ok('voiture-pleine-rien', detecterSituation({ ...nuit0810, etat: 'night_recovery_done_idle' }) === null);
ok('voiture-en-charge-rien', detecterSituation({ ...nuit0810, borneW: 1400, sourceMode: 'Réseau forcé' }) === null);
ok('pas-de-voiture-rien', detecterSituation({ ...nuit0810, voitureBranchee: false }) === null);
ok('choix-1', lireChoix('1', s.choix)?.cle === '1');
ok('choix-texte', lireChoix('je prends le 2', s.choix)?.cle === '2');
ok('choix-inconnu', lireChoix('5', s.choix) === null && lireChoix('oui', s.choix) === null);
console.log(f ? `\n${f} ECHEC(S)` : '\nARBITRAGE OK');
process.exit(f ? 1 : 0);

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env node
// Tests du dialogue avec le conseiller (08/10/2026) : vrai code des noeuds en vm.
import fs from 'fs';
import vm from 'vm';
import path from 'path';
import { fileURLToPath } from 'url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const src = (f) => fs.readFileSync(path.join(ROOT, 'src/conseiller-dialogue', f), 'utf8');
function run(code, inputJson, nodes = {}) {
const ctx = { $input: { first: () => ({ json: inputJson }) }, $: (n) => ({ first: () => ({ json: nodes[n] }) }),
console, Date, Intl, Math, Number, Object, String, JSON, Array, Boolean, Set, Map };
vm.createContext(ctx);
return vm.runInContext(`(function(){\n${code}\n})()`, ctx);
}
let f = 0;
const check = (l, c, d = '') => { if (c) console.log(`✅ ${l}`); else { f++; console.log(`❌ ${l} ${d}`); } };
const snap = (o = {}) => ({ data: JSON.stringify({ advisor_enabled: 'on', question: 'Pourquoi la VW ne charge pas ?',
question_at: 2000, derniere_question_at: 1000, maintenant: '08/10 05:09', soc_percent: '30', battery_current_a: '0.2',
pv_power_w: '0', house_ac_power_w: '1751', grid_power_w: '1908', edf_contacteur: 'on', session_hc: 'on',
vehicule_branche: 'on', vehicule: 'VW', borne_puissance_w: '0', controleur_etat: 'night_no_grid_hc_lock',
controleur_raison: 'Nuit sans autorisation HC réseau', mode_nuit: 'sans_edf', cible_matin: '30.7', acm_courant_max: '2',
bilan_requis: '30.7', revue_jour_date: '2026-10-08', revue_jour_texte: '- EDF ferme en heures pleines de 18:06 a 01:24',
revue_pistes: [{ code: 'edf_heures_pleines', piste: 'recharge de midi' }], ...o }) });
const P = src('BUILD_PROMPT.js'), V = src('VALIDATE_PARSE.js'), B = src('BUILD_HA_CALLS.js');
const p = run(P, snap())[0].json;
check('PROMPT: nouvelle question -> actif', p.enabled === true);
check('PROMPT: les mesures et la revue sont transmises', /SOC 30 %/.test(p.messages[1].content) && /night_no_grid_hc_lock/.test(p.messages[1].content) && /18:06 a 01:24/.test(p.messages[1].content) && /recharge de midi/.test(p.messages[1].content));
check('PROMPT: question deja traitee -> inactif', run(P, snap({ derniere_question_at: 2000 }))[0].json.enabled === false);
check('PROMPT: question vide -> inactif', run(P, snap({ question: '' }))[0].json.enabled === false);
check('PROMPT: kill-switch -> inactif', run(P, snap({ advisor_enabled: 'off' }))[0].json.enabled === false);
const rep = (t, extra = {}) => ({ model: 'qwen3-coder', choices: [{ message: { content: t } }], ...extra });
const v1 = run(V, rep('La VW est bloquee (night_no_grid_hc_lock) : mode sans_edf, SOC 30 %.'), { BUILD_PROMPT: p })[0].json;
check('VALIDATE: chiffres des donnees -> aucun avertissement', v1.status === 'ok' && v1.avertissement === '', JSON.stringify(v1.inventes));
const v2 = run(V, rep('Il faudrait 6,5 kWh pour la remplir.'), { BUILD_PROMPT: p })[0].json;
check('VALIDATE: ANTI-INVENTION — chiffre absent signale', /6.5/.test(v2.avertissement), v2.avertissement);
check('VALIDATE: OpenRouter detecte', run(V, rep('ok', { id: 'gen-1' }), { BUILD_PROMPT: p })[0].json.source === 'openrouter');
const v3 = run(V, { error: 'x' }, { BUILD_PROMPT: p })[0].json;
check('VALIDATE: hors ligne -> llm_offline', v3.status === 'llm_offline');
const v4 = run(V, rep('Courant de la batterie : 0,2 A.'), { BUILD_PROMPT: run(P, snap({ battery_current_a: '0.20000000298023224' }))[0].json })[0].json;
check('VALIDATE: arrondi d une mesure accepte (0.2000000029 -> 0,2)', v4.avertissement === '', v4.avertissement);
check('PROMPT: vocabulaire du contacteur transmis', /« on » = EDF FERME/.test(p.messages[0].content));
const calls = run(B, v2, { BUILD_PROMPT: p }).map((i) => i.json);
check('PERIMETRE: une seule ecriture, sensor.evcs_conseiller_reponse', calls.length === 1 && calls[0].state_entity === 'sensor.evcs_conseiller_reponse');
check('PERIMETRE: question, horodatage, avertissement et source publies', calls[0].data.attributes.question_at === 2000 && /6.5/.test(calls[0].data.attributes.avertissement) && /Spark/.test(calls[0].data.attributes.source_texte));
const hl = run(B, v3, { BUILD_PROMPT: p }).map((i) => i.json)[0];
check('PERIMETRE: hors ligne -> reponse explicite, question marquee traitee', /pas repondu/.test(hl.data.attributes.reponse) && hl.data.attributes.question_at === 2000);
console.log(f ? `\n${f} ECHEC(S)` : '\nDIALOGUE OK');
process.exit(f ? 1 : 0);