A pure move; behaviour unchanged. - models (L9, L7): llm, catalog, probe, render, render_anthropic, context - log (L7, L6): canonical, views - state (L8): store (was state), reconcile, worker, cancel, dump - knowledge (L10, L6.4): vault, memory - conversation (L5): transport, matrix - execution (L13): ssh (was execution), testing - authority (P3, P4, L4): actions - gitea, oplog, envfile stay at the top until each gains a second module. Every import, monkeypatch target, script and doc reference rewritten. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
219 lines
7.2 KiB
Python
219 lines
7.2 KiB
Python
"""Multi-milestone work on the new loop (M2.4): each milestone starts from
|
|
the latest default branch, the next waits for the previous PR's merge (read
|
|
from Gitea when asked), earlier PRs are part of the issue and the last closes
|
|
it, and the item closes only with nothing left open (D1, D2, D4). Real git on
|
|
the throwaway build host; a fake Gitea."""
|
|
|
|
from typing import Any
|
|
|
|
from test_loop_milestones import (
|
|
RESTATE,
|
|
Forge,
|
|
ScriptedModel,
|
|
call,
|
|
git,
|
|
item,
|
|
loop_agent,
|
|
say,
|
|
)
|
|
from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture
|
|
|
|
from nisse_core.gitea import Issue
|
|
from nisse_core.log.canonical import ToolCall, ToolResult
|
|
from nisse_dev.loop.items import MERGE, WAITING, loop_of
|
|
|
|
|
|
def milestone(path: str, final: str) -> list[Any]:
|
|
return [
|
|
call("start_milestone", name=path),
|
|
call("write_file", path=path, content=f"# {path}\n"),
|
|
call("finish_milestone", summary=f"adds {path}", title=path, final=final),
|
|
]
|
|
|
|
|
|
def tree(world: dict[str, Any], branch: str) -> list[str]:
|
|
bare = str(world["bare"])
|
|
return git("--git-dir", bare, "ls-tree", "--name-only", branch).split()
|
|
|
|
|
|
def result_of(world: dict[str, Any], tool: str) -> str:
|
|
events = world["store"].transcript(item(world).id, "driver").read()
|
|
ids = {
|
|
e.event.call_id
|
|
for e in events
|
|
if isinstance(e.event, ToolCall) and e.event.tool == tool
|
|
}
|
|
results = [
|
|
e.event.result
|
|
for e in events
|
|
if isinstance(e.event, ToolResult) and e.event.call_id in ids
|
|
]
|
|
return results[-1]
|
|
|
|
|
|
def test_the_second_milestone_builds_on_the_merged_first(world: dict[str, Any]) -> None:
|
|
forge: Forge = world["forge"]
|
|
model = ScriptedModel(
|
|
RESTATE,
|
|
milestone("a.py", final="no"),
|
|
milestone("b.py", final="yes"),
|
|
[call("close_item", summary="both done")],
|
|
)
|
|
agent, transport = loop_agent(world, model)
|
|
agent.intake()
|
|
say(transport, world, "go")
|
|
[first] = forge.prs
|
|
assert forge.prs[first]["body"].endswith("Part of #14")
|
|
forge.merge(first)
|
|
agent.watch()
|
|
agent._schedule()
|
|
|
|
second = max(forge.prs)
|
|
assert second != first
|
|
assert "Closes #14" in forge.prs[second]["body"]
|
|
assert forge.files(second) == ["b.py"] # only its own work
|
|
head = forge.prs[second]["head"]
|
|
assert "a.py" in tree(world, head) # cut from main, with the first merged
|
|
assert loop_of(item(world)).milestone == 2
|
|
|
|
forge.merge(second)
|
|
agent.watch()
|
|
agent._schedule()
|
|
assert item(world).status == "closed"
|
|
|
|
|
|
def test_the_next_milestone_waits_for_the_merge(world: dict[str, Any]) -> None:
|
|
forge: Forge = world["forge"]
|
|
model = ScriptedModel(
|
|
RESTATE,
|
|
milestone("a.py", final="no"),
|
|
[ # a message arrives while the PR is open: try to move on
|
|
call("start_milestone", name="b"),
|
|
call("wait_for_review", note="waiting for the merge"),
|
|
],
|
|
)
|
|
agent, transport = loop_agent(world, model)
|
|
agent.intake()
|
|
say(transport, world, "go")
|
|
say(transport, world, "carry on")
|
|
assert "hasn't merged yet" in result_of(world, "start_milestone")
|
|
assert (item(world).position, loop_of(item(world)).wait) == (WAITING, MERGE)
|
|
assert len(forge.prs) == 1
|
|
|
|
|
|
def test_a_merge_not_yet_watched_is_read_from_gitea(world: dict[str, Any]) -> None:
|
|
forge: Forge = world["forge"]
|
|
model = ScriptedModel(
|
|
RESTATE,
|
|
milestone("a.py", final="no"),
|
|
milestone("b.py", final="yes"),
|
|
)
|
|
agent, transport = loop_agent(world, model)
|
|
agent.intake()
|
|
say(transport, world, "go")
|
|
[first] = forge.prs
|
|
forge.merge(first) # merged in Gitea; no watch round has seen it
|
|
say(transport, world, "merged, carry on")
|
|
assert len(forge.prs) == 2
|
|
assert "a.py" in tree(world, forge.prs[max(forge.prs)]["head"])
|
|
|
|
|
|
def test_a_redo_after_a_closed_pr_drops_the_rejected_work(
|
|
world: dict[str, Any],
|
|
) -> None:
|
|
forge: Forge = world["forge"]
|
|
model = ScriptedModel(
|
|
RESTATE,
|
|
milestone("a.py", final="yes"),
|
|
milestone("c.py", final="yes"),
|
|
)
|
|
agent, transport = loop_agent(world, model)
|
|
agent.intake()
|
|
say(transport, world, "go")
|
|
[first] = forge.prs
|
|
forge.prs[first]["state"] = "closed" # rejected
|
|
agent.watch()
|
|
agent._schedule()
|
|
redo = max(forge.prs)
|
|
assert forge.files(redo) == ["c.py"]
|
|
assert "a.py" not in tree(world, forge.prs[redo]["head"])
|
|
|
|
|
|
def test_closing_with_a_pr_open_waits_for_the_merge(world: dict[str, Any]) -> None:
|
|
forge: Forge = world["forge"]
|
|
model = ScriptedModel(
|
|
RESTATE,
|
|
milestone("a.py", final="yes"),
|
|
[call("close_item", summary="done, I think")], # too soon
|
|
[call("close_item", summary="done")],
|
|
)
|
|
agent, transport = loop_agent(world, model)
|
|
agent.intake()
|
|
say(transport, world, "go")
|
|
say(transport, world, "thanks")
|
|
assert item(world).status == "open"
|
|
assert (item(world).position, loop_of(item(world)).wait) == (WAITING, MERGE)
|
|
[pr] = forge.prs
|
|
forge.merge(pr)
|
|
agent.watch()
|
|
agent._schedule()
|
|
assert item(world).status == "closed"
|
|
|
|
|
|
def test_an_unsaid_final_closes_the_issue(world: dict[str, Any]) -> None:
|
|
forge: Forge = world["forge"]
|
|
model = ScriptedModel(
|
|
RESTATE,
|
|
[
|
|
call("start_milestone", name="a"),
|
|
call("write_file", path="a.py", content="x = 1\n"),
|
|
call("finish_milestone", summary="adds a"),
|
|
],
|
|
)
|
|
agent, transport = loop_agent(world, model)
|
|
agent.intake()
|
|
say(transport, world, "go")
|
|
[pr] = forge.prs
|
|
assert "Closes #14" in forge.prs[pr]["body"]
|
|
|
|
|
|
def test_a_design_milestone_holds_documents_only(world: dict[str, Any]) -> None:
|
|
"""D1, D2: a `design` item agrees its design before any code; the
|
|
harness refuses a design milestone with code in it."""
|
|
forge: Forge = world["forge"]
|
|
forge.issues[0] = Issue(14, "Add count_words", "count words", "don", ("design",))
|
|
model = ScriptedModel(
|
|
RESTATE,
|
|
[
|
|
call("start_milestone", name="design"),
|
|
call("write_file", path="ARCHITECTURE.md", content="# arch\n"),
|
|
call("write_file", path="docs/plans/words.md", content="# plan\n"),
|
|
call("write_file", path="words.py", content="W = 1\n"), # too soon
|
|
call("finish_milestone", summary="design", final="no"),
|
|
],
|
|
[
|
|
call("run_command", command="rm words.py"),
|
|
call("finish_milestone", summary="design", final="no"),
|
|
],
|
|
milestone("words.py", final="yes"), # after the design merged
|
|
)
|
|
agent, transport = loop_agent(world, model)
|
|
agent.intake()
|
|
say(transport, world, "go")
|
|
[design] = forge.prs
|
|
assert sorted(forge.files(design)) == ["ARCHITECTURE.md", "docs/plans/words.md"]
|
|
refused = [
|
|
e.event.result
|
|
for e in world["store"].transcript(item(world).id, "driver").read()
|
|
if isinstance(e.event, ToolResult)
|
|
and "this milestone is the design" in e.event.result
|
|
]
|
|
assert refused and "words.py" in refused[0]
|
|
|
|
forge.merge(design)
|
|
agent.watch()
|
|
agent._schedule()
|
|
code = max(forge.prs)
|
|
assert code != design and forge.files(code) == ["words.py"]
|
|
assert loop_of(item(world)).design_merged
|