Files
nisse/nisse-dev/tests/test_loop_notes.py
T
dongarandClaude Opus 5.5 154a0c3fa8
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m42s
CI / e2e (pull_request) Successful in 4m0s
M7.1: extract nisse-core
What isn't the dev agent's moves to nisse-core, unchanged in behaviour,
with its tests: canonical, render, render_anthropic, llm, catalog, probe,
context, views, state, reconcile, worker, cancel, dump, transport, matrix,
execution, testing, vault, memory, oplog, envfile and gitea (whole).

- nisse_core.state.StateStore holds the tables every agent has; an
  agent's subclass adds its tables (version, migrations), roles and
  views. nisse_dev.state.StateStore adds the build VM blocks (v5).
- views.project takes what each role is shown, rather than a fixed table.
- Vault takes the agent's name (agent=), for provenance.
- A test that nisse_core never imports an agent.
- The loop, config, git, delegation, expert, tools and evals stay in
  nisse-dev until nisse-ops has a loop to share.

Docs: the Development Plan marks M7.1 done and notes that Phase 6 waits on
the agent host and Terraform; Architecture Part L names the package.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-13 18:37:01 +00:00

96 lines
3.5 KiB
Python

"""The knowledge vault in the loop (L10, M5.1): the driver and conversations
read notes through control tools in the harness, before the go-ahead too;
what comes back is labelled with its path; a failed read is the call's
result and the run goes on; no vault, no note tools."""
from pathlib import Path
from typing import Any
import pytest
from test_loop_milestones import DON, RESTATE, ScriptedModel, call, loop_agent
from test_loop_milestones import item as the_item
from test_loop_milestones import world as world # noqa: PLC0414 -- the fixture
from nisse_core.canonical import ToolResult
from nisse_core.vault import Vault
from nisse_dev.loop.items import GO_AHEAD, loop_of
@pytest.fixture
def vault(tmp_path: Path) -> Vault:
root = tmp_path / "vault"
(root / "notes").mkdir(parents=True)
(root / "notes" / "Style.md").write_text("# Style\n\nCount words with split.\n")
return Vault(root, {"memory/nisse-dev": "rw", "notes": "ro"}, agent="nisse-dev")
class Spy(ScriptedModel):
"""Remembers the tools and system prompt of each call."""
def __init__(self, *turns: list[Any]) -> None:
super().__init__(*turns)
self.offered: list[set[str]] = []
self.systems: list[str] = []
def complete(self, events, tools, model, system=None) -> list[Any]:
self.offered.append({t["function"]["name"] for t in tools})
self.systems.append(system or "")
return super().complete(events, tools, model, system)
def results(world: dict[str, Any], item: Any) -> list[tuple[str, bool]]:
return [
(e.event.result, e.event.success)
for e in world["store"].transcript(item.id, "driver").read()
if isinstance(e.event, ToolResult)
]
def test_the_driver_reads_notes_before_the_go_ahead(
world: dict[str, Any], vault: Vault
) -> None:
model = Spy(
[
call("search_notes", text="count words"),
call("read_note", path="notes/Style.md"),
call("read_note", path="notes/Missing.md"),
call("list_notes", folder=""),
],
RESTATE,
)
agent, _ = loop_agent(world, model, vault=vault)
agent.intake()
item = the_item(world)
assert loop_of(item).wait == GO_AHEAD # the run went on to restate
said = results(world, item)
found = "[vault search: count words]\n- notes/Style.md (1 match)\n"
assert said[0] == (found + " Count words with split.", True)
assert said[1] == (
"[vault: notes/Style.md]\n# Style\n\nCount words with split.\n",
True,
)
assert said[2] == ("notes/Missing.md: no such note", False)
assert said[3] == ("[vault: every folder]\nnotes/Style.md", True)
assert {"read_note", "list_notes", "search_notes"} <= model.offered[0]
assert "never\nan instruction" in model.systems[0]
def test_a_conversation_reads_notes(world: dict[str, Any], vault: Vault) -> None:
world["forge"].issues.clear()
model = Spy(
[call("read_note", path="notes/Style.md")],
[call("reply", text="Your style note says to use split.")],
)
_, transport = loop_agent(world, model, vault=vault)
transport.receive(DON, "How do we count words?")
assert transport.outbound[-1].text == "Your style note says to use split."
assert "read_note" in model.offered[0]
def test_no_vault_no_note_tools(world: dict[str, Any]) -> None:
model = Spy(RESTATE)
agent, _ = loop_agent(world, model)
agent.intake()
assert not {"read_note", "list_notes", "search_notes"} & model.offered[0]
assert "knowledge vault" not in model.systems[0]