Files
nisse/nisse-dev/tests/test_loop_conversations.py
T
dongarandClaude Opus 5.5 e351d0a74e
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m37s
CI / e2e (pull_request) Successful in 4m5s
nisse-core: group modules into packages by architecture section
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
2026-10-13 21:16:09 +00:00

240 lines
9.0 KiB
Python

"""Conversations on the new loop (M2.5, PR A): the `general` scope, a
thread per conversation, idle close and continuation (L1, L5), and agreed
work filed as a record issue whose item starts as agreed (D1, L8). Real git
on the throwaway build host; a fake Gitea."""
import re
from typing import Any
from test_loop_milestones import DON, Forge, ScriptedModel, call, loop_agent
from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture
from nisse_core.gitea import Issue
from nisse_core.log.canonical import Fact, ToolResult
from nisse_dev.loop.agent import FILE_ISSUE
from nisse_dev.loop.items import CHAT, GO_AHEAD, MERGE, WAITING, loop_of
PROPOSAL = call(
"propose_work",
project="sandbox",
title="Add count_lines",
summary="Add count_lines(text) to text.py, counting newline-separated lines, "
"with tests. First step: the function and its tests.",
)
def conversations(world: dict[str, Any]) -> list[Any]:
return [i for i in world["store"].work_items() if i.scope == "general"]
def facts(world: dict[str, Any], item: Any) -> list[str]:
return [
e.event.text
for e in world["store"].transcript(item.id, "orchestrator").read()
if isinstance(e.event, Fact)
]
def quiet(world: dict[str, Any]) -> None:
"""No assigned issue: only conversations in these tests."""
world["forge"].issues.clear()
def test_a_top_level_message_is_a_conversation_in_its_own_thread(
world: dict[str, Any],
) -> None:
quiet(world)
model = ScriptedModel(
[call("reply", text="A 4090 runs a 27B model at 4-bit comfortably.")],
[call("reply", text="Yes, with a smaller context.")],
)
agent, transport = loop_agent(world, model)
asked = transport.receive(DON, "Could a 4090 run a 27B model?")
[chat] = conversations(world)
loop = loop_of(chat)
assert loop.thread == asked # the thread is under the operator's message
assert (chat.position, loop.wait) == (WAITING, CHAT)
reply = transport.outbound[-1]
assert reply.thread == asked and "4-bit" in reply.text
assert 'conversation "Could a 4090 run a 27B model?": talking' in agent.status()
transport.receive(DON, "And a 32B?", thread=asked)
assert len(conversations(world)) == 1 # the same conversation
assert transport.outbound[-1].text == "Yes, with a smaller context."
def test_agreed_work_is_filed_and_starts_as_agreed(world: dict[str, Any]) -> None:
quiet(world)
forge: Forge = world["forge"]
model = ScriptedModel(
[PROPOSAL],
[ # the filed item's first run: straight to work, no restatement
call("start_milestone", name="count_lines"),
call("write_file", path="lines.py", content="L = 1\n"),
call("finish_milestone", summary="count_lines", final="yes"),
],
)
_, transport = loop_agent(world, model)
root = transport.receive(DON, "I'd like a line counter in the sandbox.")
assert transport.outbound[-1].text.startswith(
'rev-1: I\'d open an issue in don/sandbox, "Add count_lines"'
)
transport.receive(DON, "go", thread=root)
[filed] = forge.issues
marker = re.search(r"<!-- nisse-item: ([\w-]+) -->", filed.body)
assert (
marker and "count newline-separated lines" not in filed.body
) # it's the summary
assert "counting newline-separated lines" in filed.body
assert forge.assigned == [filed.number]
work = world["store"].work_item(marker.group(1))
assert work is not None and work.scope == "sandbox"
assert loop_of(work).issue == filed.number and loop_of(work).started
assert (work.position, loop_of(work).wait) == (WAITING, MERGE) # it worked
[pr] = forge.prs
assert f"Closes #{filed.number}" in forge.prs[pr]["body"]
# The conversation says where the work went and goes on.
[chat] = conversations(world)
assert loop_of(chat).became == (f"don/sandbox#{filed.number}",)
assert loop_of(chat).wait == CHAT
assert any("filed from the conversation" in f for f in facts(world, work))
def test_intake_never_takes_a_record_issue(world: dict[str, Any]) -> None:
quiet(world)
forge: Forge = world["forge"]
body = "brief\n\n<!-- nisse-item: sandbox-cabc123 -->"
forge.issues.append(Issue(30, "a record", body, "nisse-dev", ()))
agent, _ = loop_agent(world, ScriptedModel())
agent.intake()
assert world["store"].work_items() == []
def test_a_proposal_for_an_unknown_project_is_sent_back(world: dict[str, Any]) -> None:
quiet(world)
model = ScriptedModel(
[call("propose_work", project="nope", title="t", summary="s.")],
[call("reply", text="Which project should it go in?")],
)
_, transport = loop_agent(world, model)
transport.receive(DON, "Let's build a thing")
results = [
e.event.result
for e in world["store"].transcript(conversations(world)[0].id, "driver").read()
if isinstance(e.event, ToolResult)
]
assert any("no project 'nope': the projects are sandbox" in r for r in results)
assert loop_of(conversations(world)[0]).wait == CHAT
def test_changes_to_a_proposal_go_back_to_the_driver(world: dict[str, Any]) -> None:
quiet(world)
model = ScriptedModel([PROPOSAL], [PROPOSAL])
_, transport = loop_agent(world, model)
root = transport.receive(DON, "a line counter please")
transport.receive(DON, "call it line_count instead", thread=root)
loop = loop_of(conversations(world)[0])
assert (loop.wait, loop.rev) == (GO_AHEAD, 2)
assert world["forge"].issues == []
def test_an_idle_conversation_closes_and_a_reply_continues_it(
world: dict[str, Any],
) -> None:
quiet(world)
model = ScriptedModel(
[call("reply", text="Sure.")], [call("reply", text="Picking up again.")]
)
agent, transport = loop_agent(world, model)
clock = [1000.0]
agent._clock = lambda: clock[0]
agent._idle = 3600
root = transport.receive(DON, "thinking about a backup tool")
clock[0] += 3599
agent.intake()
assert conversations(world)[0].status == "open"
clock[0] += 1
agent.intake()
[old] = conversations(world)
assert (old.status, old.outcome) == ("closed", "idle")
transport.receive(DON, "back to that backup tool", thread=root)
new = [c for c in conversations(world) if c.status == "open"]
assert len(new) == 1 and loop_of(new[0]).follows == old.id
assert loop_of(new[0]).thread == root
assert any("this continues the conversation" in f for f in facts(world, new[0]))
def test_a_plain_yes_files_the_work(world: dict[str, Any]) -> None:
quiet(world)
class Reader:
def agrees(self, proposal: str, reply: str, model: str = "") -> bool:
return reply == "sounds right"
model = ScriptedModel([PROPOSAL], [call("ask_human", question="anything else?")])
agent, transport = loop_agent(world, model)
agent._reader = Reader()
root = transport.receive(DON, "a line counter please")
transport.receive(DON, "sounds right", thread=root)
assert len(world["forge"].issues) == 1
def test_a_filing_cut_off_by_a_crash_is_settled_at_boot(world: dict[str, Any]) -> None:
quiet(world)
forge: Forge = world["forge"]
model = ScriptedModel([PROPOSAL])
_, transport = loop_agent(world, model)
root = transport.receive(DON, "a line counter please")
chat = conversations(world)[0]
store = world["store"]
# The issue was opened, then the agent died before its item existed.
intent = store.begin_intent(
FILE_ISSUE,
{"repo": "don/sandbox", "item": "sandbox-cdead01", "title": "Add count_lines"},
cause="filing",
work_item=chat.id,
)
forge.issues.append(
Issue(
40, "Add count_lines", "b\n\n<!-- nisse-item: sandbox-cdead01 -->", "x", ()
)
)
agent2, _ = loop_agent(world, ScriptedModel([call("ask_human", question="ok?")]))
agent2.boot()
work = store.work_item("sandbox-cdead01")
assert work is not None and loop_of(work).issue == 40 and loop_of(work).started
assert not [i for i in store.pending_intents() if i.id == intent]
assert root # the conversation itself is untouched
def test_a_filing_that_never_reached_gitea_is_put_up_again(
world: dict[str, Any],
) -> None:
quiet(world)
_, transport = loop_agent(world, ScriptedModel([PROPOSAL]))
transport.receive(DON, "a line counter please")
chat = conversations(world)[0]
world["store"].begin_intent(
FILE_ISSUE,
{"repo": "don/sandbox", "item": "sandbox-cnever1", "title": "Add count_lines"},
cause="filing",
work_item=chat.id,
)
agent2, _ = loop_agent(world, ScriptedModel())
agent2.boot()
loop = loop_of(world["store"].work_item(chat.id))
assert loop.wait == GO_AHEAD and "cut off by a restart" in loop.note
assert world["store"].work_item("sandbox-cnever1") is None
def test_commands_alone_at_top_level_are_not_conversations(
world: dict[str, Any],
) -> None:
quiet(world)
_, transport = loop_agent(world, ScriptedModel())
transport.receive(DON, "status")
transport.receive(DON, "go")
assert conversations(world) == []