From 2dd8cf77139c3b9cbbbaec248cc6d14414ad8929 Mon Sep 17 00:00:00 2001 From: pymenvert Date: Mon, 17 Aug 2026 19:08:02 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Le=20relev=C3=A9=20d'ordre=203D=20pariait?= =?UTF-8?q?=20sur=20une=20dur=C3=A9e=20au=20lieu=20d'attendre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La CI Windows du 2026-08-17, sur main, a fait tomber « ordre = axe 3D fait suivre au chase la scéno, pas la liste ». Le relevé rendait ["bar1","bar2","bar3"] au lieu de ["bar1","bar2","bar3","bar0"]. L'ordre n'était pas faux : il était TRONQUÉ. Les trois barres relevées sont les trois premières de l'ordre géométrique attendu — un ordre cassé aurait rendu l'ordre de la liste (bar0, bar1, bar2), c'est-à-dire une réponse différente, pas une réponse plus courte. Le quatrième pas n'était simplement pas encore arrivé. `ordreVu()` attendait 560 ms pour quatre pas de 120 ms, soit 80 ms de marge. Sur un runner chargé qui fait tourner une dizaine de serveurs en parallèle, ça ne tient pas. C'est le piège écrit dans le CLAUDE.md : un test qui mesure une grandeur qui évolue doit être long devant le bruit de charge. Le relevé attend désormais la CONDITION — les quatre barres — au lieu de parier sur une durée, avec un plafond de 3 s pour rester borné si le chase ne démarre pas. Il est donc indépendant de la vitesse de la machine, et il ne ralentit rien sur une machine rapide : il sort dès que le compte y est. Mesuré, le fichier met 59,2 s contre 59,5 s avant. ⚠ Le CLAUDE.md prévient qu'élargir une fenêtre au hasard a déjà fait passer une suite de 1 à 19 échecs. Ici l'élargissement est sûr, et c'est démontrable : chaque barre n'est retenue qu'à son PREMIER allumage, donc les cycles suivants du chase sont ignorés. Attendre plus longtemps ne peut rien ajouter de faux. Et le test n'a pas été rendu complaisant : vérifié en cassant l'ordre 3D pour de bon dans un clone (`list = [...list]`, le tri géométrique supprimé) — la suite tombe. Il mord toujours. 346 tests, 0 ignoré. dist/ synchrone. Aucun changement dans le code livré. --- tests/champ3d.test.js | 51 +++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/tests/champ3d.test.js b/tests/champ3d.test.js index 691371f..aed351d 100644 --- a/tests/champ3d.test.js +++ b/tests/champ3d.test.js @@ -611,23 +611,52 @@ describe('Champ 3D', () => { { id: 'o3', p3: [2, 0, 2], dir3: [1, 0, 0], len3: 1 }, ] }); - /** Ordre d'allumage des barres, relevé sur le flux OSC. */ - const ordreVu = async () => { + /** + * Ordre d'allumage des barres, relevé sur le flux OSC. + * + * ⚠ On attend que les 4 barres se soient allumées, on ne PARIE PAS sur une + * durée. La version d'avant coupait à 560 ms pour 4 pas de 120 ms, soit + * 80 ms de marge : la CI Windows du 2026-08-17 a relevé + * ["bar1","bar2","bar3"] — le bon ordre, tronqué d'un cran, parce que le + * quatrième pas n'était pas encore arrivé sur un runner chargé. Un test qui + * mesure une grandeur qui évolue doit être long devant le bruit de charge. + * + * Attendre la CONDITION plutôt qu'une durée rend le relevé indépendant de + * la vitesse de la machine, sans allonger le test sur une machine rapide : + * il sort dès que le compte y est. Le plafond le garde borné si rien ne + * vient — et un relevé incomplet fera échouer l'assertion, comme il doit. + * + * Attendre plus longtemps ne peut RIEN fausser ici : chaque barre n'est + * retenue qu'à son PREMIER allumage, donc les cycles suivants du chase sont + * ignorés. C'est ce qui rend cet élargissement sûr, là où élargir une + * fenêtre au hasard ne l'est pas. + */ + const ordreVu = async (attendu = 4) => { await h.post('/api/blackout'); await sleep(80); h.clearOsc(); await h.post('/api/resync'); await h.post('/api/start'); - await sleep(560); // ~4 pas de 120 ms - await h.post('/api/stop'); - const vus = []; - const precedent = {}; - for (const m of h.osc()) { - const mm = /^\/fixtures\/(bar\d+)\/luminosity$/.exec(m.address); - if (!mm) continue; - if (m.args[0] > 0.5 && !(precedent[mm[1]] > 0.5) && !vus.includes(mm[1])) vus.push(mm[1]); - precedent[mm[1]] = m.args[0]; + /** Relit tout le flux depuis le dernier `clearOsc()` — idempotent. */ + const relever = () => { + const vus = []; + const precedent = {}; + for (const m of h.osc()) { + const mm = /^\/fixtures\/(bar\d+)\/luminosity$/.exec(m.address); + if (!mm) continue; + if (m.args[0] > 0.5 && !(precedent[mm[1]] > 0.5) && !vus.includes(mm[1])) vus.push(mm[1]); + precedent[mm[1]] = m.args[0]; + } + return vus; + }; + let vus = []; + // Plafond à 3 s : largement au-dessus des ~480 ms nominales, et le test + // reste borné si le chase ne démarre pas du tout. + for (let i = 0; i < 60 && vus.length < attendu; i++) { + await sleep(50); + vus = relever(); } + await h.post('/api/stop'); return vus; }; From 2a6d409e639a5114c476959dfc1c695233e983ea Mon Sep 17 00:00:00 2001 From: pymenvert Date: Mon, 17 Aug 2026 19:20:11 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Le=20navigateur=20avait=2020=20s=20pour=20d?= =?UTF-8?q?=C3=A9marrer,=20un=20runner=20charg=C3=A9=20en=20demande=20plus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second échec de la CI Windows sur main, le même jour, et pour une raison différente du premier — celui-ci ne fait tomber aucun test : # tests 346 # pass 300 # fail 0 # cancelled 46 # skipped 0 Zéro échec, mais les 46 tests d'interface ANNULÉS, et le job rouge. La cause réelle était noyée sous 46 lignes « did not finish before its parent » : le `before` de ui.test.js avait levé « le navigateur n'a pas démarré ». `attendreDevTools` accordait 20 s à Edge pour répondre sur son point d'entrée DevTools. Le même runner avait fait passer ces 46 tests deux fois le jour même : ce n'est donc pas un refus de démarrer, c'est la charge de la machine. Le délai passe à 60 s. Attendre plus longtemps ne coûte rien quand tout va bien : la boucle sort dès que DevTools répond, en général sous la seconde. Ça ne coûte que dans le cas déjà perdu, où le navigateur ne démarrera jamais — et là, échouer 40 s plus tard ne change rien à personne. ⚠ À savoir pour la prochaine fois : quand ce `before` lève, les 46 tests s'annoncent « cancelled » SANS dire pourquoi, et le compte reste à 346. Ni le nombre de tests ni le nombre d'échecs ne signalent quoi que ce soit — seul `cancelled` le fait. Le message d'erreur dit désormais le délai attendu, pour qu'on ne recherche plus la cause à l'aveugle. 346 tests, 0 annulé, 0 ignoré. Aucun changement dans le code livré. --- tests/browser.js | 12 +++++++++++- tests/ui.test.js | 7 ++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/browser.js b/tests/browser.js index 0e95616..b3bd02b 100644 --- a/tests/browser.js +++ b/tests/browser.js @@ -184,7 +184,17 @@ async function launch() { // On interroge le point d'entrée HTTP de DevTools plutôt que de lire stderr : // Edge n'y annonce rien, contrairement à Chrome. - const urlWs = await attendreDevTools(port, 20000); + // + // ⚠ 60 s, et pas 20. Sur la CI Windows du 2026-08-17, Edge n'a pas répondu + // dans les 20 s : `launch()` a rendu `null`, le `before` de `ui.test.js` a + // levé, et les 46 tests d'interface sont passés en « cancelled » — 0 échec, + // mais le job rouge quand même. Le même runner les avait fait passer deux + // fois le jour même : c'est la charge de la machine, pas un refus de démarrer. + // + // Attendre plus longtemps ne coûte RIEN quand tout va bien — la boucle sort + // dès que DevTools répond, en général sous la seconde. Ça ne coûte que dans + // le cas déjà perdu, où le navigateur ne démarrera jamais. + const urlWs = await attendreDevTools(port, 60000); if (!urlWs) { try { child.kill(); } catch (e) {} try { fs.rmSync(profil, { recursive: true, force: true }); } catch (e) {} diff --git a/tests/ui.test.js b/tests/ui.test.js index 5e62ad4..ab09ce8 100644 --- a/tests/ui.test.js +++ b/tests/ui.test.js @@ -24,7 +24,12 @@ describe('Interface dans un vrai navigateur', { skip: AUCUN_NAVIGATEUR && h = await start(); await h.post('/api/fixtures', { fixtures: fixtures(6) }); nav = await launch(); - if (!nav) throw new Error('le navigateur n’a pas démarré'); + // ⚠ Ce message est tout ce qu'on aura pour diagnostiquer : quand ce `before` + // lève, les 46 tests d'en dessous s'annoncent « cancelled » sans dire + // pourquoi. Dire le délai attendu évite de rechercher la cause à l'aveugle, + // comme il a fallu le faire le 2026-08-17. + if (!nav) throw new Error( + 'le navigateur n’a pas démarré : DevTools n’a pas répondu en 60 s'); await nav.goto('http://127.0.0.1:' + h.port + '/'); }); after(async () => { From e2754301768d65f7fb3c4b5ec029c8892b151128 Mon Sep 17 00:00:00 2001 From: pymenvert Date: Mon, 17 Aug 2026 19:33:11 +0200 Subject: [PATCH 3/3] =?UTF-8?q?L'=C3=A9cart-type=20du=20bruit=20se=20mesur?= =?UTF-8?q?ait=20avant=20que=20le=20champ=20soit=20=C3=A9tabli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Troisième échec de la CI Windows, encore une autre cause — et celle-ci accusait le produit à tort : « la statistique du bruit ne devrait pas dependre de l axe : 0.1087 contre 0.2149 » Or 0,2149 est la valeur NOMINALE des deux azimuts, celle que le commentaire du test cite lui-même. Ce n'est donc pas l'axe qui a bougé : c'est le premier relevé qui s'est effondré de moitié. Un écart-type ne s'effondre que d'une façon — des valeurs plates. Les tout premiers `state()` arrivaient avant le premier tick du champ, sur une machine chargée, et un relevé plat sur dix suffit à diviser la dispersion par deux. La mesure laisse maintenant le champ s'établir (150 ms) et prend 20 relevés au lieu de 10, pour que la statistique converge. ⚠ Le seuil de 15 % n'est PAS touché. Il avait été resserré depuis 50 % sur mesure, parce que 50 % laissait passer une dépendance à l'axe de 40 % — soit précisément le défaut que ce test interdit. Face à un test qui tombe, élargir la tolérance est la correction tentante et la mauvaise : c'est la mesure qu'on fiabilise, jamais l'exigence qu'on relâche. Et le test n'a pas été rendu complaisant : les deux mutations qui le visent — « etalement du bruit revenu a la version rejetee » et « normalisation par l'etendue de l'axe remplacee par une constante » — sont toujours détectées, vérifié dans un clone. Coût : 61 s au lieu de 59 s sur ce fichier. 346 tests, 0 ignoré. --- tests/champ3d.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/champ3d.test.js b/tests/champ3d.test.js index aed351d..defd861 100644 --- a/tests/champ3d.test.js +++ b/tests/champ3d.test.js @@ -1037,8 +1037,21 @@ describe('Champ 3D', () => { await base({ engine: 'field', field: 'bruit', axAz, axEl: 0, stepMs: 10000, group: 8, speed: 0.05, width: 2 }); await h.post('/api/start'); + // ⚠ Laisser le champ s'ÉTABLIR avant de mesurer, et prendre assez de + // relevés pour que la statistique converge. Sur la CI Windows du + // 2026-08-17, ce test a rendu 0,1087 contre 0,2149 — or 0,2149 est la + // valeur nominale des DEUX azimuts. Ce n'est donc pas l'axe qui a bougé : + // c'est un écart-type écrasé par des relevés plats, pris avant le premier + // tick sur une machine chargée. Un relevé plat sur dix suffit à diviser la + // dispersion par deux. + // + // On ne touche PAS au seuil de 15 % : il a été resserré depuis 50 % sur + // mesure, précisément parce que 50 % laissait passer le défaut que ce test + // interdit. Élargir la tolérance annulerait ce travail — c'est la mesure + // qu'on fiabilise, pas l'exigence qu'on relâche. + await sleep(150); const vals = []; - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 20; i++) { const st = await h.state(); for (const v of (st.levels || [])) if (typeof v === 'number') vals.push(v); await sleep(70);