Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 68 additions & 3 deletions scripts/pick_idle_grain.py
Original file line number Diff line number Diff line change
Expand Up @@ -10178,6 +10178,54 @@ def expired():
user_delivery_event_probe = (delivery_event_probe
or delivery_event_probe_inert)

def marker_read_is_local(number):
"""La lecture du marqueur de `number` resterait-elle LOCALE ?

Le mur de temps (`expired`) borne le RESEAU ; il ne borne pas une
lecture locale. Les deux filtres de marqueur (recent, conteneur)
lisent `_last_comment`, qui sert depuis l'instantane de la passe
quand il le peut (#22630). Les sauter au motif de l'echeance sert
donc un candidat dont la cloture etait CONNUE.

Mesure du 2026-11-03 (draw `32e6c570269a`, lane
`myia-po-2027:CoursIA`) : le tapis a servi 4 candidats sur 4
portant chacun un marqueur de livraison dans leur dernier
commentaire, avec un `ACTION REQUISE` « poser [CLAIMED] sur
#19694 » -- alors que l'instantane avait servi 36 stamps sur 36
SANS une seule requete et que `time_budget_hit` etait vrai. Le
cout du filtre n'etait pas le reseau : c'etait le mur.

Le predicat est lu sur la MEME surface que la sonde
(`_snapshot_comments`), donc il ne peut pas promettre une lecture
locale que la sonde ne ferait pas : hors instantané, la garde
`expired()` reste entiere et le fail-OPEN reste celui d'avant.

RESERVE #24505 (review Hermes, reproduite par execution) :
« instantané présent » ne suffit pas. Un corps de DOSSIER DE
CLOTURE (verdict CLOSE, ou annonce en prose) déclenche dans
`has_recent_delivery_marker` l'appel `_delivery_voided_by_reopen`
-> `_reopen_events` -> une requete REST du TIMELINE, tiree
APRES le mur. Le predicat rend donc faux pour cette classe de
corps : le fail-OPEN historique (sauter le filtre, le dire)
reprend, au lieu d'une requete reseau non budgetee. Les corps
ordinaires (`[DELIVERED]`, `[FROZEN]`, `[INFO]
candidate-delivered`, `[RELEASED]`) restent 100 % locaux --
c'est exactement la classe que la mesure fondatrice couvrait.
"""
comments = _snapshot_comments(number)
if comments is None:
return False
if not isinstance(comments, list) or not comments:
return True
last = comments[-1]
body = (last or {}).get("body") if isinstance(last, dict) else None
if not isinstance(body, str):
return True
# Un dossier de cloture rallume le reseau (regle de reouverture
# #24250) : il ne compte pas comme une lecture locale.
return not (_closure_dossier_verdict(body) == "CLOSE"
or _closure_announced_close(body))

def counted_probe(number, lane_name):
# L'echeance passe AVANT le plafond de sondes : les deux produisent le
# meme fail-OPEN, mais elles ne disent pas la meme chose a la lane
Expand Down Expand Up @@ -10382,8 +10430,9 @@ def claims_probe(numbers):
and age_h is not None
and age_h >= CONSOLIDATION_LAG_H)
else:
recent_flag = (False if expired()
else user_recent_marker_probe(n))
recent_flag = (
False if (expired() and not marker_read_is_local(n))
else user_recent_marker_probe(n))
if recent_flag is None:
# Meme traitement que ci-dessus : le fail-OPEN reste
# assume, le SILENCE ne l'est pas. La sonde du marqueur
Expand Down Expand Up @@ -10447,7 +10496,8 @@ def claims_probe(numbers):
# s'appliquait qu'a l'urne `grain`. La lecture est HORS budget de
# sonde (1 requete, comme le filtre recent) et reste fail-OPEN :
# une lecture en echec laisse l'umbrella servable.
recent = False if expired() else user_container_marker_probe(n)
recent = (False if (expired() and not marker_read_is_local(n))
else user_container_marker_probe(n))
if recent is None:
# La lecture a ECHOUE (reseau, 403, payload illisible) : l'etat
# de livraison du CONTENEUR est INCONNU, pas « libre ». Le
Expand Down Expand Up @@ -11710,6 +11760,21 @@ def _probe(n: int) -> str | None:
top = belt_picks[0]
print(f" ACTION REQUISE : poser [CLAIMED] lane {args.lane} sur "
f"#{top['number']} avant edition -- cf lane-claim-protocol.")
if belt_time_budget_hit[0]:
# Une tete servie au-dela du mur n'est pas verifiee : la
# ligne ci-dessus est une INJONCTION, elle doit donc dire
# a cote d'elle que son objet n'a pas ete lu. Mesure du
# 2026-11-03 : le tirage au mur a servi 4 candidats sur 4
# portant un marqueur de livraison, et la lane recevait
# « poser [CLAIMED] sur #19694 » sans reserve.
print(" CANDIDAT NON VERIFIE (#21436) : le mur de temps a")
print(" ete atteint avant ses lectures de livraison et de")
print(" claim. Lire le dernier commentaire AVANT de claimer.")
print(" Les lectures restees LOCALES apres le mur : le")
print(" dernier commentaire en instantane, sauf dossier de")
print(" cloture (qui rallume la regle de reouverture, donc")
print(" du reseau) -- pour lui, le filtre est saute et le")
print(" silence est rapporte. Les sondes RESEAU s'arretent.")
# #17038 (residu du 2026-10-09) -- contrat de verdict : un pick
# dont le claim est ERROR n'est PAS un candidat libre, c'est un
# candidat NON MESURE. Le servir reste le comportement (le tapis
Expand Down
168 changes: 168 additions & 0 deletions scripts/tests/test_pick_idle_grain_belt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3133,3 +3133,171 @@ def fake_metrics(pool, **kw):
"l'echeance fuit apres la sortie --report (#23340 point 1)")
finally:
pig._BELT_DEADLINE[0] = None


def test_belt_deadline_keeps_the_marker_filter_when_the_read_is_local(monkeypatch):
"""Le mur de temps arrete le RESEAU ; il n'a aucune raison d'arreter une
lecture deja en memoire.

Defaut mesure le 2026-11-03 (draw `32e6c570269a`, lane
`myia-po-2027:CoursIA`) : les deux filtres de marqueur (recent, conteneur)
etaient sautes des que `expired()` rendait vrai, alors que l'instantane
de la passe avait deja lu les commentaires de la fenetre (36 stamps sur
36 servis sans une seule requete). Le tapis a servi 4 candidats sur 4
portant chacun un marqueur de livraison dans leur dernier commentaire, et
la lane recevait « ACTION REQUISE : poser [CLAIMED] sur #19694 » pour une
issue livree depuis le 20/10.

Ici `30201` a ses commentaires en memoire et porte un marqueur : il doit
etre ECARTE malgre l'echeance. `30202` n'a pas d'entree locale : sa sonde
reste sautee (comportement d'avant, fail-OPEN rapporte).
"""
_patched_clock(monkeypatch)
local = _make_item(30201, age_days=100, idle=9, last=None)
remote = _make_item(30202, age_days=99, idle=9, last=None)
monkeypatch.setattr(
pig, "_snapshot_comments",
lambda n: ([{"body": "[INFO] candidate-delivered -- lane x",
"createdAt": "2026-11-03T10:00:00Z"}]
if n == 30201 else None))
calls = []

def marker(n):
calls.append(n)
return n == 30201

picks, withheld, state = pig.belt_pick_with_replacements(
[local, remote], {}, _belt_args(grains=2), probe_budget=8,
delivered_probe=lambda n, lane=None: False,
merged_pr_probe=lambda n, lane=None: False,
claims_probe=lambda nums: {},
recent_marker_probe=marker,
time_budget_s=0.0)

assert calls == [30201], (
"seul un candidat dont la lecture est LOCALE est filtre apres le mur")
assert [p["number"] for p in picks] == [30202]
assert [w[0]["number"] for w in withheld] == [30201]
assert "LIVRAISON (recent)" in withheld[0][1]
assert state["time_budget_hit"] is True


def test_belt_deadline_still_skips_the_marker_filter_without_local_read(monkeypatch):
"""Controle negatif : sans instantane, le filtre coute une requete et
reste soumis au mur. Le correctif ne rallume pas le reseau apres
l'echeance."""
_patched_clock(monkeypatch)
a = _make_item(30211, age_days=100, idle=9, last=None)
b = _make_item(30212, age_days=99, idle=9, last=None)
monkeypatch.setattr(pig, "_snapshot_comments", lambda n: None)
calls = []

picks, withheld, state = pig.belt_pick_with_replacements(
[a, b], {}, _belt_args(grains=2), probe_budget=8,
delivered_probe=lambda n, lane=None: False,
merged_pr_probe=lambda n, lane=None: False,
claims_probe=lambda nums: {},
recent_marker_probe=lambda n: calls.append(n) or True,
time_budget_s=0.0)

assert calls == [], "aucune sonde de marqueur ne part apres le mur"
assert [p["number"] for p in picks] == [30211, 30212]
assert withheld == []


# Corps reel d'un dossier de cloture CLOSE (grammaire canonique de
# `check_closure_dossier.py` -- meme fixture que test_pick_closure_reopened).
DOSSIER_CLOSE_24505 = """[CLOSURE PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA
issue: 40003
verdict: CLOSE
acceptance:
- "criteres confrontes a main avec preuve citee"
residue: none
open-prs: 0
comments-reviewed: 3
[/CLOSURE PREFLIGHT]"""


def test_belt_deadline_skips_the_marker_filter_on_closure_dossier(monkeypatch):
"""Reserve review #24505, reproduite par execution : un dossier de
cloture en dernier commentaire rallume le RESEAU meme apres le mur.

Le corps CLOSE declenche dans `has_recent_delivery_marker` la regle de
reouverture (#24250) -> `_reopen_events` -> une requete REST du timeline.
Le predicat `marker_read_is_local` d'avant la correction repondait
« True » des que l'instantane existait : le filtre de marqueur passait,
et la requete timeline partait au-dela de l'echeance (reproduit par
Hermes sur un dossier CLOSE valide ET sur l'annonce en prose : appels
`gh api --paginate ... issues/<N>/timeline` malgre le mur).

Apres correction, les deux classes dans le meme tirage : le porteur du
dossier n'est PAS sonde (lecture non locale -> filtre saute, fail-OPEN
historique, silence rapporte) ; le porteur d'un marqueur ordinaire reste
filtre comme avant le mur -- le correctif ne dessert pas la classe que
la mesure fondatrice (#21436) couvrait."""
_patched_clock(monkeypatch)
dossier = _make_item(30301, age_days=100, idle=9, last=None)
ordinary = _make_item(30302, age_days=99, idle=9, last=None)
bodies = {
30301: [{"body": DOSSIER_CLOSE_24505,
"createdAt": "2026-11-03T10:00:00Z"}],
30302: [{"body": "[INFO] candidate-delivered -- lane x",
"createdAt": "2026-11-03T10:00:00Z"}],
}
monkeypatch.setattr(pig, "_snapshot_comments", lambda n: bodies.get(n))
calls = []

def marker(n):
calls.append(n)
return True

picks, withheld, state = pig.belt_pick_with_replacements(
[dossier, ordinary], {}, _belt_args(grains=2), probe_budget=8,
delivered_probe=lambda n, lane=None: False,
merged_pr_probe=lambda n, lane=None: False,
claims_probe=lambda nums: {},
recent_marker_probe=marker,
time_budget_s=0.0)

assert calls == [30302], (
"seul le marqueur ordinaire (lecture locale) est sonde apres le mur ; "
"le dossier CLOSE ne doit pas y rattacher une requete timeline")
assert [p["number"] for p in picks] == [30301], (
"le porteur du dossier reste servi : filtre saute (fail-OPEN)")
assert [w[0]["number"] for w in withheld] == [30302]
assert "LIVRAISON (recent)" in withheld[0][1]
assert state["time_budget_hit"] is True


def test_belt_deadline_skips_marker_filter_on_prose_close_announcement(
monkeypatch):
"""Meme reserve #24505, seconde classe reproduite par Hermes : l'annonce
de fermeture EN PROSE (« Fermeture sur dossier ... verdict CLOSE »)-
declenche la meme regle de reouverture qu'un dossier qui parse. Le
predicat doit la traiter pareil : lecture non locale -> filtre saute
apres le mur, aucune sonde."""
_patched_clock(monkeypatch)
annonce = _make_item(30311, age_days=100, idle=9, last=None)
body = ("Fermeture sur dossier `[CLOSURE PREFLIGHT]` (verdict CLOSE, "
"gate `check_closure_dossier.py` rc=0, commentaire ci-dessus).")
monkeypatch.setattr(
pig, "_snapshot_comments",
lambda n: ([{"body": body, "createdAt": "2026-11-03T10:00:00Z"}]
if n == 30311 else None))
calls = []

picks, withheld, state = pig.belt_pick_with_replacements(
[annonce], {}, _belt_args(grains=1), probe_budget=4,
delivered_probe=lambda n, lane=None: False,
merged_pr_probe=lambda n, lane=None: False,
claims_probe=lambda nums: {},
recent_marker_probe=lambda n: calls.append(n) or True,
time_budget_s=0.0)

assert calls == [], (
"l'annonce en prose ne doit pas rattacher une sonde apres le mur")
assert [p["number"] for p in picks] == [30311]
assert withheld == []
assert state["time_budget_hit"] is True
Loading