workspace: rapport solaire, prevision de nuit, concierge, courrier, fiche allegee
- Fiche agent maison (AGENTS.md) : prevision de nuit relayee par script, import
EDF, regle JEEP midi, saisie vehicule/niveau/depart, concierge qui relaie
« ✅ Noté » ou « Rien n'est noté », groupe A deplace (28 649 -> 25 831 car.).
- Nouvelles references : reference-concierge.md, reference-workspace.md.
- Scripts : rapport-solaire.js, prevision-nuit.js, airtria-ha-vehicule.js,
concierge-live.js (answer ne sort plus jamais vide, mode CONCIERGE_SIMULATION).
- Courrier Orange / AIRTRIA / Gmail : audits, regles, menage, recents.
- Bancs EVCS tests-pv (G5 accepte « anomal »).
- .gitignore : liste blanche etendue aux nouveaux scripts et bancs.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// concierge-write.test.js — Test de PORTÉE D'ÉCRITURE (garde-fou du canal HA).
|
||||
// Doit ÉCHOUER si une écriture peut sortir de la constante airtria_vehicle_label.
|
||||
// À faire tourner à CHAQUE modification, pas une seule fois (cf. EVCS advisor).
|
||||
// Pur statique + require : aucun réseau, aucun token, aucune écriture.
|
||||
// Doit ÉCHOUER si une écriture peut sortir des DEUX constantes autorisées :
|
||||
// airtria_vehicle_label (étiquette) et victron_evcs_vehicle (sélecteur,
|
||||
// ajouté 19/09/2026). À faire tourner à CHAQUE modification (cf. EVCS advisor).
|
||||
// Statique + require + fetch STUBBÉ : aucun réseau, aucun token, aucune écriture.
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
@@ -12,46 +13,102 @@ const SRC = path.join(__dirname, 'concierge-write.js');
|
||||
const src = fs.readFileSync(SRC, 'utf8');
|
||||
const mod = require('./concierge-write.js');
|
||||
|
||||
// Les tests 6-9 sont asynchrones : on déclare, puis on déroule dans l'ordre.
|
||||
let ko = 0;
|
||||
const check = (name, fn) => {
|
||||
try { fn(); console.log(` ok ${name}`); }
|
||||
catch (e) { ko++; console.log(` FAIL ${name} : ${e.message}`); }
|
||||
};
|
||||
const queue = [];
|
||||
const check = (name, fn) => queue.push([name, fn]);
|
||||
|
||||
console.log('=== Test de portée d\'écriture concierge-write.js ===');
|
||||
|
||||
// 1. La constante est exactement l'entité attendue.
|
||||
check('constante = input_text.airtria_vehicle_label', () =>
|
||||
assert.strictEqual(mod.HELPER_ENTITY, 'input_text.airtria_vehicle_label'));
|
||||
// 1. Les constantes sont exactement les entités attendues.
|
||||
check('constantes = airtria_vehicle_label + victron_evcs_vehicle', () => {
|
||||
assert.strictEqual(mod.HELPER_ENTITY, 'input_text.airtria_vehicle_label');
|
||||
assert.strictEqual(mod.SELECTOR_ENTITY, 'input_select.victron_evcs_vehicle');
|
||||
assert.deepStrictEqual(mod.SELECTOR_LABELS, ['VW', 'JEEP']);
|
||||
});
|
||||
|
||||
// 2. entity_id n'est JAMAIS accolé à autre chose que la constante
|
||||
// 2. entity_id n'est JAMAIS accolé à autre chose que l'une des DEUX constantes
|
||||
// (pas de littéral d'entité, pas d'interpolation, pas de concaténation).
|
||||
check('entity_id uniquement = HELPER_ENTITY', () => {
|
||||
check('entity_id uniquement = HELPER_ENTITY ou SELECTOR_ENTITY', () => {
|
||||
const occ = [...src.matchAll(/entity_id\s*:\s*([^,}\n]+)/g)].map(m => m[1].trim());
|
||||
assert(occ.length > 0, 'aucun entity_id trouvé');
|
||||
for (const v of occ) assert.strictEqual(v, 'HELPER_ENTITY',
|
||||
assert(occ.length >= 2, `entity_id attendus >= 2, trouvés ${occ.length}`);
|
||||
for (const v of occ) assert(v === 'HELPER_ENTITY' || v === 'SELECTOR_ENTITY',
|
||||
`entity_id dynamique ou littéral interdit : ${v}`);
|
||||
});
|
||||
|
||||
// 3. Aucune autre entité HA en dur (aucun autre "domaine.entite") que la constante.
|
||||
check('aucune entité HA littérale hors la constante', () => {
|
||||
const decl = "'input_text.airtria_vehicle_label'";
|
||||
// 3. Aucune entité HA en dur hors les DEUX déclarations de constantes.
|
||||
check('aucune entité HA littérale hors les deux constantes', () => {
|
||||
const decls = ["'input_text.airtria_vehicle_label'",
|
||||
"'input_select.victron_evcs_vehicle'"];
|
||||
const lits = [...src.matchAll(/['"`]([a-z_]+\.[a-z0-9_]+)['"`]/g)].map(m => m[0]);
|
||||
for (const l of lits) assert.strictEqual(l, decl,
|
||||
for (const l of lits) assert(decls.includes(l),
|
||||
`littéral d'entité interdit : ${l}`);
|
||||
});
|
||||
|
||||
// 4. Un SEUL appel de service, et c'est input_text/set_value (aucun actionneur).
|
||||
check('seul service = input_text/set_value', () => {
|
||||
// 4. DEUX appels de service exactement : set_value puis select_option.
|
||||
check('services = input_text/set_value + input_select/select_option', () => {
|
||||
const services = [...src.matchAll(/\/api\/services\/([a-z_]+\/[a-z_]+)/g)]
|
||||
.map(m => m[1]);
|
||||
assert.deepStrictEqual(services, ['input_text/set_value'],
|
||||
assert.deepStrictEqual(services.sort(),
|
||||
['input_select/select_option', 'input_text/set_value'],
|
||||
`services inattendus : ${services.join(', ') || '(aucun)'}`);
|
||||
});
|
||||
|
||||
// 5. La relecture existe (garde-fou rejet silencieux).
|
||||
check('relecture présente (/api/states/)', () =>
|
||||
assert(/\/api\/states\//.test(src), 'pas de relecture après écriture'));
|
||||
// 5. La relecture existe pour CHAQUE écriture (garde-fou rejet silencieux).
|
||||
check('relectures présentes (/api/states/ x2)', () => {
|
||||
const n = [...src.matchAll(/\/api\/states\//g)].length;
|
||||
assert(n >= 2, `relectures attendues >= 2, trouvées ${n}`);
|
||||
});
|
||||
|
||||
console.log(ko === 0 ? '\nTOUT VERT' : `\n${ko} test(s) EN ÉCHEC`);
|
||||
process.exit(ko === 0 ? 0 : 1);
|
||||
// ---- Comportement du sélecteur, fetch STUBBÉ (aucun réseau) ----
|
||||
const calls = [];
|
||||
global.fetch = async (url, opts) => {
|
||||
calls.push({ url: String(url), body: opts && opts.body });
|
||||
if (String(url).includes('/api/states/')) {
|
||||
return { ok: true, json: async () => ({ state: 'VW' }) };
|
||||
}
|
||||
return { ok: true, json: async () => ({}) };
|
||||
};
|
||||
process.env.HA_TOKEN_FILE = '/dev/null';
|
||||
|
||||
// 6. Refus d'une source non humaine, sans AUCUN appel réseau.
|
||||
check('sélecteur refuse une source non humaine', async () => {
|
||||
calls.length = 0;
|
||||
await assert.rejects(() => mod.setVehicleSelector('VW', 'deduction'), /non humaine/);
|
||||
assert.strictEqual(calls.length, 0, 'un fetch est parti malgré le refus');
|
||||
});
|
||||
|
||||
// 7. Refus des labels hors foyer, sans AUCUN appel réseau.
|
||||
check('sélecteur refuse MEGANE / INVITE / ZERO', async () => {
|
||||
for (const bad of ['MEGANE', 'INVITE', 'ZERO', 'vw', 'Jeep']) {
|
||||
calls.length = 0;
|
||||
await assert.rejects(() => mod.setVehicleSelector(bad, 'whatsapp'), /hors foyer/);
|
||||
assert.strictEqual(calls.length, 0, `un fetch est parti pour '${bad}'`);
|
||||
}
|
||||
});
|
||||
|
||||
// 8. VW accepté : select_option sur la constante, puis relecture.
|
||||
check('sélecteur VW : écriture + relecture', async () => {
|
||||
calls.length = 0;
|
||||
const r = await mod.setVehicleSelector('VW', 'whatsapp');
|
||||
assert.strictEqual(r.entity, 'input_select.victron_evcs_vehicle');
|
||||
assert.strictEqual(calls.length, 2, `2 appels attendus, ${calls.length} faits`);
|
||||
assert(calls[0].url.includes('/api/services/input_select/select_option'));
|
||||
assert(calls[0].body.includes('"option":"VW"'));
|
||||
assert(calls[1].url.includes('/api/states/input_select.victron_evcs_vehicle'));
|
||||
});
|
||||
|
||||
// 9. Relecture discordante = erreur (JEEP demandé, HA resté sur VW).
|
||||
check('sélecteur détecte le refus silencieux HA', async () => {
|
||||
await assert.rejects(() => mod.setVehicleSelector('JEEP', 'whatsapp'),
|
||||
/refusée en silence/);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
for (const [name, fn] of queue) {
|
||||
try { await fn(); console.log(` ok ${name}`); }
|
||||
catch (e) { ko++; console.log(` FAIL ${name} : ${e.message}`); }
|
||||
}
|
||||
console.log(ko === 0 ? '\nTOUT VERT' : `\n${ko} test(s) EN ÉCHEC`);
|
||||
process.exit(ko === 0 ? 0 : 1);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user