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