Compare commits

..
3 Commits
Author SHA1 Message Date
dev-agent 098d42ff2c Merge pull request 'Trust test: wait for Gitea to dismiss a stale approval before merging (#162)' (#164) from flaky-stale-approval into main
CI / checks (push) Successful in 2m54s
CI / e2e (push) Skipped
2026-10-18 13:23:25 +00:00
dongarandClaude Opus 5.5 a1eeab1bc7 test_trust_live: ride out a transient Gitea error while waiting for the dismissal
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m55s
CI / e2e (pull_request) Successful in 4m45s
A dropped connection or a 5xx inside the wait is retried until the
deadline; any other error fails at once (review on #164).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-18 13:16:10 +00:00
dongarandClaude Opus 5.5 63ad679c8b Trust test: wait for Gitea to dismiss a stale approval before merging (#162)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m9s
CI / e2e (pull_request) Successful in 4m40s
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
2026-10-18 04:27:59 +00:00
2 changed files with 47 additions and 3 deletions

No files matched your search

+5 -2
View File
@@ -782,8 +782,11 @@ if the implementation differs.
with code's list of the calls they replace, become a `compaction` event with code's list of the calls they replace, become a `compaction` event
that renders in place of its range; a later one folds in the earlier. that renders in place of its range; a later one folds in the earlier.
Failed notes are logged (`context.compaction_failed`) and fitting takes Failed notes are logged (`context.compaction_failed`) and fitting takes
over; `context.compacted` logs each one. Not yet in nisse-dev's helper over; `context.compacted` logs each one. Not in nisse-dev's helper loop,
loop. on purpose (operator, 2026-10-18): its token cap (`delegation.max_tokens`,
200k, every call's whole prompt counted) ends a delegation at a prompt of
about 36k tokens, well short of `compact_at` on any window in use. Wire
`Compactor` in if that cap is raised several-fold.
- **L6 fitting order:** first shorten old tool results to their first and last - **L6 fitting order:** first shorten old tool results to their first and last
lines, then drop the oldest events with a note saying so; never the system lines, then drop the oldest events with a note saying so; never the system
prompt, the current plan or the most recent turns. View labels: prompt, the current plan or the most recent turns. View labels:
+42 -1
View File
@@ -1,9 +1,12 @@
"""The trust dial's hard wall, on a real Gitea (D4, D5; M3.3): with the """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 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; 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.""" 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 base64
import re
import time
import uuid import uuid
from pathlib import Path from pathlib import Path
@@ -15,6 +18,9 @@ from nisse_dev.config import AgentConfig
pytestmark = pytest.mark.integration 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: def add_file(gitea: GiteaClient, repo: str, branch: str, path: str) -> None:
gitea._request( gitea._request(
@@ -34,6 +40,34 @@ def approve(operator: GiteaClient, repo: str, pr: int) -> None:
) )
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 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( 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:
@@ -64,12 +98,19 @@ def test_merging_needs_the_operators_approval_of_the_pr_as_it_is(
name, head="m-2", base="main", title="b", body="" name, head="m-2", base="main", title="b", body=""
) )
approve(operator, name, second.number) 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 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 after = gitea.pull_request(name, second.number).head_sha
assert after != approved
assert not [ assert not [
a a
for a in gitea.approvals(name, second.number) for a in gitea.approvals(name, second.number)
if a.commit == after and not a.stale 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): with pytest.raises(GiteaError):
gitea.merge_pull_request(name, second.number) gitea.merge_pull_request(name, second.number)