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
51 changes: 49 additions & 2 deletions scripts/pick_idle_grain.py
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,41 @@ def _hours_since(iso: str) -> float:
return (NOW - dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))).total_seconds() / 3600.0


# #17474 : le meme raisonnement que POOL_FETCH_LIMIT, applique aux PRs -- et
# la meme trappe. `gh pr list` rend du plus RECENT au plus ancien (mesure du
# 2026-09-23 sur ce depot : `first=2026-09-23T13:29:52Z` #17565,
# `last=2026-09-13T08:33:00Z` #15942), donc un plafond franchi ampute
# exactement la traine : les PRs bloquees depuis plus de 24 h, que
# `unattributed_blocked_prs` (file de reparation) et le compte WIP de lane
# (Q41) existent pour voir. Le plafond est donc HAUT et SURVEILLE -- un
# plafond atteint se dit au lieu d'inverser l'instrument en silence.
# Cout : nul sous le plafond. `gh` pagine par 100 et s'arrete a l'epuisement
# de la population comme au plafond, donc 158 ouvertes = 2 requetes, ici
# comme avant.
OPEN_PRS_FETCH_LIMIT = POOL_FETCH_LIMIT


def _warn_open_prs_truncated(rendered: int, ceiling: int, remedy: str) -> None:
"""La troncature se DIT : un plafond atteint ne se devine pas autrement.

Le listing rend du plus RECENT au plus ancien, et `gh` ne leve rien quand
le plafond mord. Sans ce message, la traine -- PRs bloquees de plus de
24 h, file de reparation et compte WIP de lane (Q41) -- est absente de la
mesure en silence, exactement ce que ces appelants existent pour voir. Le
garde reste utilisable (bloquer la lane serait pire) : on dit, on ne
bloque pas.
"""
print(
f"[PRS TRONQUEES] {rendered} PRs rendues pour un plafond de "
f"{ceiling} : l'ouvert est probablement plus grand. "
"gh rend les plus RECENTES, donc la traine -- PRs bloquees de "
"plus de 24 h, file de reparation et compte WIP de lane -- est "
f"absente de cette mesure. {remedy} avant de "
"conclure quoi que ce soit de ce resultat.",
file=sys.stderr,
)


def fetch_open_prs() -> list[dict]:
"""Toutes les PRs ouvertes, avec le corps (pour y lire le tag de lane).

Expand All @@ -2179,11 +2214,16 @@ def fetch_open_prs() -> list[dict]:
"""
try:
out = subprocess.run(
["gh", "pr", "list", "--repo", REPO, "--state", "open", "--limit", "300",
["gh", "pr", "list", "--repo", REPO, "--state", "open",
"--limit", str(OPEN_PRS_FETCH_LIMIT),
"--json", "number,title,body,createdAt,isDraft,author,headRefName"],
capture_output=True, text=True, encoding="utf-8", check=True, timeout=120,
).stdout
return json.loads(out)
prs = json.loads(out)
if len(prs) >= OPEN_PRS_FETCH_LIMIT:
_warn_open_prs_truncated(len(prs), OPEN_PRS_FETCH_LIMIT,
"Relever OPEN_PRS_FETCH_LIMIT")
return prs
except Exception: # noqa: BLE001 - on TENTE l'autre transport
pass
raw = [_pr_rest_to_gh_shape(it) for it in _rest_pages(
Expand All @@ -2194,6 +2234,13 @@ def fetch_open_prs() -> list[dict]:
f"quota distinct. {len(raw)} PRs lues.",
file=sys.stderr,
)
# Le transport REST porte son PROPRE plafond (pages x page) : un plafond
# atteint s'y dit comme sur la voie GraphQL, sinon la bascule de #17038
# reintroduit la troncature muette par l'autre porte.
rest_ceiling = POOL_REST_PAGE * POOL_REST_MAX_PAGES
if len(raw) >= rest_ceiling:
_warn_open_prs_truncated(len(raw), rest_ceiling,
"Relever POOL_REST_MAX_PAGES")
return raw


Expand Down
134 changes: 129 additions & 5 deletions scripts/tests/test_pick_idle_grain.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,12 +525,15 @@ def test_blocked_awaiting_review_is_not_a_red():
assert pig.blocking_causes(_state(checks=[("PR gate", "SUCCESS", True)])) == []


def _patch_backlog(monkeypatch, prs, states, nits=None):
monkeypatch.setattr(pig, "fetch_open_prs", lambda: prs)
def _neutralize_organs(monkeypatch, states, nits=None):
"""Les organes reseau du garde rouge, neutralises par DEFAUT.

Sans cela chaque test partirait sur le reseau interroger des numeros de PR
fictifs (mesure : 2,9 s pour trois numeros), et la suite deviendrait non
deterministe sans jamais rougir. #17474 : partage avec les tests qui
laissent le VRAI `fetch_open_prs` courir derriere un faux `gh`.
"""
monkeypatch.setattr(pig, "fetch_pr_states", lambda nums: {n: states[n] for n in nums if n in states})
# Neutraliser l'organe B.0 par DEFAUT : sans cela chaque test partirait sur
# le reseau interroger des numeros de PR fictifs (mesure : 2,9 s pour trois
# numeros), et la suite deviendrait non deterministe sans jamais rougir.
monkeypatch.setattr(pig, "unaddressed_review_points", lambda nums: dict(nits or {}))
# L721 : l'ardoise de lane ajoute un fetch gh dans main() AVANT le garde
# rouge -- meme neutralisation par defaut, meme raison (reseau +
Expand All @@ -545,6 +548,11 @@ def _patch_backlog(monkeypatch, prs, states, nits=None):
monkeypatch.setattr(pig, "fetch_main_head_probe", lambda *a, **k: None)


def _patch_backlog(monkeypatch, prs, states, nits=None):
monkeypatch.setattr(pig, "fetch_open_prs", lambda: prs)
_neutralize_organs(monkeypatch, states, nits)


def _pr(n, lane, age_hours, *, draft=False):
created = (pig.NOW - pig.dt.timedelta(hours=age_hours)).strftime("%Y-%m-%dT%H:%M:%SZ")
body = f"Grain: MED/guard -- lane {lane}\n" if lane else "pas de tag\n"
Expand Down Expand Up @@ -934,6 +942,38 @@ def test_pr_list_bascule_rest_et_normalise_la_forme(monkeypatch, capsys):
assert "bascule REST" in capsys.readouterr().err


def test_truncation_rest_est_dite_comme_celle_de_graphql(monkeypatch, capsys):
"""#17474 -- le transport REST porte son propre plafond : il se dit aussi.

La bascule de #17038 a ouvert une seconde porte au meme defaut : un plafond
atteint y est muet tout autant, `_rest_pages` s'arrete a
`POOL_REST_MAX_PAGES` sans rien lever, et le listing rend du plus recent au
plus ancien -- la traine disparait donc par cette porte-la sans un mot. Le
plafond est atteint ici pour de vrai (pages pleines jusqu'a epuisement du
quota), pas simule par un drapeau.
"""
pages = {p: [dict(_REST_PULL, number=p * 1000 + i)
for i in range(pig.POOL_REST_PAGE)]
for p in range(1, pig.POOL_REST_MAX_PAGES + 1)}
_fake_transport(monkeypatch, pr_graphql_ok=False, rest_pages=pages)
prs = pig.fetch_open_prs()
assert len(prs) == pig.POOL_REST_PAGE * pig.POOL_REST_MAX_PAGES
err = capsys.readouterr().err
assert "bascule REST" in err
assert "[PRS TRONQUEES]" in err
assert "POOL_REST_MAX_PAGES" in err, (
"le remede nomme doit etre celui du transport REST, pas celui de la "
"voie GraphQL")

# Controle NEGATIF : un plafond qui n'a pas mordu ne se dit pas. Sans lui,
# un avertissement inconditionnel passerait le test ci-dessus.
capsys.readouterr()
monkeypatch.setattr(pig, "POOL_REST_MAX_PAGES", pig.POOL_REST_MAX_PAGES + 1)
prs = pig.fetch_open_prs()
assert len(prs) == 400
assert "[PRS TRONQUEES]" not in capsys.readouterr().err


def test_les_deux_transports_morts_ne_se_disent_pas_pool_vide(monkeypatch):
"""#17038 acceptance 1+3+5 -- « non mesurable » n'est pas « aucun candidat ».

Expand Down Expand Up @@ -2711,6 +2751,90 @@ def test_orphan_report_neg2_human_on_chore_pending_stays(monkeypatch):
assert [r["number"] for r in pig.unattributed_blocked_prs()] == [9]


# --- #17474 : le plafond de `fetch_open_prs` amputait la traine -------------
# `gh pr list` rend du plus RECENT au plus ancien (mesure firsthand du
# 2026-09-23 sur ce depot : #17565 `2026-09-23T13:29:52Z` en tete, #15942
# `2026-09-13T08:33:00Z` en queue pour 158 ouvertes), donc un plafond franchi
# fait disparaitre les PRs les plus ANCIENNES -- exactement celles que la file
# de reparation et le compte WIP de lane (Q41) existent pour voir. Le faux `gh`
# ci-dessous reproduit ce mode d'echec plutot que de le supposer : il tronque
# la population au `--limit` DEMANDE, comme le vrai.


def _fake_gh_pr_list(monkeypatch, population):
"""Faux `gh pr list` : les plus recentes de `population`, tronquees au plafond.

`population` est ordonnee du plus recent au plus ancien, comme ce que rend
le vrai `gh`.
"""
calls = []

def fake_run(cmd, **kwargs):
calls.append(cmd)
limit = int(cmd[cmd.index("--limit") + 1])
return _FakeCompleted(json.dumps(population[:limit]))

monkeypatch.setattr(pig.subprocess, "run", fake_run)
return calls


def _tagged_pr(n):
"""PR ouverte taggee par une lane -- hors des deux volets du garde rouge."""
return {"number": n, "title": f"pr {n}",
"body": "Grain: MED/guard -- lane myia-po-2026:CoursIA\n",
"createdAt": "2026-09-20T00:00:00Z", "isDraft": False,
"author": {"login": "jsboige"}, "headRefName": f"feature/{n}"}


def test_open_prs_fetch_limit_no_longer_the_old_300(monkeypatch):
"""Le plafond demande a `gh` est celui du pool, et il est surveille."""
calls = _fake_gh_pr_list(monkeypatch, [_tagged_pr(1)])
prs = pig.fetch_open_prs()
assert prs == [_tagged_pr(1)]
limit = int(calls[0][calls[0].index("--limit") + 1])
assert limit == pig.OPEN_PRS_FETCH_LIMIT >= pig.POOL_FETCH_LIMIT


def test_open_prs_truncation_is_said_not_silent(monkeypatch, capsys):
"""Sous le plafond : rien de dit. Au plafond : la troncature se dit."""
monkeypatch.setattr(pig, "OPEN_PRS_FETCH_LIMIT", 4)
_fake_gh_pr_list(monkeypatch, [_tagged_pr(n) for n in (5, 4, 2)])
pig.fetch_open_prs()
assert capsys.readouterr().err == "", "aucun avertissement sous le plafond"

monkeypatch.setattr(pig, "OPEN_PRS_FETCH_LIMIT", 3)
_fake_gh_pr_list(monkeypatch, [_tagged_pr(n) for n in (5, 4, 3, 2)])
pig.fetch_open_prs()
err = capsys.readouterr().err
assert "[PRS TRONQUEES]" in err
assert "3 PRs rendues pour un plafond de 3" in err
assert "OPEN_PRS_FETCH_LIMIT" in err


def test_the_oldest_blocked_orphan_survives_the_cap(monkeypatch):
"""#17474, faux negatif : la traine reste vue par la file de reparation.

La population porte 320 PRs taggees (invisibles au garde rouge) et, en
queue -- donc la plus ANCIENNE --, une orpheline bloquee. Le controle de
falsification est dans le test : au plafond historique de 300, la meme
fixture ne voit RIEN, ce qui prouve que le test mord sur le defaut et non
sur un faux `gh` complaisant.
"""
population = [_tagged_pr(10_000 - i) for i in range(320)] + [_untagged_pr(17)]
red = {17: _state(checks=[("PR gate", "FAILURE", True)])}
_fake_gh_pr_list(monkeypatch, population)
_neutralize_organs(monkeypatch, red)

monkeypatch.setattr(pig, "OPEN_PRS_FETCH_LIMIT", 300)
assert pig.unattributed_blocked_prs() == [], (
"au plafond de 300 la traine doit etre invisible -- sinon la fixture "
"ne reproduit pas la troncature reelle"
)

monkeypatch.setattr(pig, "OPEN_PRS_FETCH_LIMIT", pig.POOL_FETCH_LIMIT)
assert [r["number"] for r in pig.unattributed_blocked_prs()] == [17]


# --- #15139 : delegation a l'organe check_unaddressed_nits -----------------
# Incident fondateur (2026-09-07, mesure sur myia-po-2027:CoursIA-2) : la lane
# portait trois PRs a remarques non levees dont deux du user, et le picker
Expand Down
Loading