#!/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);