Answer where asked, wait on a merge, and let the driver title PRs
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m17s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m17s
From the new loop's first live run (sandbox #255, PR #256): - A question asked on the PR is answered on the PR (an inline one quoted with its file and line, since the API can't reply in its thread), with a pointer in the item's Matrix thread; one asked in Matrix stays there. - wait_for_review: when a review needs no change, or only the operator's merge is left, the item waits on the merge instead of on an answer. - finish_milestone takes the PR's title from the driver. The e2e test's merge waits out Gitea's brief 'try again later' after a push. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
bf416cad09
commit
651551aab4
6 files changed
+198
-13
No files matched your search
@@ -80,7 +80,13 @@ class Forge:
|
||||
|
||||
def open_pull_request(self, repo: str, *, head, base, title, body) -> PullRequest:
|
||||
n = 100 + len(self.prs)
|
||||
self.prs[n] = {"head": head, "state": "open", "merged": False, "body": body}
|
||||
self.prs[n] = {
|
||||
"head": head,
|
||||
"state": "open",
|
||||
"merged": False,
|
||||
"body": body,
|
||||
"title": title,
|
||||
}
|
||||
return self._pr(n)
|
||||
|
||||
def pull_request(self, repo: str, index: int) -> PullRequest:
|
||||
@@ -476,3 +482,89 @@ def test_abandoning_an_item_closes_its_open_pr(world: dict[str, Any]) -> None:
|
||||
assert item(world).outcome == "abandoned"
|
||||
assert forge.prs[pr]["state"] == "closed"
|
||||
assert (pr, "abandoned: abandoned by @don:test") in forge.posted
|
||||
|
||||
|
||||
# --- answering where asked (D5), waiting for review, PR titles -----------------
|
||||
|
||||
|
||||
def test_a_question_on_the_pr_is_answered_on_the_pr(world: dict[str, Any]) -> None:
|
||||
forge: Forge = world["forge"]
|
||||
model = ScriptedModel(
|
||||
RESTATE,
|
||||
WORK + FINISH,
|
||||
[call("ask_human", question="bool is a subclass of int; keep the check?")],
|
||||
)
|
||||
agent, transport = loop_agent(world, model)
|
||||
agent.intake()
|
||||
say(transport, world, "go")
|
||||
[pr] = forge.prs
|
||||
forge.notes[pr] = [ReviewNote("line-9", "don", "Is bool an int?", "text.py:9")]
|
||||
agent.watch()
|
||||
agent._schedule()
|
||||
number, body = forge.posted[-1]
|
||||
assert number == pr
|
||||
assert body.startswith("> on `text.py:9`:\n> Is bool an int?\n\n")
|
||||
assert body.endswith("keep the check?")
|
||||
# Matrix gets a pointer, not the conversation.
|
||||
assert (
|
||||
transport.outbound[-1].text
|
||||
== f"I answered your comment on PR #{pr}: https://g/pr/{pr}"
|
||||
)
|
||||
assert transport.outbound[-1].thread == loop_of(item(world)).thread
|
||||
|
||||
|
||||
def test_after_a_matrix_reply_questions_go_back_to_matrix(
|
||||
world: dict[str, Any],
|
||||
) -> None:
|
||||
forge: Forge = world["forge"]
|
||||
model = ScriptedModel(
|
||||
RESTATE,
|
||||
WORK + FINISH,
|
||||
[call("ask_human", question="first?")],
|
||||
[call("ask_human", question="second?")],
|
||||
)
|
||||
agent, transport = loop_agent(world, model)
|
||||
agent.intake()
|
||||
say(transport, world, "go")
|
||||
[pr] = forge.prs
|
||||
forge.comments[pr] = [Comment(3, "don", "why this?", "t")]
|
||||
agent.watch()
|
||||
agent._schedule()
|
||||
posted_on_pr = len(forge.posted)
|
||||
say(transport, world, "answering in Matrix") # the operator moved
|
||||
assert len(forge.posted) == posted_on_pr
|
||||
assert transport.outbound[-1].text == "second?"
|
||||
|
||||
|
||||
def test_wait_for_review_waits_on_the_merge_then_closes(world: dict[str, Any]) -> None:
|
||||
forge: Forge = world["forge"]
|
||||
model = ScriptedModel(
|
||||
RESTATE,
|
||||
WORK + FINISH,
|
||||
[call("wait_for_review", note="no change needed; ready to merge")],
|
||||
[call("close_item", summary="merged")],
|
||||
)
|
||||
agent, transport = loop_agent(world, model)
|
||||
agent.intake()
|
||||
say(transport, world, "go")
|
||||
[pr] = forge.prs
|
||||
forge.comments[pr] = [Comment(4, "don", "looks fine, merge it?", "t")]
|
||||
agent.watch()
|
||||
agent._schedule()
|
||||
loop = loop_of(item(world))
|
||||
assert (loop.wait, loop.note) == (MERGE, f"PR #{pr}")
|
||||
assert forge.posted[-1][1].endswith("no change needed; ready to merge")
|
||||
forge.merge(pr)
|
||||
agent.watch()
|
||||
agent._schedule()
|
||||
assert item(world).status == "closed"
|
||||
|
||||
|
||||
def test_the_driver_titles_its_pr(world: dict[str, Any]) -> None:
|
||||
forge: Forge = world["forge"]
|
||||
finish = [call("finish_milestone", summary="s", title="Add count_words")]
|
||||
agent, transport = loop_agent(world, ScriptedModel(RESTATE, WORK + finish))
|
||||
agent.intake()
|
||||
say(transport, world, "go")
|
||||
[pr] = forge.prs
|
||||
assert forge.prs[pr]["title"] == "Add count_words"
|
||||
@@ -53,6 +53,8 @@ class PolicyModel:
|
||||
return [ToolCall(call_id=f"p{self.n}", tool="nonsense", arguments={})]
|
||||
if "finish_milestone" not in offered:
|
||||
return [call("restate", text=f"restatement {self.n}")]
|
||||
if "wait_for_review" in offered and self.rng.random() < 0.2:
|
||||
return [call("wait_for_review", note=f"waiting {self.n}")]
|
||||
merged = any("merged" in t for t in facts[-6:])
|
||||
if merged and self.rng.random() < 0.8:
|
||||
return [call("close_item", summary=f"done {self.n}")]
|
||||
|
||||
Reference in new issue
Block a user