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
98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
"""Agent memory (L6.4, M5.2): one note per fact in the agent's folder, an
|
|
index the harness keeps, and a snapshot of it for the transcript."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from nisse_core.memory import Memory
|
|
from nisse_core.vault import Vault, VaultError, split_frontmatter
|
|
|
|
FOLDER = "memory/nisse-dev"
|
|
|
|
|
|
@pytest.fixture
|
|
def memory(tmp_path: Path) -> Memory:
|
|
return Memory(
|
|
Vault(tmp_path / "v", {FOLDER: "rw", "notes": "ro"}, agent="nisse-dev"), FOLDER
|
|
)
|
|
|
|
|
|
def note(memory: Memory, name: str) -> tuple[dict, str]:
|
|
path = memory._vault.root / FOLDER / f"{name}.md"
|
|
return split_frontmatter(path.read_text())
|
|
|
|
|
|
def test_a_fact_is_a_note_with_its_description_and_provenance(memory: Memory) -> None:
|
|
replaced = memory.remember(
|
|
"prefers-small-prs",
|
|
"The operator prefers small PRs",
|
|
"Don said PRs over 400 lines are hard to review.\n",
|
|
provenance={"source": "work item x"},
|
|
)
|
|
assert not replaced
|
|
front, body = note(memory, "prefers-small-prs")
|
|
assert front["description"] == "The operator prefers small PRs"
|
|
assert front["provenance"]["source"] == "work item x"
|
|
assert body == "Don said PRs over 400 lines are hard to review.\n"
|
|
|
|
|
|
def test_the_same_name_replaces_and_the_index_follows(memory: Memory) -> None:
|
|
memory.remember("b-fact", "Bee", "b", provenance={})
|
|
memory.remember("a-fact", "Ay", "a", provenance={})
|
|
assert memory.remember("a-fact", "Ay, again", "a2", provenance={})
|
|
index = memory.snapshot().text
|
|
assert index.endswith(
|
|
"- [[memory/nisse-dev/a-fact]] — Ay, again\n- [[memory/nisse-dev/b-fact]] — Bee"
|
|
)
|
|
memory.forget("a-fact")
|
|
assert "a-fact" not in memory.snapshot().text
|
|
with pytest.raises(VaultError, match="no such note"):
|
|
memory.forget("a-fact")
|
|
|
|
|
|
@pytest.mark.parametrize("name", ["index", "Caps", "../x", "a/b", "", "-x"])
|
|
def test_names_are_plain(memory: Memory, name: str) -> None:
|
|
with pytest.raises(VaultError, match="a note's name"):
|
|
memory.remember(name, "d", "f", provenance={})
|
|
|
|
|
|
def test_a_note_needs_a_description_and_a_fact(memory: Memory) -> None:
|
|
with pytest.raises(VaultError, match="description and the fact"):
|
|
memory.remember("x", " ", "f", provenance={})
|
|
|
|
|
|
def test_the_snapshot_is_hashed_and_made_if_missing(memory: Memory) -> None:
|
|
first = memory.snapshot()
|
|
assert first.paths == (f"{FOLDER}/index.md",)
|
|
assert "Nothing yet." in first.text
|
|
assert memory.snapshot().sha == first.sha # same index, same hash
|
|
memory.remember("x", "Ex", "f", provenance={})
|
|
assert memory.snapshot().sha != first.sha
|
|
|
|
|
|
def test_a_projects_notes_have_their_own_folder_and_index(memory: Memory) -> None:
|
|
memory.remember("general-one", "General", "g", provenance={})
|
|
memory.remember("deploy", "Deploys by tag", "d", provenance={}, project="sandbox")
|
|
memory.remember("other", "Other's", "o", provenance={}, project="other")
|
|
root = memory._vault.root / FOLDER
|
|
assert (root / "projects" / "sandbox" / "deploy.md").exists()
|
|
top = memory.snapshot().text
|
|
assert "- [[memory/nisse-dev/general-one]] — General" in top
|
|
assert "deploy" not in top.split("Projects:")[0]
|
|
assert "- [[memory/nisse-dev/projects/sandbox/index]] — notes about sandbox" in top
|
|
seen = memory.snapshot("sandbox")
|
|
assert seen.paths == (
|
|
f"{FOLDER}/index.md",
|
|
f"{FOLDER}/projects/sandbox/index.md",
|
|
)
|
|
assert (
|
|
"- [[memory/nisse-dev/projects/sandbox/deploy]] — Deploys by tag" in seen.text
|
|
)
|
|
assert "projects/other/other]]" not in seen.text
|
|
memory.forget("deploy", "sandbox")
|
|
assert memory.snapshot("sandbox").paths == (f"{FOLDER}/index.md",)
|
|
assert "sandbox/index" not in memory.snapshot().text
|
|
with pytest.raises(VaultError, match="not a project's name"):
|
|
memory.remember("x", "d", "f", provenance={}, project="../x")
|