Files
nisse/nisse-dev/tests/test_loop_queue.py
T
dongarandClaude Opus 5.5 154a0c3fa8
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m42s
CI / e2e (pull_request) Successful in 4m0s
M7.1: extract nisse-core
What isn't the dev agent's moves to nisse-core, unchanged in behaviour,
with its tests: canonical, render, render_anthropic, llm, catalog, probe,
context, views, state, reconcile, worker, cancel, dump, transport, matrix,
execution, testing, vault, memory, oplog, envfile and gitea (whole).

- nisse_core.state.StateStore holds the tables every agent has; an
  agent's subclass adds its tables (version, migrations), roles and
  views. nisse_dev.state.StateStore adds the build VM blocks (v5).
- views.project takes what each role is shown, rather than a fixed table.
- Vault takes the agent's name (agent=), for provenance.
- A test that nisse_core never imports an agent.
- The loop, config, git, delegation, expert, tools and evals stay in
  nisse-dev until nisse-ops has a loop to share.

Docs: the Development Plan marks M7.1 done and notes that Phase 6 waits on
the agent host and Terraform; Architecture Part L names the package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-13 18:37:01 +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_core.gitea import Comment, Issue
from nisse_core.transport import InboundMessage, InMemoryTransport
from nisse_dev.loop.items import (
ANSWER,
BLOCKED,
GO_AHEAD,
MERGE,
READY,
WAITING,
item_for_issue,
loop_of,
)
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"]