"""The knowledge vault (L10, M5.1): only configured folders exist, paths can't escape them, read-only folders refuse writes, writes are atomic and record provenance, and the editor's own files are never notes.""" import os from datetime import UTC, datetime from pathlib import Path import pytest import yaml from nisse_core.knowledge import vault as vault_module from nisse_core.knowledge.vault import Vault, VaultError, split_frontmatter @pytest.fixture def vault(tmp_path: Path) -> Vault: root = tmp_path / "vault" (root / "notes" / "deep").mkdir(parents=True) (root / "notes" / "Procedures.md").write_text("# Procedures\n\nRestart the hub.\n") (root / "notes" / "deep" / "Hub.md").write_text( "The hub is in the shed.\nhub hub\n" ) (root / "notes" / ".silverbullet").mkdir() (root / "notes" / ".silverbullet" / "state.md").write_text("hub") (root / "notes" / "_plug").mkdir() (root / "notes" / "_plug" / "x.md").write_text("hub") (root / "private").mkdir() (root / "private" / "Secret.md").write_text("the hub password") (root / "memory" / "nisse-ops").mkdir(parents=True) (root / "memory" / "nisse-ops" / "Ops.md").write_text("ops hub") return Vault( root, {"memory/nisse-dev": "rw", "notes": "ro"}, agent="nisse-dev", clock=lambda: datetime(2026, 10, 8, 12, 0, tzinfo=UTC), ) def test_only_configured_folders_exist(vault: Vault) -> None: assert vault.read("notes/Procedures.md").startswith("# Procedures") for path in ("private/Secret.md", "memory/nisse-ops/Ops.md", "Top.md"): with pytest.raises(VaultError, match="not in a folder you can see"): vault.read(path) assert vault.notes() == ["notes/Procedures.md", "notes/deep/Hub.md"] assert vault.notes("notes/deep") == ["notes/deep/Hub.md"] @pytest.mark.parametrize( "path", ["/etc/passwd", "notes/../private/Secret.md", "", "notes\\x.md", "../x.md"], ) def test_paths_cant_escape(vault: Vault, path: str) -> None: with pytest.raises(VaultError): vault.read(path) def test_a_link_out_of_its_folder_is_refused(vault: Vault) -> None: os.symlink(vault.root / "private" / "Secret.md", vault.root / "notes" / "Ln.md") with pytest.raises(VaultError, match="leads outside its folder"): vault.read("notes/Ln.md") assert "notes/Ln.md" not in vault.notes() assert all(f.path != "notes/Ln.md" for f in vault.search("password")) def test_the_editors_files_are_not_notes(vault: Vault) -> None: with pytest.raises(VaultError, match="editor's own files"): vault.read("notes/.silverbullet/state.md") with pytest.raises(VaultError, match="editor's own files"): vault.read("notes/_plug/x.md") with pytest.raises(VaultError, match="Markdown"): vault.read("notes/deep") def test_search_ranks_matches_and_sees_only_what_it_may(vault: Vault) -> None: found = vault.search("HUB") assert [f.path for f in found] == ["notes/deep/Hub.md", "notes/Procedures.md"] assert found[0].lines == ("The hub is in the shed.", "hub hub") with pytest.raises(VaultError): vault.search(" ") def test_a_long_note_is_cut_and_says_so(vault: Vault) -> None: (vault.root / "notes" / "Long.md").write_text("x" * 30_000) text = vault.read("notes/Long.md") assert text.endswith("[10000 more characters not shown]") def test_read_only_folders_refuse_writes(vault: Vault) -> None: with pytest.raises(VaultError, match="read-only"): vault.write("notes/New.md", "hi", provenance={"source": "t"}) assert not (vault.root / "notes" / "New.md").exists() def test_a_write_records_provenance_and_keeps_the_rest(vault: Vault) -> None: text = "---\ntags: [hub]\naliases: [h]\n---\n# Hub\n\nIn the shed.\n" vault.write( "memory/nisse-dev/sub/Hub.md", text, provenance={"source": "matrix", "work_item": "sandbox-14"}, ) written = (vault.root / "memory" / "nisse-dev" / "sub" / "Hub.md").read_text() front, body = split_frontmatter(written) assert front["tags"] == ["hub"] and front["aliases"] == ["h"] assert front["provenance"] == { "by": "nisse-dev", "source": "matrix", "work_item": "sandbox-14", "written": "2026-10-08T12:00:00+00:00", } assert body == "# Hub\n\nIn the shed.\n" assert vault.notes("memory/nisse-dev") == ["memory/nisse-dev/sub/Hub.md"] def test_an_interrupted_write_leaves_the_old_note_and_no_litter( vault: Vault, monkeypatch: pytest.MonkeyPatch ) -> None: vault.write("memory/nisse-dev/N.md", "old\n", provenance={"source": "t"}) folder = vault.root / "memory" / "nisse-dev" before = (folder / "N.md").read_text() def crash(src: str, dst: str) -> None: raise OSError("disk gone") monkeypatch.setattr(vault_module.os, "replace", crash) with pytest.raises(VaultError, match="couldn't write it"): vault.write("memory/nisse-dev/N.md", "new\n", provenance={"source": "t"}) assert (folder / "N.md").read_text() == before assert sorted(p.name for p in folder.iterdir()) == ["N.md"] def test_bad_frontmatter_is_refused(vault: Vault) -> None: with pytest.raises(VaultError, match="valid YAML"): vault.write( "memory/nisse-dev/B.md", "---\n: [\n---\nx\n", provenance={"source": "t"} ) with pytest.raises(VaultError, match="mapping"): vault.write("memory/nisse-dev/B.md", "---\n- a\n---\nx\n", provenance={}) def test_read_write_folders_are_made(tmp_path: Path) -> None: Vault(tmp_path / "v", {"memory/nisse-dev": "rw", "notes": "ro"}, agent="a") assert (tmp_path / "v" / "memory" / "nisse-dev").is_dir() assert not (tmp_path / "v" / "notes").exists() # not ours to make assert yaml # frontmatter is YAML