Files
nisse/nisse-core/tests/test_views.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

154 lines
5.5 KiB
Python

"""Per-role transcript views (architecture L6; Development Plan M1.2)."""
import io
from collections.abc import Mapping
from pathlib import Path
from nisse_core.log.canonical import (
AssistantText,
DelegationCompleted,
DelegationStarted,
Fact,
PlanSubmitted,
StoredEvent,
ToolCall,
ToolResult,
UserMessage,
)
from nisse_core.models.render import OpenAIChatRenderer
from nisse_core.models.render_anthropic import AnthropicMessagesRenderer
from nisse_core.oplog import OpLog
from nisse_core.state.store import StateStore
class Store(StateStore):
"""An agent's store: the helper is shown the driver's work as narration,
the driver only the helper's report."""
SHOWN: Mapping[str, Mapping[str, str]] = {
"driver": {"helper": "report"},
"helper": {"driver": "narrate"},
}
def new_store(tmp_path: Path) -> StateStore:
return Store(tmp_path / "state.db", oplog=OpLog("nisse-dev", io.StringIO()))
def texts(events: list[StoredEvent]) -> list[str]:
return [getattr(e.event, "text", "") for e in events]
# --- the projection, per role ----------------------------------------------------
def delegated_milestone(store: StateStore) -> None:
"""A milestone in which the driver failed and handed off to a helper."""
store.transcript("a", "driver").append(PlanSubmitted(plan="1. fix it", model="y"))
driver = store.transcript("a", "driver")
driver.append(
ToolCall(call_id="e1", tool="run_command", arguments={"command": "make"})
)
driver.append(ToolResult(call_id="e1", result="error: missing ;", success=False))
helper = driver.as_actor("helper")
helper.append(
DelegationStarted(brief="make fails", role="helper", model="y", caps={})
)
helper.append(AssistantText(text="looking at main.c", model="y"))
helper.append(
ToolCall(call_id="h1", tool="read_file", arguments={"path": "main.c"})
)
helper.append(ToolResult(call_id="h1", result="int main()", success=True))
helper.append(
DelegationCompleted(
handback="fixed the missing semicolon",
artifacts=["main.c"],
tokens=0,
duration_s=1.0,
exit_status="completed",
)
)
def test_the_driver_sees_the_helpers_report_not_its_turns(tmp_path: Path) -> None:
store = new_store(tmp_path)
delegated_milestone(store)
view = store.transcript("a", "driver").view()
assert [type(e.event).__name__ for e in view] == [
"ToolCall",
"ToolResult",
"UserMessage",
]
assert view[-1].event.text == (
"[helper report, completed] fixed the missing semicolon"
)
def test_the_helper_sees_the_plan_and_the_drivers_work_as_narration(
tmp_path: Path,
) -> None:
store = new_store(tmp_path)
delegated_milestone(store)
view = store.transcript("a", "helper").view()
said = texts(view)
assert said[0] == "[approved plan] 1. fix it"
assert said[1] == "[driver] run_command(make) → failed: error: missing ;"
# Its own events are unchanged, tool calls and all.
assert any(isinstance(e.event, ToolCall) and e.event.call_id == "h1" for e in view)
assert not any(
isinstance(e.event, ToolCall) and e.event.call_id == "e1" for e in view
)
def test_a_view_keeps_log_ids_so_cursors_still_work(tmp_path: Path) -> None:
store = new_store(tmp_path)
delegated_milestone(store)
raw = store.transcript("a", "driver").read()
view = store.transcript("a", "driver").view(after=raw[2].id)
assert [e.id for e in view] == [raw[-1].id] # the helper's report
# --- rendering ---------------------------------------------------------------------
def test_both_renderers_show_a_fact_as_a_labelled_user_message() -> None:
events = [StoredEvent(id=1, ts="", event=Fact(text="PR #3 opened"))]
openai = OpenAIChatRenderer().render(events, tools=None, model="m")
assert openai["messages"] == [
{"role": "user", "content": "[orchestrator] PR #3 opened"}
]
anthropic = AnthropicMessagesRenderer().render(events, tools=None, model="m")
assert anthropic["messages"] == [
{
"role": "user",
"content": [{"type": "text", "text": "[orchestrator] PR #3 opened"}],
}
]
def test_operator_messages_carry_their_channel(tmp_path: Path) -> None:
store = new_store(tmp_path)
operator = store.transcript("a", "operator")
for source in ("transport:@don", "gitea:issue:9:comment:4", "gitea:comment:5"):
operator.append(UserMessage(text="hi", source=source))
assert texts(store.transcript("a", "driver").view()) == [
"[operator, Matrix] hi",
"[operator, comment on plan issue #9] hi",
"[operator, Gitea comment] hi",
]
def test_something_written_mid_call_moves_after_the_result(tmp_path: Path) -> None:
# Live, M2.1: start_milestone's fact landed between the call and its
# result, and the model API rejected every later request in the run.
store = new_store(tmp_path)
driver = store.transcript("a", "driver")
driver.append(ToolCall(call_id="c1", tool="start_milestone", arguments={}))
store.transcript("a", "orchestrator").append(Fact(text="milestone 1 started"))
driver.append(ToolResult(call_id="c1", result="started", success=True))
kinds = [type(e.event).__name__ for e in driver.view()]
assert kinds == ["ToolCall", "ToolResult", "UserMessage"]
body = AnthropicMessagesRenderer().render(driver.view(), tools=None, model="m")
result_first = body["messages"][1]["content"][0]
assert result_first["type"] == "tool_result"