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."""
import base64
import re
import time
import uuid
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.
A merge tried before that job has run can still go through (#162)."""
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, (
f"Gitea didn't dismiss the approval within {DISMISS_WAIT_S}s"
)
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(
tmp_path: Path, live_config: AgentConfig, gitea: GiteaClient
) -> None: