Files
nisse/nisse-dev/tests/test_loop_queue.py
T
dongarandClaude Opus 5.5 cd9ab2559e
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 3m45s
Multi-milestone work and documents on the new loop (M2.4)
- Every milestone branch is cut from the latest default branch: the next
  milestone builds on the merged previous one, and a redo after a closed PR
  drops the rejected work. Uncommitted work comes along.
- While the item's PR is open, start_milestone is refused, with the PR's
  state read from Gitea at that moment, not the last watch round.
- finish_milestone(final): earlier PRs say "Part of #N", the last "Closes
  #N"; unsaid, it's final, as before.
- close_item with the item's PR still open waits for the merge (L2: a
  closed item leaves nothing open).
- The driver's instructions cover ARCHITECTURE.md and build plans under
  docs/plans/ (D2); the harness never parses a plan.
- Parking stashes uncommitted work and pushes it to <item>-parked, put back
  on resume, instead of committing it onto the item's branch. The walk
  found a half-done review fix parked onto a PR that had merged meanwhile,
  where it was lost. Architecture D4's parking line says so.

Tests: unit tests for fresh milestone branches, the merge wait (including
a merge not yet watched), redo after a closed PR, Part of/Closes, close
waiting on an open PR, and the parked-fix regression; the walk's new
milestones_start_fresh property (fork-from-current fails 9 seeds); an e2e
two-milestone feature.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-05 20:57:37 +00:00

246 lines
9.6 KiB
Python

"""The queue in the new loop (M2.2): several items, parking and resuming on
switch, the operator's order, and items blocked by Gitea dependencies (L1,
D4). Real git on the throwaway build host; a fake Gitea."""
from typing import Any
from test_loop_milestones import DON, Forge, ScriptedModel, call, git, loop_agent
from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture
from nisse_dev.gitea import Comment, Issue
from nisse_dev.loop.items import (
ANSWER,
BLOCKED,
GO_AHEAD,
MERGE,
READY,
WAITING,
item_for_issue,
loop_of,
)
from nisse_dev.transport import InboundMessage, InMemoryTransport
def two_issues(forge: Forge) -> None:
forge.issues.append(Issue(15, "Add count_lines", "count lines", "don", ()))
def item(world: dict[str, Any], issue: int) -> Any:
found = item_for_issue(world["store"], issue)
assert found is not None
return found
def say(
transport: InMemoryTransport, world: dict[str, Any], issue: int, text: str
) -> None:
transport.receive(DON, text, thread=loop_of(item(world, issue)).thread)
def restated(transport: InMemoryTransport) -> list[str]:
"""The threads restatements went up in, in order."""
return [m.thread for m in transport.outbound if m.text.startswith("rev-1:")]
def test_switching_parks_the_work_and_resuming_finds_it(
world: dict[str, Any],
) -> None:
forge: Forge = world["forge"]
two_issues(forge)
model = ScriptedModel(
[call("restate", text="count_words")], # 14
[call("restate", text="count_lines")], # 15
[ # 14, after its go: a PR
call("start_milestone", name="count_words"),
call("write_file", path="words.py", content="W = 1\n"),
call("finish_milestone", summary="count_words"),
],
[ # 15, after its go: half done, then a question
call("start_milestone", name="count_lines"),
call("write_file", path="lines.py", content="L = 1\n"),
call("ask_human", question="one per line, or per paragraph?"),
],
[ # 14, a review comment
call("write_file", path="words.py", content="W = 2\n"),
call("finish_milestone", summary="review fix"),
],
[ # 15, answered: back where it was
call("run_command", command="cat lines.py"),
call("finish_milestone", summary="count_lines"),
],
)
agent, transport = loop_agent(world, model)
agent.intake()
assert [loop_of(item(world, n)).wait for n in (14, 15)] == [GO_AHEAD, GO_AHEAD]
# The second item is acknowledged with its place in line.
assert any("#15" in m.text and "1 ahead" in m.text for m in transport.outbound)
say(transport, world, 14, "go")
assert loop_of(item(world, 14)).wait == MERGE
a_branch = loop_of(item(world, 14)).branch
say(transport, world, 15, "go")
assert loop_of(item(world, 15)).wait == ANSWER
# 15 was prepared while 14's branch was on the workspace: kept, not set aside.
assert not any("Set aside" in m.text for m in transport.outbound)
assert a_branch in world["repo"].local_branches()
[pr] = forge.prs
forge.comments[pr] = [Comment(7, "don", "make W 2", "t")]
agent.watch()
agent._schedule()
# Switching to 14 parked 15: its half-done work is pushed as a stash to
# a side branch (new files in its third parent), never onto 15's branch.
b_branch = loop_of(item(world, 15)).branch
bare = ["--git-dir", str(world["bare"])]
parked = f"refs/heads/{item(world, 15).id}-parked"
assert git(*bare, "show", f"{parked}^3:lines.py") == "L = 1"
# The branch has no commits of its own, so it wasn't pushed at all.
assert b_branch not in git(*bare, "branch", "--list")
# 14's fix went to its own branch, without 15's work.
assert loop_of(item(world, 14)).wait == MERGE
files = git("--git-dir", str(world["bare"]), "ls-tree", "--name-only", a_branch)
assert "words.py" in files.split() and "lines.py" not in files.split()
assert "W = 2" in git(
"--git-dir", str(world["bare"]), "show", f"refs/heads/{a_branch}:words.py"
)
say(transport, world, 15, "one per line")
assert world["repo"].current_branch() == b_branch
assert (world["repo"].root / "lines.py").read_text() == "L = 1\n"
# Put back, so the side branch is gone.
assert item(world, 15).id + "-parked" not in git(*bare, "branch", "--list")
assert loop_of(item(world, 15)).wait == MERGE
assert len(forge.prs) == 2
def test_a_blocked_item_waits_for_its_dependency_to_close(
world: dict[str, Any],
) -> None:
forge: Forge = world["forge"]
two_issues(forge)
forge.deps[15] = [14]
model = ScriptedModel(
[call("restate", text="count_words")],
[call("restate", text="count_lines")],
)
agent, transport = loop_agent(world, model)
agent.intake()
assert item(world, 15).position == BLOCKED
assert loop_of(item(world, 15)).blocked_by == ("don/sandbox#14",)
assert any(
"#15" in m.text and "blocked by don/sandbox#14" in m.text
for m in transport.outbound
)
assert "#15 Add count_lines: blocked by don/sandbox#14" in agent.status()
# Talking to it doesn't start it.
say(transport, world, 15, "go")
assert item(world, 15).position == BLOCKED
assert "blocked" in transport.outbound[-1].text
forge.closed_issues.append(14)
agent.intake()
assert (item(world, 15).position, loop_of(item(world, 15)).wait) == (
WAITING,
GO_AHEAD,
)
assert loop_of(item(world, 15)).blocked_by == ()
def test_an_item_that_gains_a_dependency_is_blocked(world: dict[str, Any]) -> None:
forge: Forge = world["forge"]
two_issues(forge)
model = ScriptedModel([call("restate", text="count_words")])
agent, _ = loop_agent(world, model)
agent._running = True # hold the queue: both stay ready
agent.intake()
assert item(world, 15).position == READY
forge.deps[15] = [14]
agent.check_blocked()
assert item(world, 15).position == BLOCKED
agent._running = False
agent._schedule() # 14 restates; 15 stays blocked
assert item(world, 15).position == BLOCKED
assert loop_of(item(world, 14)).wait == GO_AHEAD
def test_the_operators_order_comes_before_the_oldest(world: dict[str, Any]) -> None:
forge: Forge = world["forge"]
two_issues(forge)
forge.issues.append(Issue(16, "Add count_chars", "count chars", "don", ()))
forge.issues.append(Issue(17, "Add count_vowels", "count vowels", "don", ()))
for n in (15, 16, 17):
forge.deps[n] = [14]
model = ScriptedModel(*[[call("restate", text=f"r{n}")] for n in range(4)])
agent, transport = loop_agent(world, model)
agent.intake() # 14 runs; the rest wait on it
transport.receive(DON, "Do #17 next.")
assert "#17 goes first once it's unblocked" in transport.outbound[-1].text
transport.receive(DON, "do #99 next")
assert "#99 isn't one of my open work items" in transport.outbound[-1].text
forge.closed_issues.append(14)
agent.intake()
threads = {loop_of(item(world, n)).thread: n for n in (14, 15, 16, 17)}
assert [threads[t] for t in restated(transport)] == [14, 17, 15, 16]
def test_status_shows_the_line(world: dict[str, Any]) -> None:
forge: Forge = world["forge"]
two_issues(forge)
agent, _ = loop_agent(world, ScriptedModel())
agent._running = True # hold the queue
agent.intake()
assert "#14 Add count_words: ready, 1 of 2 in line" in agent.status()
assert "#15 Add count_lines: ready, 2 of 2 in line" in agent.status()
agent.handle(InboundMessage(DON, "do #15 next"), None)
assert "#15 Add count_lines: ready, 1 of 2 in line" in agent.status()
def test_a_fix_half_done_when_its_pr_merges_survives_a_switch(
world: dict[str, Any],
) -> None:
"""Found by the walk: work on a review fix, parked after the PR merged,
was pushed onto the merged branch, where nobody would see it. Now it
comes back with the item and goes out in its next PR."""
forge: Forge = world["forge"]
two_issues(forge)
model = ScriptedModel(
[call("restate", text="count_words")], # 14
[call("restate", text="count_lines")], # 15
[ # 14: a PR
call("start_milestone", name="words"),
call("write_file", path="words.py", content="W = 1\n"),
call("finish_milestone", summary="words", final="yes"),
],
[ # 14, a review comment: half a fix, then a question
call("write_file", path="fix.py", content="F = 1\n"),
call("ask_human", question="fix it in words.py too?"),
],
[ # 15 runs meanwhile
call("start_milestone", name="lines"),
call("write_file", path="lines.py", content="L = 1\n"),
call("finish_milestone", summary="lines"),
],
[call("finish_milestone", summary="the fix", final="yes")], # 14 again
)
agent, transport = loop_agent(world, model)
agent.intake()
say(transport, world, 14, "go")
[pr] = forge.prs
forge.comments[pr] = [Comment(7, "don", "add fix.py", "t")]
agent.watch()
agent._schedule()
assert loop_of(item(world, 14)).wait == ANSWER
say(transport, world, 15, "go") # parks 14's half-done fix
forge.merge(pr)
agent.watch()
agent._schedule() # 14 resumes; its fix goes out as a new PR
bare = ["--git-dir", str(world["bare"])]
merged_head = forge.prs[pr]["head"]
assert "fix.py" not in git(*bare, "ls-tree", "--name-only", merged_head).split()
newest = max(
n for n, p in forge.prs.items() if p["head"].startswith(item(world, 14).id)
)
assert newest != pr and forge.files(newest) == ["fix.py"]