From e37c6d9c5ab69f2e745c14428bd0203abb1adc15 Mon Sep 17 00:00:00 2001 From: jsboige Date: Wed, 7 Oct 2026 12:30:16 +0200 Subject: [PATCH 1/2] Fix(guard): ratchet output-failure -- le remplacement se prouve aussi a la tete (FP mesure sur #19697) L'exemption DECLARED_FALLBACK (#19038) rejetait un hit des que la cellule base portait une sortie substantielle, sans verifier que la tete l'avait perdue : une cellule byte-identique entre base et tete etait classee DECLARED_FALLBACK au scan de base (sans contexte) et TOOL_FAILURE au scan de tete -- tout PR touchant le carnet sans toucher la cellule declenchait un faux 0 -> 1. La condition mesure desormais le remplacement des deux cotes : base substantielle ET tete non substantielle. Co-Authored-By: Claude Sonnet 5.5 --- .../notebook_tools/check_output_failure_text.py | 15 +++++++++++---- .../tests/test_check_output_failure_text.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/notebook_tools/check_output_failure_text.py b/scripts/notebook_tools/check_output_failure_text.py index 3797ae8c0c..b71296def4 100644 --- a/scripts/notebook_tools/check_output_failure_text.py +++ b/scripts/notebook_tools/check_output_failure_text.py @@ -637,9 +637,15 @@ def _declared_fallback(nb, idx, out_text, matched, base_nb=None): re-execution : toute cellule a ``try/except`` porte sa banniere en litteral dans sa source, les deux tiennent dans la meme ligne. Quand ``base_nb`` est fourni et que la meme cellule (par ``id``) portait une sortie substantielle - a la base, le hit reste TOOL_FAILURE : la banniere a REMPLACE un rendu, - c'est une perte de capacite (#3473 / #11685), pas un repli documente. Une - cellule nouvelle, ou deja en repli a la base, garde l'exemption. + a la base MAIS ne la porte plus a la tete, le hit reste TOOL_FAILURE : la + banniere a REMPLACE un rendu, c'est une perte de capacite (#3473 / #11685), + pas un repli documente. Une cellule nouvelle, deja en repli a la base, ou + qui CONSERVE sa sortie substantielle a la tete garde l'exemption : la + condition mesure un remplacement, pas la presence d'un rendu a la base + seule -- sinon une cellule inchangee entre base et tete serait classee + DECLARED_FALLBACK d'un cote (scan de base, sans contexte) et TOOL_FAILURE + de l'autre, et toute PR touchant le carnet sans toucher la cellule + declencherait un faux 0 -> 1 (mesure sur #19697 : 01-5b cell-10). """ cells = nb.get("cells", []) or [] if not (0 <= idx < len(cells)): @@ -651,7 +657,8 @@ def _declared_fallback(nb, idx, out_text, matched, base_nb=None): if not declared: return False base_cell = _base_cell_for(base_nb, cell) - if base_cell is not None and _substantial_output(base_cell): + if (base_cell is not None and _substantial_output(base_cell) + and not _substantial_output(cell)): return False return True diff --git a/scripts/notebook_tools/tests/test_check_output_failure_text.py b/scripts/notebook_tools/tests/test_check_output_failure_text.py index d435c4946d..1f656198e1 100644 --- a/scripts/notebook_tools/tests/test_check_output_failure_text.py +++ b/scripts/notebook_tools/tests/test_check_output_failure_text.py @@ -355,6 +355,22 @@ def test_declared_banner_replacing_a_base_render_still_fires(): assert not got["DECLARED_FALLBACK"] +def test_declared_fallback_kept_when_head_keeps_its_substantial_output(): + """Faux 0 -> 1 mesure sur #19697 : cellule byte-identique entre base et + tete. La base est scanee sans contexte (-> DECLARED_FALLBACK), la tete + voyait une base substantial et restait TOOL_FAILURE -- le ratchet creait + une regression sur une cellule que la PR ne touchait pas. Un remplacement + se prouve des deux cotes : la tete doit avoir PERDU le rendu.""" + out = "graphe: 2 noeuds, 1 arete\n" + BANNER_OUT + base = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(out))]} + head = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(out))]} + + got = scan(head, base_nb=base) + + assert not got["TOOL_FAILURE"] + assert len(got["DECLARED_FALLBACK"]) == 1 + + def test_declared_fallback_kept_when_base_was_already_in_fallback(): base = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(BANNER_OUT))]} head = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(BANNER_OUT))]} From 32521ff8342ad3e291b695a0710af9a665281075 Mon Sep 17 00:00:00 2001 From: jsboige Date: Thu, 8 Oct 2026 04:26:45 +0200 Subject: [PATCH 2/2] test(guard): ratchet output-failure -- verrouiller _in_fallback (#19697) et sa borne Le fix de code de cette branche est abandonne au profit de celui de main (#19697, _in_fallback) : voir le commit de fusion. Ce qui reste est ce que main ne couvre pas -- deux tests. - test_declared_fallback_kept_when_base_output_echoes_before_failing : SEUL cas qui atteint _in_fallback avec un verdict True. Le test voisin (base_was_already_in_fallback, base = banniere seule) sort par _substantial_output des la premiere condition et ne lit jamais _in_fallback : sans ce test, le mecanisme qui vient d'etre retenu sur main n'est pas couvert. - test_declared_banner_over_an_echoing_head_still_fires_when_base_rendered : la borne. Base qui rendait, tete reduite a ses lignes d'echo + banniere. Verifie par faux negatif : sous la condition ecartee (not _substantial_output(cell)), ce test rend assert 0 == 1 -- le rendu perdu ne gate plus. C'est la mesure qui a tranche l'arbitrage du merge. 37 passed (suite complete du fichier). Co-Authored-By: Claude Sonnet 5.5 --- .../tests/test_check_output_failure_text.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/scripts/notebook_tools/tests/test_check_output_failure_text.py b/scripts/notebook_tools/tests/test_check_output_failure_text.py index 1f656198e1..734634ef90 100644 --- a/scripts/notebook_tools/tests/test_check_output_failure_text.py +++ b/scripts/notebook_tools/tests/test_check_output_failure_text.py @@ -355,12 +355,14 @@ def test_declared_banner_replacing_a_base_render_still_fires(): assert not got["DECLARED_FALLBACK"] -def test_declared_fallback_kept_when_head_keeps_its_substantial_output(): - """Faux 0 -> 1 mesure sur #19697 : cellule byte-identique entre base et - tete. La base est scanee sans contexte (-> DECLARED_FALLBACK), la tete - voyait une base substantial et restait TOOL_FAILURE -- le ratchet creait - une regression sur une cellule que la PR ne touchait pas. Un remplacement - se prouve des deux cotes : la tete doit avoir PERDU le rendu.""" +def test_declared_fallback_kept_when_base_output_echoes_before_failing(): + """Faux 0 -> 1 mesure sur #19697 : le repli execute son chemin normal AVANT + d'echouer, donc sa sortie de base porte des lignes d'echo (« Inpainting: + '...' », soumission du workflow) qui la rendent ``substantial`` au sens + textuel. Ce cas est le seul qui atteint ``_in_fallback`` avec un verdict + True : ``test_declared_fallback_kept_when_base_was_already_in_fallback`` + (base = banniere seule) sort par ``_substantial_output`` des la premiere + condition, sans jamais lire ``_in_fallback``.""" out = "graphe: 2 noeuds, 1 arete\n" + BANNER_OUT base = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(out))]} head = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(out))]} @@ -371,6 +373,22 @@ def test_declared_fallback_kept_when_head_keeps_its_substantial_output(): assert len(got["DECLARED_FALLBACK"]) == 1 +def test_declared_banner_over_an_echoing_head_still_fires_when_base_rendered(): + """Le pendant du test precedent, et la raison pour laquelle la decision se + prend sur la BASE : la base RENDAIT (aucun repli), la tete n'imprime plus + que ses lignes d'echo puis la banniere. L'echo rend la sortie de tete + ``substantial`` au sens textuel -- une condition qui exigerait que la tete + ait perdu sa substance laisserait donc passer la perte de rendu.""" + out = "graphe: 2 noeuds, 1 arete\n" + BANNER_OUT + base = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, RENDER)]} + head = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(out))]} + + got = scan(head, base_nb=base) + + assert len(got["TOOL_FAILURE"]) == 1, "un rendu perdu doit continuer a gater" + assert not got["DECLARED_FALLBACK"] + + def test_declared_fallback_kept_when_base_was_already_in_fallback(): base = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(BANNER_OUT))]} head = {"cells": [id_cell("c-gv", GRAPHVIZ_SRC, stream(BANNER_OUT))]}