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
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.knowledge.memory import Memory
|
|
from nisse_core.knowledge.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")
|