From e2badc9c7cbc1e931dab37eb1ca6864d232917a6 Mon Sep 17 00:00:00 2001 From: jsboige Date: Sun, 27 Sep 2026 21:06:12 +0200 Subject: [PATCH] fix(coordination,#18095): closure gate evaluates the latest dossier (latest-wins) evaluate() stopped at the first [CLOSURE PREFLIGHT] comment: a malformed first dossier or a superseded KEEP masked the correct dossier posted after it. The gate now evaluates only the last dossier, like the PR gate; earlier dossiers are replaced, never an error nor a staleness of the newest one. Tests: malformed-then-valid -> CLOSE (positive control, #16916 shape), valid-then-malformed -> REFUSED, KEEP-then-CLOSE -> CLOSE, CLOSE-then-KEEP -> KEEP. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/check_closure_dossier.py | 30 +++++++---- scripts/tests/test_check_closure_dossier.py | 60 +++++++++++++++++++++ 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/scripts/check_closure_dossier.py b/scripts/check_closure_dossier.py index 7281d526fb..87c4dfec9a 100644 --- a/scripts/check_closure_dossier.py +++ b/scripts/check_closure_dossier.py @@ -425,7 +425,14 @@ def validate_dossier(dossier: Dossier, snapshot: dict[str, Any], def evaluate(snapshot: dict[str, Any], replay: bool = False) -> tuple[str, list[str], Dossier | None]: - """(verdict, errors, dossier) -- verdict CLOSE / KEEP / NO-DOSSIER.""" + """(verdict, errors, dossier) -- verdict CLOSE / KEEP / NO-DOSSIER. + + Seul le DERNIER dossier compte (latest-wins, meme regle que le gate PR, + #18095) : un dossier plus ancien est remplace, jamais une erreur ni une + peremption du plus recent. Evaluer le premier laissait un dossier + malforme ou un KEEP perime masquer indefiniment le dossier correct + poste apres lui.""" + latest: tuple[Dossier, list[str]] | None = None for index, row in enumerate(snapshot["comments"]): body = row.get("body") or "" if not body.strip().startswith(START): @@ -435,16 +442,17 @@ def evaluate(snapshot: dict[str, Any], (row.get("author") or {}).get("login", ""), row.get("created_at", ""), ) - if dossier is None: - continue - errors.extend(validate_dossier(dossier, snapshot, replay)) - verdict = dossier.fields.get("verdict", "") - if errors: - return ("REFUSED", errors, dossier) - if verdict == VERDICT_CLOSE: - return (VERDICT_CLOSE, [], dossier) - return (VERDICT_KEEP, [], dossier) - return ("NO-DOSSIER", ["no [CLOSURE PREFLIGHT] comment on this issue"], None) + if dossier is not None: + latest = (dossier, errors) + if latest is None: + return ("NO-DOSSIER", ["no [CLOSURE PREFLIGHT] comment on this issue"], None) + dossier, errors = latest + errors.extend(validate_dossier(dossier, snapshot, replay)) + if errors: + return ("REFUSED", errors, dossier) + if dossier.fields.get("verdict", "") == VERDICT_CLOSE: + return (VERDICT_CLOSE, [], dossier) + return (VERDICT_KEEP, [], dossier) def render_template(snapshot: dict[str, Any], lane: str) -> str: diff --git a/scripts/tests/test_check_closure_dossier.py b/scripts/tests/test_check_closure_dossier.py index dc06b2d46a..b8353b62b7 100644 --- a/scripts/tests/test_check_closure_dossier.py +++ b/scripts/tests/test_check_closure_dossier.py @@ -151,6 +151,66 @@ def test_commentaire_neutre_posterieur_ne_perime_pas(): assert errors == [] +# --- latest-wins : seul le dernier dossier compte (#18095) ------------------- + +def test_dossier_malforme_puis_dossier_valide_ferme(): + # Controle positif (forme #16916) : un premier dossier malforme ne masque + # plus le dossier correct poste apres lui. + malformed = _dossier_body().replace("residue: none\n", "") + snap = _snapshot(comments=[ + _comment(malformed, created="2026-09-20T10:00:00Z"), + _comment(_dossier_body(comments_reviewed=1), + created="2026-09-21T10:00:00Z"), + ]) + verdict, errors, dossier = evaluate(snap) + assert verdict == "CLOSE" + assert errors == [] + assert dossier.comment_index == 1 + + +def test_dossier_valide_puis_dossier_malforme_refuse(): + # Controle negatif : le dernier dossier fait foi, meme malforme -- un + # dossier valide plus ancien ne le rattrape pas. + malformed = _dossier_body(comments_reviewed=1).replace("residue: none\n", "") + snap = _snapshot(comments=[ + _comment(_dossier_body(), created="2026-09-20T10:00:00Z"), + _comment(malformed, created="2026-09-21T10:00:00Z"), + ]) + verdict, errors, dossier = evaluate(snap) + assert verdict == "REFUSED" + assert any("missing fields: residue" in e for e in errors) + assert dossier.comment_index == 1 + + +def test_keep_puis_close_rend_close(monkeypatch): + monkeypatch.setattr(ccd, "gh_json", lambda args: { + "number": 17910, "state": "OPEN"}) + snap = _snapshot(comments=[ + _comment(_dossier_body(verdict="KEEP", residue="followup #17910"), + created="2026-09-20T10:00:00Z"), + _comment(_dossier_body(comments_reviewed=1), + created="2026-09-21T10:00:00Z"), + ]) + verdict, errors, _ = evaluate(snap) + assert verdict == "CLOSE" + assert errors == [] + + +def test_close_puis_keep_rend_keep(monkeypatch): + # Symetrique : un KEEP recent retire un CLOSE plus ancien. + monkeypatch.setattr(ccd, "gh_json", lambda args: { + "number": 17910, "state": "OPEN"}) + snap = _snapshot(comments=[ + _comment(_dossier_body(), created="2026-09-20T10:00:00Z"), + _comment(_dossier_body(verdict="KEEP", residue="followup #17910", + comments_reviewed=1), + created="2026-09-21T10:00:00Z"), + ]) + verdict, errors, _ = evaluate(snap) + assert verdict == "KEEP" + assert errors == [] + + def test_pr_ouverte_referencant_lissue_refuse(): # Le contrat dit open-prs: 0 ; le live en trouve une -> perime. snap = _snapshot(comments=[_comment(_dossier_body())], open_prs=[18050])