Fixes from the rest of the live session
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m13s
CI / e2e (pull_request) Successful in 3m22s

- A near miss for abandon ("abort", "cancel", "drop it", ...) in an item's
  thread is asked about, and changes nothing.
- The driver is told only the operator stops or closes an item, so it
  doesn't claim to have done either.
- An issue assigned again after its item closed starts a new item that
  notes the one before (Gitea's timeline gives the assignment time);
  item_for_issue returns the latest item.
- Abandoning an issue Gitea won't close because of open dependencies says
  so, naming them.
- An agreed item isn't offered restate.

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:
dongarandClaude Opus 5.5 committed 2026-10-08 02:16:51 +00:00
1 parent 613a2a7f72
commit 0064ec7696
9 files changed
+242 -14

No files matched your search

+27
View File
@@ -229,3 +229,30 @@ def test_closing_comments_first_then_closes_issue_and_pr() -> None:
("POST", "/api/v1/repos/o/r/issues/20/comments", {"body": "abandoned by don"}),
("PATCH", "/api/v1/repos/o/r/pulls/20", {"state": "closed"}),
]
def test_assigned_at_is_the_last_assignment_across_pages() -> None:
def assigned(login: str, at: str, removed: bool = False) -> dict:
return {
"type": "assignees",
"assignee": {"login": login},
"removed_assignee": removed,
"created_at": at,
}
filler = {"type": "comment", "created_at": "t0"}
pages = {
1: [assigned("dev-agent", "t1")] + [filler] * (PR_PAGE_SIZE - 1),
2: [
assigned("dev-agent", "t2", removed=True),
assigned("someone", "t3"),
assigned("dev-agent", "t4"),
],
}
def handler(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/api/v1/repos/o/r/issues/6/timeline"
return httpx.Response(200, json=pages[int(request.url.params["page"])])
assert client(handler).assigned_at("o/r", 6, "dev-agent") == "t4"
assert client(handler).assigned_at("o/r", 6, "nobody") is None
+4
View File
@@ -49,6 +49,7 @@ class FakeGitea:
self.comments: list[tuple[int, str]] = []
self.closed: list[int] = []
self.deps: dict[int, list[int]] = {}
self.reassigned: dict[int, str] = {} # issue -> when assigned again
def create_issue(self, repo: str, title: str, body: str) -> int:
n = max([i.number for i in self.issues] + [0]) + 1
@@ -67,6 +68,9 @@ class FakeGitea:
def assigned_by(self, repo: str, index: int, assignee: str) -> str:
return self.assigner
def assigned_at(self, repo: str, index: int, assignee: str) -> str | None:
return self.reassigned.get(index)
def post_comment(self, repo: str, index: int, body: str) -> None:
self.comments.append((index, body))
+86 -2
View File
@@ -1,14 +1,16 @@
"""What the first live session of the new loop found (2026-10-07), each as a
test: a request ending on the assistant's side (refused by models without
prefill: the driver's prose nudge, a helper's first request), `status` in an
item's thread taken as a reply, and no way for the driver to answer a remark.
item's thread taken as a reply, no way for the driver to answer a remark;
later, near-miss commands, the driver claiming a stop, a reassigned issue
ignored, an unexplained failed abandon and restating an agreed item.
"""
from pathlib import Path
from typing import Any
from model_contract import assert_renderable
from test_loop import RESTATED, FakeGitea, Harness, ScriptedModel, call, issue
from test_loop import DON, RESTATED, FakeGitea, Harness, ScriptedModel, call, issue
from test_loop_help import ROLES, hand_back
from test_loop_milestones import RESTATE, Forge, loop_agent, say
from test_loop_milestones import item as the_item
@@ -152,3 +154,85 @@ def test_a_review_comment_can_be_answered_without_a_fix(
assert (loop.wait, loop.pr) == (MERGE, pr)
assert forge.posted[-1][0] == pr # answered on the PR, where asked
assert "It's a constant on purpose" in forge.posted[-1][1]
# --- the session's later findings ----------------------------------------------
def test_a_near_miss_for_abandon_is_asked_about(tmp_path: Path) -> None:
model = ScriptedModel(RESTATED) # called once only: nothing reaches it
h = Harness(tmp_path, model, FakeGitea(issue()))
h.agent.intake()
h.say("abort")
assert h.posted()[-1].startswith("Did you mean `abandon`?")
assert (h.item().status, loop_of(h.item()).wait) == ("open", GO_AHEAD)
h.say("abandon")
assert h.item().status == "closed"
def test_the_driver_is_told_it_cant_stop_or_close(tmp_path: Path) -> None:
seen: list[str] = []
class Spy(ScriptedModel):
def complete(self, events, tools, model, system=None): # type: ignore[no-untyped-def]
seen.append(system or "")
return super().complete(events, tools, model, system)
h = Harness(tmp_path, Spy(RESTATED), FakeGitea(issue()))
h.agent.intake()
assert "never say you have stopped or closed anything yourself" in seen[0]
def test_an_issue_assigned_again_after_its_item_closed_is_taken_again(
tmp_path: Path,
) -> None:
gitea = FakeGitea(issue())
h = Harness(tmp_path, ScriptedModel(RESTATED, RESTATED), gitea)
h.agent.intake()
h.say("abandon")
[old] = h.store.work_items()
gitea.closed.remove(14) # the operator reopened it
h.agent.intake()
assert len(h.store.work_items()) == 1 # the same assignment: not again
gitea.reassigned[14] = "2999-01-01T00:00:00Z" # assigned anew, later
h.agent.intake()
items = h.store.work_items()
assert len(items) == 2
new = next(i for i in items if i.id != old.id)
assert new.status == "open" and loop_of(new).follows == old.id
def test_abandoning_a_blocked_issue_says_why_it_stays_open(tmp_path: Path) -> None:
from nisse_dev.gitea import GiteaError
class Strict(FakeGitea):
def close_issue(self, repo: str, index: int) -> None:
if self.open_dependencies(repo, index):
raise GiteaError("gitea PATCH returned 500: blocked")
super().close_issue(repo, index)
gitea = Strict(issue(14), issue(15, title="other"))
gitea.deps[14] = [15]
h = Harness(tmp_path, ScriptedModel(RESTATED), gitea)
h.agent._running = True # hold the queue: only the abandon matters
h.agent.intake()
item = next(i for i in h.store.work_items() if loop_of(i).issue == 14)
h.transport.receive(DON, "abandon", thread=loop_of(item).thread)
said = " ".join(h.posted())
assert "won't close an issue while it depends on open ones" in said
assert "don/sandbox#15" in said
def test_an_agreed_item_isnt_offered_restate(world: dict[str, Any]) -> None:
offered: list[set[str]] = []
class Spy(ScriptedModel):
def complete(self, events, tools, model, system=None): # type: ignore[no-untyped-def]
offered.append({t["function"]["name"] for t in tools})
return super().complete(events, tools, model, system)
model = Spy(RESTATE, [call("ask_human", question="anything else?")])
agent, transport = loop_agent(world, model)
agent.intake()
say(transport, world, "go")
assert "restate" in offered[0] and "restate" not in offered[1]
+4
View File
@@ -61,6 +61,7 @@ class Forge:
self.issues = [Issue(14, "Add count_words", "count words", "don", ())]
self.deps: dict[int, list[int]] = {} # issue -> the issues it depends on
self.assigned: list[int] = [] # issues the agent assigned to itself
self.reassigned: dict[int, str] = {} # issue -> when assigned again
# intake
def assigned_issues(self, repo: str, assignee: str) -> list[Issue]:
@@ -84,6 +85,9 @@ class Forge:
def assigned_by(self, repo: str, index: int, assignee: str) -> str:
return "don"
def assigned_at(self, repo: str, index: int, assignee: str) -> str | None:
return self.reassigned.get(index)
# PRs
def _pr(self, n: int) -> PullRequest:
p = self.prs[n]
+7 -1
View File
@@ -400,7 +400,13 @@ class Walk:
where = f"seed {self.seed}, steps {self.steps}"
store = self.world["store"]
items = self.items()
issues = [(i.scope, loop_of(i).issue) for i in items if loop_of(i).issue]
# Open items: an issue assigned again after its item closed gets a new
# one (D1), which loop.qnt doesn't model.
issues = [
(i.scope, loop_of(i).issue)
for i in items
if loop_of(i).issue and i.status == "open"
]
assert len(issues) == len(set(issues)), f"one_item_per_issue: {where}"
for item in items:
loop = loop_of(item)