Trust test: wait for Gitea to dismiss a stale approval before merging (#162) #164

Merged
dev-agent merged 2 commits from flaky-stale-approval into main 2026-10-18 13:23:25 +00:00
Showing only changes of commit a1eeab1bc7 - Show all commits
+18 -1
View File
@@ -5,6 +5,7 @@ and a commit pushed after the approval needs approving again: at once by the
agent's own check, and by Gitea once it has dismissed the approval.""" agent's own check, and by Gitea once it has dismissed the approval."""
import base64 import base64
import re
import time import time
import uuid import uuid
from pathlib import Path from pathlib import Path
@@ -44,13 +45,29 @@ def wait_for_dismissal(gitea: GiteaClient, repo: str, pr: int) -> None:
in the push itself, and its merge check counts what isn't dismissed yet. in the push itself, and its merge check counts what isn't dismissed yet.
A merge tried before that job has run can still go through (#162).""" A merge tried before that job has run can still go through (#162)."""
deadline = time.monotonic() + DISMISS_WAIT_S deadline = time.monotonic() + DISMISS_WAIT_S
Review

This loop makes up to DISMISS_WAIT_S / 0.5 ≈ 60 GET calls, so it has far more exposure to a transient Gitea error (a 5xx, a dropped connection) than any single call elsewhere in the test. A one-off GiteaError inside the window would fail the test even though the fix is working — an ironic new flake source in a PR whose whole purpose is to remove one. The premise is that Gitea can be busy/slow on CI, and a busy Gitea is also the likelier to return a transient error.

The sibling live test (test_end_to_end_loop.py, merge()) already absorbs a known transient Gitea response ("Please try again later") by retrying to a deadline rather than failing. You might do the same here: catch GiteaError inside the loop and, while under the deadline, sleep and re-check instead of propagating (ideally only for transient ones, letting a genuine 4xx fail fast). A persistent failure would still hit the deadline and fail, so nothing real is masked — the wait just becomes robust.

This loop makes up to DISMISS_WAIT_S / 0.5 ≈ 60 GET calls, so it has far more exposure to a transient Gitea error (a 5xx, a dropped connection) than any single call elsewhere in the test. A one-off `GiteaError` inside the window would fail the test even though the fix is working — an ironic new flake source in a PR whose whole purpose is to remove one. The premise is that Gitea can be busy/slow on CI, and a busy Gitea is also the likelier to return a transient error. The sibling live test (`test_end_to_end_loop.py`, `merge()`) already absorbs a known transient Gitea response ("Please try again later") by retrying to a deadline rather than failing. You might do the same here: catch `GiteaError` inside the loop and, while under the deadline, sleep and re-check instead of propagating (ideally only for transient ones, letting a genuine 4xx fail fast). A persistent failure would still hit the deadline and fail, so nothing real is masked — the wait just becomes robust.
Review

Agreed, done in a1eeab1. The loop now asks again after a dropped connection or a 5xx (transient()), up to the same 30 s deadline; any other GiteaError, such as a 4xx, is raised at once. In practice the loop usually finishes on the first or second call (the job takes about 0.2 s), but a busy Gitea is exactly when it would matter.

Agreed, done in a1eeab1. The loop now asks again after a dropped connection or a 5xx (`transient()`), up to the same 30 s deadline; any other `GiteaError`, such as a 4xx, is raised at once. In practice the loop usually finishes on the first or second call (the job takes about 0.2 s), but a busy Gitea is exactly when it would matter.
while gitea.approvals(repo, pr): # dismissed ones aren't listed while True:
try:
if not gitea.approvals(repo, pr): # dismissed ones aren't listed
return
except GiteaError as exc:
# A busy Gitea may drop a call or answer 5xx; ask again. Anything
# else, such as a 4xx, is a real failure.
if not transient(exc) or time.monotonic() > deadline:
raise
assert time.monotonic() < deadline, ( assert time.monotonic() < deadline, (
f"Gitea didn't dismiss the approval within {DISMISS_WAIT_S}s" f"Gitea didn't dismiss the approval within {DISMISS_WAIT_S}s"
) )
time.sleep(0.5) time.sleep(0.5)
def transient(exc: GiteaError) -> bool:
"""A dropped connection or a 5xx: worth asking again."""
text = str(exc)
return text.startswith("gitea request failed") or bool(
re.search(r" returned 5\d\d:", text)
)
def test_merging_needs_the_operators_approval_of_the_pr_as_it_is( def test_merging_needs_the_operators_approval_of_the_pr_as_it_is(
tmp_path: Path, live_config: AgentConfig, gitea: GiteaClient tmp_path: Path, live_config: AgentConfig, gitea: GiteaClient
) -> None: ) -> None: