Gitea dismisses an approval made stale by a push in a job it queues on the push (services/pull AddTestPullRequestTask, v28.1.0), and its merge check counts approvals not yet dismissed. Measured here, the approval stands for 0.15-0.23 s after the push; the test's merge came a couple of API calls later, so it usually lost the race, and once won it. The test now waits for the dismissal, with a timeout, before expecting Gitea's refusal, and checks the agent's own rule at once: the PR's head is read from the branch, so the approval is for an older commit straight away. Also notes in the Development Plan why the helper loop doesn't compact. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
100 lines
4.0 KiB
Python
100 lines
4.0 KiB
Python
"""The trust dial's hard wall, on a real Gitea (D4, D5; M3.3): with the
|
|
protection a `merge: approved` project has, the agent's own token can't
|
|
merge its PR until an operator approves the PR as it is now; it can then;
|
|
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 time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from e2e_support import fresh_repo, operator_gitea
|
|
|
|
from nisse_core.gitea import GiteaClient, GiteaError
|
|
from nisse_dev.config import AgentConfig
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
# How long Gitea gets to dismiss an approval after a push (#162).
|
|
DISMISS_WAIT_S = 30.0
|
|
|
|
|
|
def add_file(gitea: GiteaClient, repo: str, branch: str, path: str) -> None:
|
|
gitea._request(
|
|
"POST",
|
|
f"/repos/{repo}/contents/{path}",
|
|
json={
|
|
"content": base64.b64encode(f"{path}\n".encode()).decode(),
|
|
"message": f"add {path}",
|
|
"branch": branch,
|
|
},
|
|
)
|
|
|
|
|
|
def approve(operator: GiteaClient, repo: str, pr: int) -> None:
|
|
operator._request(
|
|
"POST", f"/repos/{repo}/pulls/{pr}/reviews", json={"event": "APPROVED"}
|
|
)
|
|
|
|
|
|
def wait_for_dismissal(gitea: GiteaClient, repo: str, pr: int) -> None:
|
|
"""Gitea dismisses a stale approval in a job it queues on the push, not
|
|
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
|
|
while gitea.approvals(repo, pr): # dismissed ones aren't listed
|
|
assert time.monotonic() < deadline, (
|
|
f"Gitea didn't dismiss the approval within {DISMISS_WAIT_S}s"
|
|
)
|
|
time.sleep(0.5)
|
|
|
|
|
|
def test_merging_needs_the_operators_approval_of_the_pr_as_it_is(
|
|
tmp_path: Path, live_config: AgentConfig, gitea: GiteaClient
|
|
) -> None:
|
|
operator = operator_gitea(live_config.gitea_url)
|
|
run = uuid.uuid4().hex[:8]
|
|
with fresh_repo(operator, run, tmp_path / "keys", agent=gitea.whoami()) as repo:
|
|
name = repo.name
|
|
# The agent's PR, its branch made through the API.
|
|
gitea.branch_with_file(
|
|
name, base="main", branch="m-1", path="a.txt", text="a\n", message="a"
|
|
)
|
|
pr = gitea.open_pull_request(name, head="m-1", base="main", title="a", body="")
|
|
with pytest.raises(GiteaError):
|
|
gitea.merge_pull_request(name, pr.number) # no approval yet
|
|
|
|
approve(operator, name, pr.number)
|
|
[approval] = gitea.approvals(name, pr.number)
|
|
head = gitea.pull_request(name, pr.number).head_sha
|
|
assert (approval.author, approval.commit) == (operator.whoami(), head)
|
|
gitea.merge_pull_request(name, pr.number) # approved: the agent may
|
|
assert gitea.pull_request(name, pr.number).merged
|
|
|
|
# Approved, then changed: the approval no longer stands.
|
|
gitea.branch_with_file(
|
|
name, base="main", branch="m-2", path="b.txt", text="b\n", message="b"
|
|
)
|
|
second = gitea.open_pull_request(
|
|
name, head="m-2", base="main", title="b", body=""
|
|
)
|
|
approve(operator, name, second.number)
|
|
approved = gitea.pull_request(name, second.number).head_sha
|
|
add_file(gitea, name, "m-2", "c.txt") # a fix pushed after the approval
|
|
# At once, with no waiting: the PR's head is read from the branch,
|
|
# so the agent sees the new commit, and the approval is for the old
|
|
# one. Its own check (an approval at the head, not stale) holds off.
|
|
after = gitea.pull_request(name, second.number).head_sha
|
|
assert after != approved
|
|
assert not [
|
|
a
|
|
for a in gitea.approvals(name, second.number)
|
|
if a.commit == after and not a.stale
|
|
]
|
|
# Gitea's own refusal comes once it has dismissed the approval.
|
|
wait_for_dismissal(gitea, name, second.number)
|
|
with pytest.raises(GiteaError):
|
|
gitea.merge_pull_request(name, second.number)
|