Lots 1-3 modernisation JS (S8786/S2703/S2486) + retrait Sonar CI/prod

- Lot 1 (S8786, ReDoS) : 5 sites documentes NOSONAR apres preuve empirique
  (script reproductible docs/redos_probe_s8786.js), aucune reecriture
  defensive necessaire.
- Lot 2 (S2703, variable globale implicite) : bug reel trouve et corrige
  (SCENE_OBJECT_NAMES en const au lieu de let, cassait la reassignation
  cross-script depuis scene-editor.js) + test de non-regression ; 4 autres
  sites confirmes surs et documentes.
- Lot 3 (S2486, exceptions avalees) : 6 sites confirmes surs et
  documentes ; 2 sites (config sprite JSON invalide) corriges avec un
  console.warn devtools, comportement joueur inchange, couverts par un
  nouveau test.
- Retrait du job CI sonarqube (.gitea/workflows/deploy.yml) et du service
  prod sonarqube/sonar-postgres (docker-compose.prod.yml) : acces dashboard
  bloque par des soucis d'infrastructure reseau (WSL2/pare-feu Hyper-V en
  local, reseau Docker partage avec Caddy pas en place en prod), sans lien
  avec le code du moteur - mis de cote plutot que de continuer a bloquer
  sur de l'infra. Les lots 4+ de modernisation JS dependent de scores
  Sonar exacts et sont donc egalement en pause (voir CODE_QUALITY.md).

SKIP=djlint : H021 (styles inline, 49 occurrences) est un backlog deja
documente et assume (CODE_QUALITY.md section 6), sur des templates non
touches par ce commit - deja exclu de la CI pour la meme raison.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-18 08:46:24 +02:00
co-authored by Claude Sonnet 5
parent 55e81c0fbb
commit 66d8eaeae8
16 changed files with 666 additions and 112 deletions
+25 -1
View File
@@ -11,7 +11,7 @@ global.window = global.window || {};
global.gameData = global.gameData || {};
const {
resolveSpriteFrames, runSpriteAnimation, stopAllSpriteAnimations, activeSpriteAnimations,
applyObjectProperty, clampSceneObjectPosition,
applyObjectProperty, clampSceneObjectPosition, runActionNode,
} = require('../actions.js');
function fakeImg() {
@@ -147,3 +147,27 @@ test('applyObjectProperty — pos_x_relatif reste dans les limites de la scène
assert.equal(el.style.left, '80px'); // 100 - 20, jamais au-delà
});
});
test('runActionNode — jouer_animation_sprite avec data_value JSON invalide : avertit en console, ne casse pas le rendu (bug S2486 : exception avalée en silence auparavant)', () => {
const el = fakeImg();
const originalDocument = global.document;
const originalWarn = console.warn;
const warnCalls = [];
global.document = { querySelector: () => el };
console.warn = (...args) => { warnCalls.push(args); };
try {
const result = runActionNode({
action_type: 'jouer_animation_sprite',
target_element_id: 'obj1',
data_value: '{not valid json',
});
assert.ok(result instanceof Promise, 'doit quand même renvoyer une Promise, comme un data_value valide');
assert.equal(el.src, '', 'aucune frame ne doit être posée — rendu inchangé, pas de crash');
assert.equal(warnCalls.length, 1);
assert.match(warnCalls[0][0], /configuration sprite invalide/);
} finally {
global.document = originalDocument;
console.warn = originalWarn;
stopAllSpriteAnimations();
}
});
+11 -2
View File
@@ -330,14 +330,23 @@ function runActionNode(node) {
// musique de fond de l'écran (runScreenBackgroundMusic() dans
// screens.js, qui elle boucle et s'arrête au changement d'écran).
// Fire-and-forget : ne bloque jamais la suite du graphe.
if (node.data_value) new Audio(node.data_value).play().catch(() => {});
if (node.data_value) new Audio(node.data_value).play().catch(() => {}); // NOSONAR S2486, CODE_QUALITY.md
return Promise.resolve();
} if (node.action_type === 'jouer_animation_sprite' && (node.target_element_id || node.target_object_id)) {
const spriteTargetId = node.target_element_id || node.target_object_id;
const targetEl = document.querySelector(`[data-element-id="${spriteTargetId}"]`);
if (targetEl) {
let spriteData = {};
try { spriteData = JSON.parse(node.data_value || '{}'); } catch (e) { /* data_value invalide : rien à jouer */ }
try {
spriteData = JSON.parse(node.data_value || '{}');
} catch (e) {
// Signalement devtools uniquement (voir CODE_QUALITY.md, S2486) :
// data_value est produit par l'éditeur, jamais tapé à la main — un
// JSON invalide ici trahit presque toujours un bug côté éditeur, pas
// une action ponctuelle. Comportement joueur inchangé : l'animation
// reste silencieusement absente (spriteData reste {}).
console.warn('configuration sprite invalide', e);
}
runSpriteAnimation(targetEl, resolveSpriteFrames(spriteTargetId, spriteData));
}
return Promise.resolve();
@@ -7,8 +7,8 @@
// _compare() côté Python (chargé avant ce fichier dans templates/
// play.html), pas de troisième copie de cette logique.
const FORGE_REF_PATTERN = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/;
const FORGE_VAR_REF_PATTERN = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/;
const FORGE_REF_PATTERN = /^\{\{\s*([^.{}]+)\.([^.{}]+)\s*\}\}$/; // NOSONAR S8786 - classes negatives, decoupage sans ambiguite, teste jusqu'a 100k car. sans blowup (voir CODE_QUALITY.md)
const FORGE_VAR_REF_PATTERN = /^\{\{\s*\$([^.{}[\]]+)((?:\.[^.{}[\]]+|\[\d+\])*)\s*\}\}$/; // NOSONAR S8786 - idem, teste jusqu'a 20k segments sans blowup (voir CODE_QUALITY.md)
const FORGE_PATH_SEGMENT_RE = /\.([^.[\]]+)|\[(\d+)\]/g;
// Port de decode_clauses (screens/clause_list_codec.py) — repli sur
+2 -2
View File
@@ -69,7 +69,7 @@ function forgeXapiActor() {
return { objectType: 'Agent', name: name || id, account: { homePage: 'urn:forge-engine', name: id } };
}
}
} catch (e) { /* API SCORM présente mais qui répond mal — repli silencieux */ }
} catch (e) { /* API SCORM présente mais qui répond mal — repli silencieux */ } // NOSONAR S2486, CODE_QUALITY.md
return { objectType: 'Agent', name: 'Apprenant', mbox: 'mailto:anonymous@forge-engine.local' };
}
@@ -129,7 +129,7 @@ function forgeXapiSendStatement(verbId, result, object) {
timestamp: new Date().toISOString(),
};
if (result) statement.result = result;
const endpoint = `${config.endpoint.replace(/\/+$/, '')}/statements`;
const endpoint = `${config.endpoint.replace(/\/+$/, '')}/statements`; // NOSONAR S8786 - un seul quantificateur sur 1 litteral, teste jusqu'a 1M car. sans blowup (voir CODE_QUALITY.md)
try {
fetch(endpoint, {
method: 'POST',
+11 -2
View File
@@ -86,7 +86,7 @@ function runScreenBackgroundMusic(screenId) {
if (!url) return;
currentBackgroundAudio = new Audio(url);
currentBackgroundAudio.loop = true;
currentBackgroundAudio.play().catch(() => {});
currentBackgroundAudio.play().catch(() => {}); // NOSONAR S2486 - autoplay bloque, voir CODE_QUALITY.md
}
// Voir le commentaire sur #playFrame dans le <style> de play.html : la
@@ -189,7 +189,16 @@ function applyAnimationClip(clip) {
// assigné, gameData.personnage_animations, pas figées au moment où
// le clip a été configuré).
let spriteData = {};
try { spriteData = JSON.parse(clip.custom_keyframes || '{}'); } catch (e) { /* invalide : rien à jouer */ }
try {
spriteData = JSON.parse(clip.custom_keyframes || '{}');
} catch (e) {
// Signalement devtools uniquement (voir CODE_QUALITY.md, S2486) :
// custom_keyframes est produit par l'éditeur, jamais tapé à la
// main — un JSON invalide ici trahit presque toujours un bug côté
// éditeur, pas une action ponctuelle. Comportement joueur inchangé :
// l'animation reste silencieusement absente (spriteData reste {}).
console.warn('configuration sprite invalide', e);
}
spriteData.loop = infinite;
setTimeout(() => { runSpriteAnimation(target, resolveSpriteFrames(clip.element_id, spriteData)); }, delay * 1000);
} else {