Files
nisse/nisse-dev/tests/test_loop_queue.py
T
dongarandClaude Opus 5.5 cceb30c729
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 3m26s
The queue: parking, the operator's order, blocked items (M2.2)
Several work items on the new loop (L1, D4):
- Switching parks the item on the workspace: its work is committed and its
  branch pushed; resuming checks the item's branch back out. Branches of
  open items are never set aside by a fresh workspace.
- Next item: the operator's "do #N next" first, otherwise the oldest.
- Issues with open Gitea dependencies are blocked until they close, checked
  at intake and on every round; status shows blocked items and the line.

Tests: queue unit tests, the property walk over several issues with
dependencies and a new parked_work_safe property (it catches parking
disabled), and an end-to-end test with two issues where B depends on A.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
2026-10-04 13:34:09 +00:00

193 lines
7.3 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 committed and pushed.
b_branch = loop_of(item(world, 15)).branch
pushed = git(
"--git-dir", str(world["bare"]), "show", f"refs/heads/{b_branch}:lines.py"
)
assert pushed == "L = 1"
# 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"
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()