Files
dev-puppeteer/my-deepagent/tests/integration/test_instructions.py
chungyeong 96c8849e2c fix(my-deepagent): v0.3 plan-conformance — 18-item gap fix across PR #2-#9
1차 v0.3 구현 후 plan-v0.3 와 대조해 발견된 18건 누락/명세 위반을 보강.
자기 리뷰 3 라운드 (누락·미완 / 오류·엣지케이스 / 과최적화) 모두 PASS.

PR #5 plan-mode (3건):
- BLOCKED_TOOLS_IN_PLAN_MODE 에 write_todos 추가
- /plan 시 system message inject (_PLAN_MODE_SYSTEM_PROMPT)
- /approve 시 마지막 assistant 메시지를 "approved plan" system 으로 inject
- InteractiveSession._pending_system_messages 인프라 신설

PR #2 compaction (1건):
- CompactionResult.summary_text 추가, 다음 thread 첫 ainvoke 에 inject

PR #3 auto-memory (6건):
- global memory dir + bootstrap
- frontmatter name/description/type 정식 도입 + MemoryEntry/MemoryType
- _infer_memory_type (keyword heuristic, no LLM)
- _scrub_secrets (OpenRouter/Anthropic/OpenAI/AWS/Bearer redaction)
- /memory show <name> 서브명령
- /remember [--global] / /forget [--global] 스코프 토글

PR #4 skills (3건):
- project_skills_dir + 두 스코프 (global / project) merge with last-wins
- /skill <name> 본문 inject (queue_system_message) — 이전엔 REPL 출력만
- /skills show <name> 별도 서브명령

PR #6 sub-agent (4건):
- budget.py `session:<uuid>` scope + CostMiddleware 자동 전달
- resolve_root_session_id walk-up (cycle guard) + sub-agent root 에 charge
- run_subagent_to_completion 실제 ainvoke + 결과 push to parent
- /agents 서브명령 구조 (list / spawn / show) + spawn 시 parent system msg

PR #7 governance (1건):
- bootstrap_user_dirs — instructions + global/memory + skills + projects 한
  호출로 idempotent 부트스트랩

PR #8 Web GUI (1건):
- index.html → 세션 목록, runs.html (신설) → workflow archive
- conversation.html ?session=<id> deep-link

PR #9 workflow integration (2건):
- /workflow 백그라운드 WorkflowEngine.run + 진행 메시지 stream 누적
- /binding show <workflow-name[@version]> 인자 지원

테스트 (+17, 685 → 702 passed):
- test_plan_mode: write_todos 차단 + blocklist sanity
- test_memory: scrub + type 추론 + override
- test_skills: project override + find_skill + resolve_skill_sources(pk)
- test_subagents: resolve_root_session_id chain + missing fallback
- test_budget: session: scope accumulation
- test_instructions: governance bootstrap + idempotency
- test_api_static: runs.html 신설 + index.html 재구성

게이트:
- ruff check / format --check / mypy: PASS (141 source files)
- pytest -q --ignore=tests/integration/test_e2e_workflow.py
  --ignore=tests/integration/test_openrouter_smoke.py: 702 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:03:08 +09:00

193 lines
7.0 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
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