"""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.conversation.transport import InboundMessage, InMemoryTransport from nisse_core.gitea import Comment, Issue 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"]