Files
dev-puppeteer/my-deepagent/tests/integration/test_instructions.py
chungyeong 61b34af0e4 feat(my-deepagent): v0.3 PR #7 — MYDEEPAGENT.md global+project hierarchy
Claude Code 의 CLAUDE.md 글로벌/프로젝트 레이어링 등가.  세션 시작 시 두
파일을 자동 로드해 시스템 프롬프트에 inject:
- Global: <config.data_dir>/MYDEEPAGENT.md (템플릿 자동 생성, idempotent)
- Project: <repo>/MYDEEPAGENT.md (있을 때만 로드, auto-create 안 함)

순서는 [global → project → MEMORY.md → entry .md] 라서 후순위 파일이
deepagents `MemoryMiddleware`의 "later overrides earlier" 규칙에 따라
더 구체적인 맥락으로 일반 지침을 덮을 수 있음.

데이터·라이브러리:
- `instructions.py` (신규):
  - `global_instructions_path(config)`, `project_instructions_path(repo_root)`
  - `ensure_global_instructions_initialized(config)` — 글로벌 템플릿 1회 생성.
    Korean-default 협업·코드 스타일 가이드 시드.  Idempotent (사용자 편집 보존).
  - `resolve_instruction_paths(config, repo_root)` — 존재하는 파일만 절대 경로로
    글로벌 → 프로젝트 순서 반환.

REPL 통합 (`cli/interactive.py`):
- `InteractiveSession.__init__`에서 `ensure_global_instructions_initialized`
  호출.
- `build_agent_if_needed`에서 `[*instructions, *memory]` 순서로
  `memory_paths_override` 구성 → deepagents memory= kwarg 까지 전파.

테스트 (`tests/integration/test_instructions.py`, 6 케이스):
- 글로벌 부트스트랩 + idempotency (수동 편집 보존)
- 프로젝트 파일은 auto-create 안 함
- 0/1/2 개 존재 시 `resolve_instruction_paths` 반환 순서 검증
- global path 가 data_dir 아래에 위치
- **integration**: `build_agent`가 결합 리스트를 `create_deep_agent(memory=...)`
  로 그대로 전달

게이트:
- ruff check / format --check / mypy: PASS
- pytest -q --ignore=tests/integration/test_e2e_workflow.py
  --ignore=tests/integration/test_openrouter_smoke.py: 671 passed (6 신규 포함)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:55:06 +09:00

162 lines
5.7 KiB
Python

"""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
# ---------------------------------------------------------------------------
# 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