Files
moteurs/test/commun.test.mjs
T
JulesandClaude Fable 5.1 69809df5d1 LOT P1.0 : paquet @transformer/moteurs v0.1.0
Extraction du cœur autonomie de calculs.trans-former.fr (12 modules, sans
singleton, couplages sans mutation, console derrière debug), découpage en trois
moteurs d'axe + intégrateur (mode global = calculs, mode par_axe = plateforme),
moteurs commun.contexte (Corse 2A/2B, DOM 97x corrigés) et commun.geometrie,
table des coefficients harmonisés (docs/CONSTANTES.md pour Jules, R-7),
validation structurelle des données (schemas/), tests hors ligne : 28 assertions
d'origine + épingle générée depuis le code d'astro-pro + écarts d'harmonisation
figés + contrat des moteurs. dist/ committé.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019UBHYPdeQYy2m1GVJp6b12
2026-09-28 17:06:49 +02:00

128 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Tests des moteurs et helpers communs : contexte (Corse, DOM, inconnu),
// géométrie (maison B de l'étude), argent, validation des données.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { contexte, departementDepuisCodePostal, geometrie, mensualite, annuite, coutDuCredit, arrondiAuPas, additionneFourchettes, valider, VERSION, COEFFICIENTS } from '../dist/index.js'
const lire = (f) => JSON.parse(readFileSync(new URL(`../fixtures/${f}`, import.meta.url), 'utf8'))
const france_climat = lire('france_climat.json')
test('contexte : département depuis code postal (Corse 2A/2B, DOM 97x, métropole)', () => {
assert.equal(departementDepuisCodePostal('20000'), '2A')
assert.equal(departementDepuisCodePostal('20190'), '2A')
assert.equal(departementDepuisCodePostal('20200'), '2B')
assert.equal(departementDepuisCodePostal('20620'), '2B')
assert.equal(departementDepuisCodePostal('20999'), '2A')
assert.equal(departementDepuisCodePostal('97100'), '971')
assert.equal(departementDepuisCodePostal('97600'), '976')
assert.equal(departementDepuisCodePostal('75019'), '75')
assert.equal(departementDepuisCodePostal(31000), '31')
assert.equal(departementDepuisCodePostal(''), null)
assert.equal(departementDepuisCodePostal(null), null)
})
test('contexte : 2A, 2B et 971 tombent sur leur ligne france_climat (H3), pas sur le défaut H2a', () => {
for (const [cp, dept] of [
['20000', '2A'],
['20200', '2B'],
['97100', '971'],
]) {
const r = contexte.calculer({ code_postal: cp }, { france_climat })
const ligne = france_climat.find((c) => c.dept === dept)
assert.ok(ligne, `fixture : ligne ${dept} attendue`)
assert.equal(r.sorties.departement, dept)
assert.equal(r.sorties.zone_climatique_rt, ligne.zone_rt)
assert.equal(r.sorties.zone_climatique_rt, 'H3', `${dept} est en H3 dans la fixture ; l'origine donnait H2a`)
assert.equal(r.version, VERSION)
assert.ok(r.sources.some((s) => s.cle === `france_climat.${dept}`), 'la ligne climat est tracée en source')
if (ligne.pluvio_mm_an != null) {
assert.equal(r.sorties.pluviometrie_mm, ligne.pluvio_mm_an)
assert.equal(r.sorties.dju, ligne.dju_base_18)
assert.deepEqual(r.avertissements, [], `${cp} : aucun défaut appliqué`)
} else {
// DOM : la fixture ne porte ni pluviométrie ni DJU (« DOM tropical ») → défauts déclarés
assert.equal(r.sorties.pluviometrie_mm, COEFFICIENTS.contexte_defaut.valeur.pluviometrie_mm)
assert.ok(r.avertissements.some((a) => /pluviométrie absente/.test(a)), `${cp} : pluviométrie par défaut déclarée`)
assert.ok(r.avertissements.some((a) => /DJU absent/.test(a)), `${cp} : DJU par défaut déclaré`)
}
}
})
test('contexte : code postal inconnu → défaut déclaré en avertissement et en source', () => {
const r = contexte.calculer({ code_postal: '98000' }, { france_climat })
assert.equal(r.sorties.zone_climatique_rt, COEFFICIENTS.contexte_defaut.valeur.zone_rt)
assert.equal(r.avertissements.length, 1)
assert.match(r.avertissements[0], /défaut/)
assert.ok(r.sources.some((s) => s.cle === 'contexte_defaut'))
})
test('géométrie : maison B de l\'étude (Tournefeuille, emprise 73 m², 2 niveaux, 5 m) — SHAB estimée à confronter au DPE en P1.1', () => {
const r = geometrie.calculer({ emprise_m2: 73, niveaux: 2, hauteur_m: 5, pente_toit_deg: 30, mitoyennete: 0 }, {})
const s = r.sorties
// SHAB = 73 × 2 × 0,80 = 116,8 m²
assert.equal(s.shab_m2, 116.8)
assert.equal(s.shab_estimee, true)
assert.equal(s.plancher_bas_m2, 73)
assert.equal(s.hauteur_etage_m, 2.5)
assert.equal(s.volume_m3, 292)
// périmètre 4 × √(73 × 1,3) = 38,97 m ; murs ext × 5 m = 194,83 m² ; vitrage 116,8/6 = 19,47 ; opaques 175,37
assert.equal(s.perimetre_m, 38.97)
assert.equal(s.murs_exterieurs_m2, 194.83)
assert.equal(s.vitrage_m2, 19.47)
assert.equal(s.murs_opaques_m2, 175.37)
// toiture 73 / cos 30° × 1,05 = 88,51
assert.equal(s.toiture_m2, 88.51)
assert.ok(r.avertissements.some((a) => /estimée/.test(a)), 'SHAB estimée déclarée')
assert.ok(r.sources.some((x) => x.cle === 'geometrie_ratio_shab_emprise'))
// La surface habitable du DPE 2631E0015736Q n'est pas dans l'étude (aucune recherche web dans ce lot) :
// l'écart SHAB estimée vs DPE se mesure au premier appel de l'adaptateur DPE, P1.1.
console.log(` maison B : SHAB estimée ${s.shab_m2} m² (ratio 0,80) — surface DPE : à lire en P1.1 (écart visé < 20 %)`)
})
test('géométrie : mitoyenneté et surface connue', () => {
const seul = geometrie.calculer({ emprise_m2: 100, niveaux: 1, hauteur_m: 2.5 }, {}).sorties
const mitoyen = geometrie.calculer({ emprise_m2: 100, niveaux: 1, hauteur_m: 2.5, mitoyennete: 2 }, {}).sorties
assert.equal(mitoyen.perimetre_expose_m, +(seul.perimetre_m * 0.5).toFixed(2))
assert.ok(mitoyen.murs_exterieurs_m2 < seul.murs_exterieurs_m2)
const connue = geometrie.calculer({ emprise_m2: 100, niveaux: 1, hauteur_m: 2.5, surface_habitable_m2: 95 }, {})
assert.equal(connue.sorties.shab_m2, 95)
assert.equal(connue.sorties.shab_estimee, false)
assert.ok(!connue.avertissements.some((a) => /estimée/.test(a)))
const sansRien = geometrie.calculer({ emprise_m2: 80 }, {})
assert.equal(sansRien.avertissements.length, 4, 'niveaux, hauteur, pente inconnus + SHAB estimée → quatre avertissements')
})
test('argent : mensualité, annuité, coût du crédit, arrondis, fourchettes', () => {
// 10 000 € à 3 % sur 10 ans → 96,56 €/mois (formule d'annuité constante)
assert.equal(+mensualite(10000, 3, 10).toFixed(2), 96.56)
assert.equal(+annuite(10000, 3, 10).toFixed(2), 1158.73)
assert.equal(+coutDuCredit(10000, 3, 10).toFixed(2), 1587.29)
assert.equal(mensualite(12000, 0, 10), 100)
assert.equal(mensualite(0, 3, 10), 0)
assert.equal(arrondiAuPas(1234, 50), 1250)
assert.equal(arrondiAuPas(7, 0), 7)
assert.deepEqual(additionneFourchettes([{ min: 10, max: 20 }, { min: null, max: 5 }, { min: 3, max: null }, { min: null, max: null }]), { min: 18, max: 28 })
})
test('valider : les fixtures passent, une donnée cassée est refusée avec son chemin', () => {
for (const [table, fichier] of [
['postes', 'postes.json'],
['options', 'options.json'],
['france_climat', 'france_climat.json'],
['algo_config', 'algo-config.json'],
]) {
const v = valider(table, lire(fichier))
assert.deepEqual(v.erreurs, [], `${table} : fixture valide`)
assert.equal(v.ok, true)
}
const cassees = lire('postes.json').map((p, i) => (i === 2 ? { ...p, axe: 'feu', Id: 'trois' } : p))
const v = valider('postes', cassees)
assert.equal(v.ok, false)
assert.ok(v.erreurs.some((e) => e.startsWith('postes[2].Id')), 'chemin de l\'erreur')
assert.ok(v.erreurs.some((e) => /postes\[2\]\.axe.*feu/.test(e)))
assert.equal(valider('postes', {}).ok, false)
assert.equal(valider('postes', []).ok, false, 'minItems 1')
assert.equal(valider('algo_config', { poids_axes_base: { eau: 40, energie: 40, alim: 20 } }).ok, false, 'clés requises manquantes')
})