Files
nisse/nisse-dev/tests/test_memory.py
T
dongarandClaude Opus 5.5 fe4548459f
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m57s
CI / e2e (pull_request) Successful in 3m49s
M5.3: scope memory
A project's notes have their own folder, memory/nisse-dev/projects/<project>/,
with its own index; remember and forget take scope (general, or project:
always the current item's own, so no item writes another project's notes,
and a conversation has none). At an item's start the transcript gets the
general index and its project's, never another's. Index links are pages
from the vault's root, as SilverBullet names them.

The closing pass's memory_done can name repository documents the item left
out of date; the harness says so in the thread and records it. The prompt:
the repository is the authority, and a note that disagrees is corrected.

With the operator: ARCHITECTURE.md isn't put into every item's context (its
size); the infrastructure world model waits for nisse-ops. The walk writes
project notes and checks they come only from that project's items.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-08 23:33:12 +00:00

96 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_dev.memory import Memory
from nisse_dev.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"}), 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")