fix(anchors): lève 2 nits revue — contrat crop corrompu + workflow_id sidecar — 2026-07-03

Suite revue adversariale MERGE-READY (« non-bloquant ≠ oublié », Dom) :

1. Contrat crop CORROMPU figé par test : `materialize_anchor_from_b64` retire la
   clé `_anchor_image_base64` de params AVANT le try (pop). Un crop inexploitable
   (b64 invalide, PNG au magic cassé) → pas d'exception, anchor_id NULL, aucun
   VisualAnchor, ET clé retirée (pas de donnée morte volumineuse dans les params).
   3 cas paramétrés verrouillent ce comportement assumé. Docstring clarifiée.

2. `workflow_id` réintroduit dans le sidecar metadata.json (parité avec la route
   d'origine avant refactor). Nouveau paramètre optionnel du helper, passé par les
   deux appelants live (route HTTP + bridge R1). 2 tests : présent quand fourni,
   absent sinon (pas de clé None parasite). Backfill inchangé (workflow_id optionnel).

TDD : +5 tests. 29 tests unit VERTS, 0 régression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-07-03 10:43:36 +02:00
parent 0a70ac1334
commit 9d99c370b3
4 changed files with 113 additions and 4 deletions

View File

@@ -351,3 +351,99 @@ def test_http_route_delegates_to_shared_helper(db_app, anchor_data_dir):
assert step.anchor_id is not None
assert VisualAnchor.query.get(step.anchor_id) is not None
assert "_anchor_image_base64" not in step.parameters
# ---------------------------------------------------------------------------
# 7. Crop CORROMPU : contrat figé (pas d'exception, anchor_id NULL, clé retirée)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"corrupt_b64",
[
"!!! pas du base64 valide !!!", # base64 invalide
"Zm9vYmFy", # b64 valide mais pas une image
base64.b64encode(b"\x89PNG\x0d\x0a broken").decode("ascii"), # magic PNG cassé
],
ids=["b64_invalide", "b64_non_image", "png_magic_casse"],
)
def test_corrupt_crop_is_tolerated(db_app, anchor_data_dir, corrupt_b64):
"""FIGE le contrat : un `_anchor_image_base64` corrompu est inexploitable.
Le helper ne lève PAS, retourne None, laisse `anchor_id` NULL, ne crée
aucun VisualAnchor — ET retire quand même la clé b64 de params (pop avant
le try : un crop mort ne doit pas traîner, volumineux, dans les params)."""
from services.anchor_image_service import materialize_anchor_from_b64
with db_app.app_context():
wf = Workflow(id="wf_corrupt", name="wf", source="learned_import")
db.session.add(wf)
step = Step(id="step_corrupt", workflow_id="wf_corrupt",
action_type="click_anchor", order=0, label="cible")
db.session.add(step)
params = {"_anchor_image_base64": corrupt_b64, "target_text": "OK"}
# Ne doit PAS lever malgré le crop corrompu.
anchor_id = materialize_anchor_from_b64(
step, params, db_session=db.session
)
assert anchor_id is None, "crop corrompu → pas d'anchor_id"
assert step.anchor_id is None, "anchor_id reste NULL"
assert VisualAnchor.query.count() == 0, "aucun VisualAnchor créé"
assert "_anchor_image_base64" not in params, \
"clé b64 retirée même si le crop est inexploitable (pop avant try)"
assert params.get("target_text") == "OK", "autres params préservés"
# ---------------------------------------------------------------------------
# 8. workflow_id présent dans le sidecar metadata.json (parité route d'origine)
# ---------------------------------------------------------------------------
def test_workflow_id_written_to_sidecar_metadata(db_app, anchor_data_dir):
"""Le `workflow_id` passé au helper est écrit dans le sidecar metadata.json
(champ informatif — parité avec la route HTTP d'origine)."""
from services.anchor_image_service import (
materialize_anchor_from_b64,
get_anchor_metadata,
)
with db_app.app_context():
wf = Workflow(id="wf_meta", name="wf", source="learned_import")
db.session.add(wf)
step = Step(id="step_meta", workflow_id="wf_meta",
action_type="click_anchor", order=0)
db.session.add(step)
params = {"_anchor_image_base64": _png_b64()}
anchor_id = materialize_anchor_from_b64(
step, params, db_session=db.session, workflow_id="wf_meta"
)
assert anchor_id is not None
meta = get_anchor_metadata(anchor_id)
assert meta is not None, "metadata.json écrit"
extra = meta.get("extra") or {}
assert extra.get("workflow_id") == "wf_meta", \
"workflow_id présent dans le sidecar (parité route d'origine)"
assert extra.get("source") == "learned_import"
def test_workflow_id_absent_when_not_provided(db_app, anchor_data_dir):
"""Sans workflow_id fourni, la clé n'est pas écrite (pas de None parasite)."""
from services.anchor_image_service import (
materialize_anchor_from_b64,
get_anchor_metadata,
)
with db_app.app_context():
wf = Workflow(id="wf_nom", name="wf", source="learned_import")
db.session.add(wf)
step = Step(id="step_nom", workflow_id="wf_nom",
action_type="click_anchor", order=0)
db.session.add(step)
anchor_id = materialize_anchor_from_b64(
step, {"_anchor_image_base64": _png_b64()}, db_session=db.session
)
meta = get_anchor_metadata(anchor_id)
extra = meta.get("extra") or {}
assert "workflow_id" not in extra, "pas de clé workflow_id parasite"