"""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()