import tempfile from pathlib import Path from beaver_gateway.agents.claude import ClaudeAgent, Prompts from beaver_gateway.conversations.context import ( bash_paths, compose, files_touched, granules, ) from beaver_gateway.vault.links import LinkIndex def entry(name: str, tool_input: dict, stamp: str = "2026-09-02T10:00:00Z") -> dict: return { "type": "assistant", "timestamp": stamp, "message": { "content": [{"type": "tool_use", "name": name, "input": tool_input}] }, } def test_files_touched_groups_by_path_relative_to_cwd() -> None: cwd = Path("/vault") files, counts = files_touched( [ entry("Read", {"file_path": "/vault/a.md"}, "2026-09-02T10:00:00Z"), entry("Edit", {"file_path": "/vault/a.md"}, "2026-09-02T10:05:00Z"), entry("Grep", {"pattern": "x", "path": "/vault"}), entry("Bash", {"command": "ls"}), entry("Read", {"file_path": "/elsewhere/b.md"}, "2026-09-02T09:00:00Z"), ], cwd=cwd, ) assert [f["path"] for f in files] == ["a.md", "/elsewhere/b.md"] assert files[0] == { "path": "a.md", "reads": 1, "writes": 1, "other": 0, "last_at": "2026-09-02T10:05:00Z", } assert counts == {"Read": 2, "Edit": 1, "Grep": 1, "Bash": 1} def test_bash_paths_follow_cd_and_quotes() -> None: paths, here, writes = bash_paths( "cd /vault/мета && cat 'заметка.md' | head; sed -n 1,5p \"📆 доски/работа.md\"", Path("/elsewhere"), ) assert paths == ["/vault/мета/заметка.md", "/vault/мета/📆 доски/работа.md"] assert here == Path("/vault/мета") assert writes is False _, _, writes = bash_paths( 'python3 - < None: cwd = Path("/vault") files, counts = files_touched( [ entry("Bash", {"command": "cd /vault/мета && cat a.md"}, "1"), entry("Bash", {"command": "python3 - < None: root = Path(tempfile.mkdtemp(prefix="beaver-ctx-")) (root / "voice.md").write_text("be brief " * 40) (root / "env.md").write_text("you are in the master thread") skills = root / "skills" / "common" (skills / "ops").mkdir(parents=True) (skills / "ops" / "SKILL.md").write_text( "---\nname: ops\ndescription: servers\n---\n" ) agent = ClaudeAgent( name="a", model="m", cwd=root, prompts=Prompts(master=(("voice", root / "voice.md"), root / "env.md")), skill_sets=(skills,), gateway_tools=("say",), ) out = compose( agent, "master", [entry("Read", {"file_path": str(root / "env.md")})], context_tokens=50_000, ) assert [g["path"] for g in out["prompt"]["granules"]] == ["voice.md", "env.md"] assert out["prompt"]["granules"][0]["tag"] == "voice" assert out["prompt"]["tokens_est"] > 0 assert out["skills"] == [ { "set": "common", "path": "skills/common", "skills": [ {"name": "ops", "description": "servers", "path": "skills/common/ops"} ], } ] assert out["tools"]["gateway"] == ["say"] assert out["files"][0]["path"] == "env.md" assert out["history_tokens_est"] < 50_000 assert granules(agent, "deep")["granules"] == [] def test_link_index_resolves_stems_and_neighbours() -> None: root = Path(tempfile.mkdtemp(prefix="beaver-links-")) (root / "people").mkdir() (root / "people" / "Marta.md").write_text("works at [[Studio]] with [[Ilya|him]]") (root / "Studio.md").write_text("see [[people/Marta]] and [[nowhere]]") (root / "Ilya.md").write_text("plain") (root / ".obsidian").mkdir() (root / ".obsidian" / "x.md").write_text("[[Studio]]") index = LinkIndex(root) graph = index.neighbours([str(root / "people" / "Marta.md")]) nodes = {n["path"]: n for n in graph["nodes"]} assert nodes["people/Marta.md"]["touched"] is True assert set(nodes) == {"people/Marta.md", "Studio.md", "Ilya.md"} assert {(e["from"], e["to"]) for e in graph["edges"]} == { ("people/Marta.md", "Studio.md"), ("people/Marta.md", "Ilya.md"), ("Studio.md", "people/Marta.md"), } missing = index.neighbours(["gone.md"]) assert missing["nodes"] == [ {"path": "gone.md", "title": "gone", "touched": True, "exists": False} ]