"""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("- [[a-fact]] — Ay, again\n- [[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.path == 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