// Le contrat d'un moteur (SPEC §4.1) tenu par les moteurs d'axe et l'intégrateur : // déterminisme, version, sources (coefficients lus + prix des typologies), avertissements. import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { eau, energie, alimentation, integrateur, contexte, VERSION, COEFFICIENTS, autonomie } from '../dist/index.js' const lire = (f) => JSON.parse(readFileSync(new URL(`../fixtures/${f}`, import.meta.url), 'utf8')) const options = lire('options.json') const postes = lire('postes.json') const france_climat = lire('france_climat.json') const algo_config = lire('algo-config.json') const profils = lire('profils-reference.json') const donnees = { postes, options, algo_config, france_climat } const B = profils[1] const ctxB = contexte.calculer({ code_postal: B.inputs.code_postal }, { france_climat }).sorties test('déterminisme : deux appels identiques, même résultat octet pour octet', () => { for (const m of [eau, energie, alimentation]) { const a = m.calculer({ autodiag: B.inputs, budget_axe: 14000, contexte: ctxB }, donnees) const b = m.calculer({ autodiag: B.inputs, budget_axe: 14000, contexte: ctxB }, donnees) assert.equal(JSON.stringify(a), JSON.stringify(b), m.id) } const a = integrateur.calculer({ autodiag: B.inputs, budget_total: 35000, mode: 'par_axe' }, donnees) const b = integrateur.calculer({ autodiag: B.inputs, budget_total: 35000, mode: 'par_axe' }, donnees) assert.equal(JSON.stringify(a), JSON.stringify(b)) }) test('les données reçues ne sont pas mutées par un calcul', () => { const avant = JSON.stringify({ options, postes, algo_config, france_climat, COEFFICIENTS }) integrateur.calculer({ autodiag: B.inputs, budget_total: 35000, mode: 'global' }, donnees) eau.calculer({ autodiag: B.inputs, budget_axe: 14000, contexte: ctxB }, donnees) assert.equal(JSON.stringify({ options, postes, algo_config, france_climat, COEFFICIENTS }), avant) }) test('chaque résultat porte la version, les coefficients lus et les prix des typologies retenues', () => { const r = eau.calculer({ autodiag: B.inputs, budget_axe: 14000, contexte: ctxB }, donnees) assert.equal(r.version, VERSION) assert.equal(r.sorties.axe, 'eau') assert.ok(r.sorties.postes.length >= 4, 'eau : au moins quatre postes obligatoires') const cles = r.sources.map((s) => s.cle) assert.ok(cles.includes('captage_rendement'), 'le rendement de captage est lu (scoreur et taux)') assert.ok(cles.includes('besoin_eau_l_j_pers') && cles.includes('part_eau_substituable')) for (const p of r.sorties.postes) assert.ok(cles.includes(`option.${p.typologie_choisie}`), `prix de ${p.typologie_choisie} tracé`) for (const s of r.sources) { assert.ok(s.source, `${s.cle} : source`) assert.ok(typeof s.date === 'string', `${s.cle} : date`) } assert.equal(typeof r.sorties.taux_couverture, 'number') assert.ok(r.sorties.taux_detail && r.sorties.taux_detail.captable_m3 > 0) }) test('énergie : l\'autoconsommation est appliquée au taux de couverture (0,30 sans batterie, 0,70 avec 10 kWh)', () => { const r = energie.calculer({ autodiag: { ...B.inputs, orientation_toit: 'sud' }, budget_axe: 14000, contexte: ctxB }, donnees) const d = r.sorties.taux_detail assert.ok(d && d.kwc > 0, 'un kit PV est retenu à ce budget') const attendu = d.batterie_kwh >= 10 ? 0.7 : d.batterie_kwh <= 0 ? 0.3 : null if (attendu != null) assert.equal(d.taux_autoconso, attendu) assert.equal(d.autoconsomme_kwh, Math.round(d.production_kwh * d.taux_autoconso)) assert.equal(d.kwh_par_kwc, COEFFICIENTS.productible_pv_kwh_kwc.valeur.table[ctxB.zone_climatique_rt]) const cles = r.sources.map((s) => s.cle) for (const c of ['orientation_pv', 'productible_pv_kwh_kwc', 'autoconso_sans_batterie', 'autoconso_avec_batterie_10kwh', 'besoin_elec_kwh_pers_an']) assert.ok(cles.includes(c), c) assert.ok(!r.avertissements.some((a) => /H2d/.test(a)), 'B est en H2c : pas d\'avertissement H2d') }) test('énergie : zone H2d → avertissement « interpolé » ; orientation inconnue → avertissement', () => { const ctx = contexte.calculer({ code_postal: '07000' }, { france_climat }).sorties assert.equal(ctx.zone_climatique_rt, 'H2d') const r = energie.calculer({ autodiag: { ...B.inputs, code_postal: '07000', orientation_toit: 'nord-ouest' }, budget_axe: 14000, contexte: ctx }, donnees) if (r.sorties.taux_couverture != null) { assert.ok(r.avertissements.some((a) => /H2d/.test(a))) assert.ok(r.avertissements.some((a) => /orientation/.test(a))) } }) test('alimentation : parcelle trop petite → budget 0 → aucun poste, taux null, statut OK', () => { const A = profils[0] const ctxA = contexte.calculer({ code_postal: A.inputs.code_postal }, { france_climat }).sorties const r = alimentation.calculer({ autodiag: A.inputs, budget_axe: 0, contexte: ctxA }, donnees) assert.deepEqual(r.sorties.postes, []) assert.equal(r.sorties.taux_couverture, null) assert.equal(r.sorties.status, 'OK') }) test('intégrateur : aménagements cochés ignorés et signalés (P1.5) ; axes désactivés respectés', () => { const r = integrateur.calculer({ autodiag: B.inputs, budget_total: 35000, mode: 'par_axe', amenagements_coches: ['x'], axes_actifs: { alim: false } }, donnees) assert.ok(r.avertissements.some((a) => /P1\.5/.test(a))) assert.equal(r.sorties.budgets.alim, 0) assert.deepEqual(r.sorties.axes.alim.postes, []) assert.ok(r.sorties.axes.eau.postes.length > 0 && r.sorties.axes.energie.postes.length > 0) assert.equal(r.sorties.poids.eau + r.sorties.poids.energie + r.sorties.poids.alim, 100) }) test('code postal inconnu : l\'avertissement du contexte remonte dans l\'intégrateur', () => { const r = integrateur.calculer({ autodiag: { ...B.inputs, code_postal: '98000' }, budget_total: 35000 }, donnees) assert.ok(r.avertissements.some((a) => /contexte par défaut/.test(a))) assert.equal(r.sorties.contexte.departement, '98') }) test('noyau : configPourAxes ne mute pas la config ; parseParam / parseJsonArr tolèrent objet et chaîne', () => { const avant = JSON.stringify(algo_config) autonomie.noyau.configPourAxes(algo_config, { eau: false }) assert.equal(JSON.stringify(algo_config), avant) assert.deepEqual(autonomie.noyau.parseJsonArr('["a","b"]'), ['a', 'b']) assert.deepEqual(autonomie.noyau.parseJsonArr(['a']), ['a']) assert.deepEqual(autonomie.noyau.parseJsonArr('null'), []) assert.deepEqual(autonomie.noyau.parseParam('{"strategie":"fixe","default":3}'), { strategie: 'fixe', default: 3 }) assert.equal(autonomie.noyau.parseParam('{oops'), null) })