"""v0.3 PR #7 — MYDEEPAGENT.md instruction-file hierarchy tests. Covers: 1. Global file is bootstrapped with template on first call (idempotent). 2. Project file is NEVER auto-created — present iff user wrote it. 3. `resolve_instruction_paths` orders global → project. 4. Resolution is empty if global hasn't been bootstrapped yet. 5. `build_agent` passes the combined list through to `deepagents.create_deep_agent(memory=...)`. """ from __future__ import annotations from pathlib import Path from typing import Any import pytest from my_deepagent.config import load_config from my_deepagent.instructions import ( INSTRUCTION_FILENAME, ensure_global_instructions_initialized, global_instructions_path, project_instructions_path, resolve_instruction_paths, ) # --------------------------------------------------------------------------- # Bootstrap (global only) # --------------------------------------------------------------------------- def test_ensure_global_instructions_creates_template(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") p = ensure_global_instructions_initialized(cfg) assert p.is_file() assert p.name == INSTRUCTION_FILENAME body = p.read_text(encoding="utf-8") assert "MYDEEPAGENT.md (global)" in body assert "한국어" in body # template is Korean by default def test_ensure_global_instructions_idempotent(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") p = ensure_global_instructions_initialized(cfg) p.write_text("custom content", encoding="utf-8") # Second call must not overwrite user-edited content. p2 = ensure_global_instructions_initialized(cfg) assert p2 == p assert p.read_text(encoding="utf-8") == "custom content" # --------------------------------------------------------------------------- # Project file behaviour # --------------------------------------------------------------------------- def test_project_instructions_never_auto_created(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") repo = tmp_path / "repo" repo.mkdir() # Bootstrap global — must not touch project file. ensure_global_instructions_initialized(cfg) assert not project_instructions_path(repo).exists() # --------------------------------------------------------------------------- # resolve_instruction_paths # --------------------------------------------------------------------------- def test_resolve_paths_includes_only_existing_files(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") repo = tmp_path / "repo" repo.mkdir() # No files exist → empty. assert resolve_instruction_paths(cfg, repo) == [] # Only global. g = ensure_global_instructions_initialized(cfg) paths = resolve_instruction_paths(cfg, repo) assert paths == [str(g.resolve())] # Add project — order becomes global, project. proj_file = project_instructions_path(repo) proj_file.write_text("# project-specific", encoding="utf-8") paths = resolve_instruction_paths(cfg, repo) assert paths == [str(g.resolve()), str(proj_file.resolve())] def test_global_instructions_path_under_data_dir(tmp_path: Path) -> None: cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") p = global_instructions_path(cfg) assert p.parent == cfg.data_dir assert p.name == INSTRUCTION_FILENAME def test_governance_bootstrap_creates_full_skeleton(tmp_path: Path) -> None: """`bootstrap_user_dirs` materialises the user-wide layout (PR #7).""" from my_deepagent.governance import bootstrap_user_dirs from my_deepagent.memory import INDEX_FILENAME as MEMORY_INDEX_FILENAME cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") bootstrap_user_dirs(cfg) # Global MYDEEPAGENT.md created with template. assert global_instructions_path(cfg).is_file() # Global memory dir + MEMORY.md created. global_mem = Path(cfg.data_dir) / "global" / "memory" assert global_mem.is_dir() assert (global_mem / MEMORY_INDEX_FILENAME).is_file() # User skills dir created. assert (Path(cfg.data_dir) / "skills").is_dir() # Projects parent dir created. assert (Path(cfg.data_dir) / "projects").is_dir() def test_governance_bootstrap_is_idempotent(tmp_path: Path) -> None: from my_deepagent.governance import bootstrap_user_dirs cfg = load_config(workspace_root=tmp_path, data_dir=tmp_path / "data") bootstrap_user_dirs(cfg) gpath = global_instructions_path(cfg) gpath.write_text("custom edited content", encoding="utf-8") # Second call must not overwrite user edits. bootstrap_user_dirs(cfg) assert gpath.read_text(encoding="utf-8") == "custom edited content" # --------------------------------------------------------------------------- # Integration: instruction paths reach deepagents memory= kwarg # --------------------------------------------------------------------------- def test_build_agent_receives_combined_instruction_and_memory_paths( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """`build_agent(memory_paths_override=[instructions..., memory...])` passes the union through to `create_deep_agent(memory=...)`. Mirrors what InteractiveSession does at REPL bootstrap. """ from my_deepagent import session as session_mod from my_deepagent.persona import Persona captured: dict[str, Any] = {} def fake_create_deep_agent(**kwargs: Any) -> Any: captured.update(kwargs) return object() monkeypatch.setattr(session_mod, "create_deep_agent", fake_create_deep_agent) cfg = load_config( workspace_root=tmp_path, data_dir=tmp_path / "data", openrouter_api_key="test-key", ) repo = tmp_path / "repo" repo.mkdir() g = ensure_global_instructions_initialized(cfg) proj_file = project_instructions_path(repo) proj_file.write_text("# project rule", encoding="utf-8") # Simulate a project memory entry. mem_entry = tmp_path / "MEM.md" mem_entry.write_text("# memory entry", encoding="utf-8") persona = Persona( name="test-persona", version=1, backend="openrouter", model="openrouter:deepseek/deepseek-chat", provider_origin="CN/DeepSeek", capabilities=("code_edit",), max_risk_level="high", system_prompt="System prompt for test persona (must be ≥10 chars)", deepagents_backend="state", ) instruction_paths = resolve_instruction_paths(cfg, repo) combined = [*instruction_paths, str(mem_entry.resolve())] _agent = session_mod.build_agent( persona, cfg, root_dir=repo, memory_paths_override=combined, ) assert "memory" in captured # Global must come before project, project before mem entry — exact list match. expected = [str(g.resolve()), str(proj_file.resolve()), str(mem_entry.resolve())] assert captured["memory"] == expected