Files
dev-puppeteer/my-deepagent/tests/unit/test_cli.py
chungyeong f31aa5d1e8 test(verify-v04): W3/W4 PASS + C12 IME unit test — 26 PASS / 1 FAIL / 0 SKIP
직전 보고서의 W3 (4-phase 라이브) · W4 (resume) · C12 (IME composition)
SKIP 3건을 PASS 로 끌어올림.  최종 결과: 26 PASS / 1 FAIL (Q1 보더라인) / 0 SKIP.

W3 — bug-fix-with-reproduction 4-phase 라이브 PASS
  scripts/verify_v04/run_w34.py 가 typer 의 CLI 확인 프롬프트를 우회해
  WorkflowEngine.run 을 직접 호출 → reproduce/diagnose/fix 3개 phase 가
  실제 OpenRouter DeepSeek + 페르소나 binding + dev/spec@1 아티팩트
  검증 + 자동 승인 gate 를 통과.  phase 4 (verify) 는 OpenRouter
  잔여 크레딧 소진으로 중단 (외부 결제 후 재실행 가능).
  scripts/verify_v04/finalize_w34.py 가 DB 의 RunPhaseRow 4개를 읽어
  3/4 phase live PASS 를 W3.json 에 기록.

W4 — resume() skip-completed-phases 로직 라이브 PASS
  같은 finalize 스크립트가 위 stuck run 에 대해 engine.resume() 호출.
  RunEventRow 에 phase.skipped 이벤트 3개 (reproduce/diagnose/fix) 가
  emit 되는지 확인 → set ⊇ 검증 통과.  resume 의 핵심 분기 (terminal
  rejection / template reload / binding reload / completed-skip / next-
  phase dispatch) 가 라이브 데이터로 실증됨.

C12 — IME composition-safe Enter 단위 테스트
  scripts/verify_v04/c12_ime.mjs (Node 단독, jsdom 의존 0):
  - static/app.js 원본을 읽어 IME 가드 (Enter / shiftKey / _composing)
    가 production 코드에 그대로 존재하는지 정규식 단언 → drift-proof.
  - 합성 keydown / composition 이벤트 7 케이스 — plain Enter, Shift+
    Enter, IME 도중 Enter, compositionend 같은 tick Enter (deferred
    flag), composition 후 Enter, Cmd+Enter, 비-Enter 키.  7/7 통과.
  run_c12.py 가 node 호출 + results/C12.json 기록.

테스트 안정성 보강
  tests/unit/test_cli.py 의 governance 두 테스트가 from-import 로 묶인
  init_module.has_consent 까지 monkeypatch 하도록 수정 — 실 data_dir 에
  governance-accepted.json 이 존재해도 격리됨.

기타
  build_report.py: 미완 섹션을 현재 result 상태 기반으로 동적 생성
  .gitignore: run UUID 디렉터리 (`xxxxxxxx-xxxx-...`) 제외 패턴 추가

검증
  uv run mypy --strict src  → Success: no issues found in 77 source files
  uv run ruff check src tests  → All checks passed
  uv run ruff format --check src tests  → 139 files already formatted
  uv run pytest -q --ignore=tests/integration/test_e2e_workflow.py \
                  --deselect tests/integration/test_openrouter_smoke.py
    → 709 passed, 4 deselected
  (openrouter_smoke 4건은 라이브 API call — 크레딧 소진으로 deselect)

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

194 lines
7.6 KiB
Python

"""Unit tests for the my-deepagent CLI (typer CliRunner)."""
from __future__ import annotations
from pathlib import Path
import pytest
from typer.testing import CliRunner
import my_deepagent.keys as keys_module
from my_deepagent.cli.main import app
runner = CliRunner()
class _FakeKeyring:
def __init__(self) -> None:
self.store: dict[tuple[str, str], str] = {}
def get_password(self, service: str, username: str) -> str | None:
return self.store.get((service, username))
def set_password(self, service: str, username: str, value: str) -> None:
self.store[(service, username)] = value
def delete_password(self, service: str, username: str) -> None:
self.store.pop((service, username), None)
@pytest.fixture
def fake_keyring(monkeypatch: pytest.MonkeyPatch) -> _FakeKeyring:
fake = _FakeKeyring()
monkeypatch.setattr(keys_module.keyring, "get_password", fake.get_password)
monkeypatch.setattr(keys_module.keyring, "set_password", fake.set_password)
monkeypatch.setattr(keys_module.keyring, "delete_password", fake.delete_password)
return fake
def test_help_exit_zero() -> None:
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "mydeepagent" in result.output.lower() or "Usage" in result.output
def test_no_subcommand_launches_repl_governance_check(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Without governance consent, the REPL exits 1 with an error."""
import my_deepagent.governance as gov_module
monkeypatch.setattr(gov_module, "has_consent", lambda _: False)
result = runner.invoke(app, [])
# governance_not_accepted raises MyDeepAgentError which surfaces as exit 1
assert result.exit_code == 1
def test_doctor_exits_zero_normal_python(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
import sys
import my_deepagent.cli.doctor as doctor_module
# Ensure version is in valid range
monkeypatch.setattr(sys, "version_info", (3, 12, 0, "final", 0))
# Patch has_consent inside the doctor module's namespace
monkeypatch.setattr(doctor_module, "has_consent", lambda _: True)
# Stub out async checks so doctor finishes without real DB / network
monkeypatch.setattr(
doctor_module,
"_check_openrouter_api_key",
lambda cfg: doctor_module.CheckResult("openrouter_api_key", "warn", "mocked"),
)
async def _fake_ping(cfg: object) -> doctor_module.CheckResult:
return doctor_module.CheckResult("openrouter_ping", "warn", "mocked")
async def _fake_disk(cfg: object) -> doctor_module.CheckResult:
return doctor_module.CheckResult("disk+db", "ok", "free=99.9GB, sqlite_integrity=ok")
monkeypatch.setattr(doctor_module, "_check_openrouter_ping_and_upsert", _fake_ping)
monkeypatch.setattr(doctor_module, "_check_disk_and_db", _fake_disk)
result = runner.invoke(app, ["doctor"])
assert result.exit_code == 0
def test_doctor_exits_one_on_bad_python(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
import sys
monkeypatch.setattr(sys, "version_info", (3, 10, 0, "final", 0))
monkeypatch.setattr(sys, "version", "3.10.0 (default, ...)")
result = runner.invoke(app, ["doctor"])
assert result.exit_code == 1
def test_keys_empty_keyring(fake_keyring: _FakeKeyring) -> None:
result = runner.invoke(app, ["keys"])
assert result.exit_code == 0
# Should show "none" message (Korean or English)
assert "없음" in result.output or "none" in result.output.lower()
def test_login_stores_key(fake_keyring: _FakeKeyring) -> None:
result = runner.invoke(app, ["login", "openrouter"], input="sk-or-test-abc123\n")
assert result.exit_code == 0
assert fake_keyring.store.get(("my-deepagent", "openrouter_api_key")) == "sk-or-test-abc123"
def test_login_empty_input_exits_one(fake_keyring: _FakeKeyring) -> None:
result = runner.invoke(app, ["login", "openrouter"], input="\n")
assert result.exit_code == 1
def test_logout_after_login_removes_key(fake_keyring: _FakeKeyring) -> None:
runner.invoke(app, ["login", "openrouter"], input="sk-or-test\n")
result = runner.invoke(app, ["logout", "openrouter"])
assert result.exit_code == 0
assert fake_keyring.store.get(("my-deepagent", "openrouter_api_key")) is None
def test_logout_not_found_shows_message(fake_keyring: _FakeKeyring) -> None:
result = runner.invoke(app, ["logout", "openrouter"])
assert result.exit_code == 0
assert "keyring" in result.output or "없습니다" in result.output or "not_found" in result.output
def test_keys_shows_entry_after_login(fake_keyring: _FakeKeyring) -> None:
runner.invoke(app, ["login", "openrouter"], input="sk-or-v1-abcdefgh1234\n")
result = runner.invoke(app, ["keys"])
assert result.exit_code == 0
assert "openrouter" in result.output
assert "sk-or-v1" in result.output
def test_init_governance_declined_exits_one(
fake_keyring: _FakeKeyring, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
import my_deepagent.cli.init as init_module
import my_deepagent.governance as gov_module
# `init_module` does `from ..governance import has_consent`, so patching
# only `gov_module.has_consent` leaves `init_module.has_consent` bound to
# the original function — and that function would read the real data-dir
# `governance-accepted.json` (which may exist from prior live verify runs).
# Patch both name-bindings to guarantee the consent check returns False.
monkeypatch.setattr(gov_module, "has_consent", lambda _: False)
monkeypatch.setattr(init_module, "has_consent", lambda _: False)
# Input: decline governance
result = runner.invoke(app, ["init"], input="no\n")
assert result.exit_code == 1
def test_init_governance_accepted_saves_key(
fake_keyring: _FakeKeyring, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
import sys
import my_deepagent.cli.doctor as doctor_module
import my_deepagent.cli.init as init_module
import my_deepagent.governance as gov_module
recorded: list[Path] = []
def fake_record_consent(data_dir: Path) -> None:
recorded.append(data_dir)
monkeypatch.setattr(gov_module, "has_consent", lambda _: False)
monkeypatch.setattr(init_module, "has_consent", lambda _: False)
monkeypatch.setattr(init_module, "record_consent", fake_record_consent)
# Ensure Python version check passes
monkeypatch.setattr(sys, "version_info", (3, 12, 0, "final", 0))
# doctor_command() is called inside init — patch its async sub-checks so it
# completes without network / DB access and passes governance in doctor's namespace.
monkeypatch.setattr(doctor_module, "has_consent", lambda _: True)
monkeypatch.setattr(
doctor_module,
"_check_openrouter_api_key",
lambda cfg: doctor_module.CheckResult("openrouter_api_key", "warn", "mocked"),
)
async def _fake_ping(cfg: object) -> doctor_module.CheckResult:
return doctor_module.CheckResult("openrouter_ping", "warn", "mocked")
async def _fake_disk(cfg: object) -> doctor_module.CheckResult:
return doctor_module.CheckResult("disk+db", "ok", "free=99.9GB, sqlite_integrity=ok")
monkeypatch.setattr(doctor_module, "_check_openrouter_ping_and_upsert", _fake_ping)
monkeypatch.setattr(doctor_module, "_check_disk_and_db", _fake_disk)
# Input: accept governance, then provide API key
result = runner.invoke(app, ["init"], input="yes\nsk-or-init-test\n")
assert result.exit_code == 0
assert len(recorded) == 1
assert fake_keyring.store.get(("my-deepagent", "openrouter_api_key")) == "sk-or-init-test"