직전 보고서의 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>
194 lines
7.6 KiB
Python
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"
|