# pyright: reportUnusedVariable=false
"""
task-2431 회귀 테스트: post_merge_probe.py Scope-aware (단일 책임)

목적:
    익스텐션 PR이 server/tests 환경 결함으로 자동 revert되는 사고(task-2423/2429)를
    영구 차단한다. 본 task는 changed-path 기반 scope 판정과 scoped test 실행만
    책임진다 (baseline pre-flight는 별도 P1 task scope 외).

검증 시나리오:
    1. test_extension_pr_with_server_fail_does_not_revert — 핵심 회귀
    2. test_extension_pr_with_extension_fail_triggers_revert
    3. test_server_pr_with_server_fail_triggers_revert
    4. test_unmapped_changed_paths_runs_smoke_only
    5. test_no_changed_paths_fallback_to_smoke_not_full — full sweep 금지 회귀 가드
    6. test_run_tests_scoped_passes_rootdir_to_pytest — pytest 실제 인자 검증
    7. test_run_probe_does_not_have_baseline_check — baseline 코드 부재 회귀 가드
    8. test_resolve_test_scope_unions_multiple_areas — 단위
    9. test_changed_paths_returns_empty_on_git_failure — 단위
"""

import subprocess
import sys
from pathlib import Path

import pytest

# ---------------------------------------------------------------------------
# 모듈 경로 설정
# ---------------------------------------------------------------------------
_WORKSPACE = Path("/home/jay/workspace")
if str(_WORKSPACE) not in sys.path:
    sys.path.insert(0, str(_WORKSPACE))


# ---------------------------------------------------------------------------
# 헬퍼: post_merge_probe 모듈을 깨끗하게 재임포트
# ---------------------------------------------------------------------------

_PROBE_MODULE_CACHE = None


def _fresh_probe_module():
    """post_merge_probe 모듈을 한 번만 로드하여 캐시.

    fixture가 monkeypatch한 모듈 인스턴스를 테스트가 그대로 받도록
    프로세스 단위 캐시를 사용한다. monkeypatch는 fixture finalize 시
    자동 복원되므로 테스트 간 격리는 유지된다.
    """
    global _PROBE_MODULE_CACHE
    if _PROBE_MODULE_CACHE is not None:
        return _PROBE_MODULE_CACHE

    import importlib.util

    spec = importlib.util.spec_from_file_location(
        "post_merge_probe",
        _WORKSPACE / "scripts" / "post_merge_probe.py",
    )
    if spec is None or spec.loader is None:
        raise ImportError("Could not load post_merge_probe module spec")
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    _PROBE_MODULE_CACHE = mod
    return mod


# ---------------------------------------------------------------------------
# 공통 fixture: PROBE_MARK_DIR / AUDIT_LOG를 tmp_path 아래로 격리
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def isolate_paths(monkeypatch, tmp_path):
    """PROBE_MARK_DIR, AUDIT_LOG를 tmp_path 아래로 리다이렉트하여 테스트 격리."""
    events_dir = tmp_path / "memory" / "events"
    audit_dir = tmp_path / "memory" / "audit"
    events_dir.mkdir(parents=True, exist_ok=True)
    audit_dir.mkdir(parents=True, exist_ok=True)

    probe = _fresh_probe_module()
    monkeypatch.setattr(probe, "PROBE_MARK_DIR", events_dir)
    monkeypatch.setattr(probe, "AUDIT_LOG", audit_dir / "auto-merge.log")

    yield events_dir


# ---------------------------------------------------------------------------
# 헬퍼: Popen 호출 캡처용 더미 클래스
# ---------------------------------------------------------------------------

class _DummyPopen:
    """subprocess.Popen 대체 — 호출 횟수/인자 기록."""
    calls: list  # 클래스 공유 저장소 (인스턴스 생성 시 append)

    def __init__(self, args, **_kwargs):
        _DummyPopen.calls.append(args)

    def wait(self):
        return 0


def _make_popen_recorder():
    """DummyPopen 클래스를 초기화하고 반환."""
    _DummyPopen.calls = []
    return _DummyPopen


# ---------------------------------------------------------------------------
# 시나리오 1 (★ 핵심 회귀):
#   익스텐션 PR + server/tests 환경 fail → revert 차단
# ---------------------------------------------------------------------------

def test_extension_pr_with_server_fail_does_not_revert(monkeypatch, tmp_path):
    """changed_paths가 extension/ 파일만 포함하면 server/tests fail은 무시되어야 한다.

    _resolve_test_scope → mode="scoped", test_paths=["extension/__tests__/"]
    _run_tests_scoped는 extension/__tests__/만 실행하고 PASS 반환.
    auto_revert(Popen) 호출이 없어야 한다.
    """
    probe = _fresh_probe_module()

    # extension/__tests__/ 디렉토리 생성 (test_paths 존재 체크 우회)
    ext_tests = tmp_path / "extension" / "__tests__"
    ext_tests.mkdir(parents=True)

    popen_mock = _make_popen_recorder()
    monkeypatch.setattr(subprocess, "Popen", popen_mock)

    # _changed_paths: extension/ 파일만
    monkeypatch.setattr(probe, "_changed_paths",
                        lambda project_path, merge_sha: ["extension/content.js", "extension/popup.js"])

    # _resolve_test_scope: scoped — extension/__tests__/만
    monkeypatch.setattr(probe, "_resolve_test_scope",
                        lambda changed: (["extension/__tests__/"], "scoped"))

    # _run_tests_scoped: extension 테스트 PASS
    monkeypatch.setattr(probe, "_run_tests_scoped",
                        lambda project_path, test_paths: (True, "extension tests ok"))

    # time.sleep 비활성화
    monkeypatch.setattr("time.sleep", lambda s: None)

    result = probe.run_probe("task-ext-test", "abc123", tmp_path, delay=0)

    assert len(popen_mock.calls) == 0, (
        "auto_revert가 호출되지 않아야 함: server/tests 환경 fail은 extension PR과 무관"
    )
    assert result.get("outcome") in ("probe_pass", "scoped_pass"), (
        f"probe 결과가 PASS여야 함, 실제: {result.get('outcome')}"
    )
    # 단일 책임 회귀 방지: scope.mode는 'scoped'여야 함
    assert result.get("scope", {}).get("mode") == "scoped", (
        f"scope.mode='scoped' 기대, 실제: {result.get('scope', {}).get('mode')}"
    )
    # baseline_check 키는 본 task scope 외 — 절대 record에 존재하면 안 됨
    assert "baseline_check" not in result, (
        f"baseline_check 키는 본 task에서 제거되어야 함: {result.get('baseline_check')}"
    )


# ---------------------------------------------------------------------------
# 시나리오 2:
#   익스텐션 PR + extension/ 테스트 진짜 fail → revert 트리거
# ---------------------------------------------------------------------------

def test_extension_pr_with_extension_fail_triggers_revert(monkeypatch, tmp_path):
    """changed_paths=extension/, _run_tests_scoped는 extension/__tests__/에서 fail.

    baseline에서도 fail이 아닐 때 auto_revert가 1회 호출되어야 한다.
    """
    probe = _fresh_probe_module()

    ext_tests = tmp_path / "extension" / "__tests__"
    ext_tests.mkdir(parents=True)

    popen_mock = _make_popen_recorder()
    monkeypatch.setattr(subprocess, "Popen", popen_mock)

    monkeypatch.setattr(probe, "_changed_paths",
                        lambda project_path, merge_sha: ["extension/content.js"])
    monkeypatch.setattr(probe, "_resolve_test_scope",
                        lambda changed: (["extension/__tests__/"], "scoped"))

    # extension 테스트 FAIL (PR이 실제로 망가뜨린 케이스)
    monkeypatch.setattr(probe, "_run_tests_scoped",
                        lambda project_path, test_paths: (False, "FAILED 2 tests in extension/__tests__/"))

    monkeypatch.setattr("time.sleep", lambda s: None)

    result = probe.run_probe("task-ext-fail", "def456", tmp_path, delay=0)

    assert len(popen_mock.calls) == 1, (
        f"auto_revert가 1회 호출되어야 함, 실제 호출 횟수: {len(popen_mock.calls)}"
    )
    assert result.get("outcome") in ("probe_fail", "scoped_fail"), (
        f"probe 결과가 FAIL이어야 함, 실제: {result.get('outcome')}"
    )


# ---------------------------------------------------------------------------
# 시나리오 3:
#   server/tests PR + server/tests fail → revert 트리거 (정상 동작 유지)
# ---------------------------------------------------------------------------

def test_server_pr_with_server_fail_triggers_revert(monkeypatch, tmp_path):
    """changed_paths=server/, server/tests fail은 server PR에 대한 정당한 revert 사유다.

    auto_revert가 1회 호출되어야 한다.
    """
    probe = _fresh_probe_module()

    server_tests = tmp_path / "server" / "tests"
    server_tests.mkdir(parents=True)

    popen_mock = _make_popen_recorder()
    monkeypatch.setattr(subprocess, "Popen", popen_mock)

    monkeypatch.setattr(probe, "_changed_paths",
                        lambda project_path, merge_sha: ["server/app.py", "server/routes.py"])
    monkeypatch.setattr(probe, "_resolve_test_scope",
                        lambda changed: (["server/tests/"], "scoped"))

    # server 테스트 FAIL
    monkeypatch.setattr(probe, "_run_tests_scoped",
                        lambda project_path, test_paths: (False, "ModuleNotFoundError: No module named 'tests.conftest'"))

    monkeypatch.setattr("time.sleep", lambda s: None)

    result = probe.run_probe("task-server-fail", "ghi789", tmp_path, delay=0)

    assert len(popen_mock.calls) == 1, (
        f"server PR의 server/tests fail은 revert를 트리거해야 함, 호출 횟수: {len(popen_mock.calls)}"
    )


# ---------------------------------------------------------------------------
# 시나리오 4:
#   SCOPE_MAP 미매치 PR → smoke only 실행
# ---------------------------------------------------------------------------

def test_unmapped_changed_paths_runs_smoke_only(monkeypatch, tmp_path):
    """changed_paths가 docs/ README.md 등 SCOPE_MAP 미매치 경로일 때
    mode='smoke', test_paths=SMOKE_TEST_PATHS 가 반환되어야 한다.
    """
    probe = _fresh_probe_module()

    # smoke test 경로 생성
    smoke_dir = tmp_path / "tests" / "smoke"
    smoke_dir.mkdir(parents=True)

    monkeypatch.setattr(probe, "_changed_paths",
                        lambda project_path, merge_sha: ["docs/README.md", "docs/api.md"])

    captured = {}

    def _mock_resolve(changed_paths):
        # SCOPE_MAP 미매치 → smoke
        scope_map = getattr(probe, "SCOPE_MAP", {})
        matched = []
        for prefix, test_dirs in scope_map.items():
            if any(p.startswith(prefix) for p in changed_paths):
                matched.extend(test_dirs)
        if not matched:
            smoke_paths = getattr(probe, "SMOKE_TEST_PATHS", ["tests/smoke/"])
            captured["mode"] = "smoke"
            captured["test_paths"] = smoke_paths
            return (smoke_paths, "smoke")
        captured["mode"] = "scoped"
        captured["test_paths"] = sorted(set(matched))
        return (sorted(set(matched)), "scoped")

    monkeypatch.setattr(probe, "_resolve_test_scope", _mock_resolve)
    monkeypatch.setattr(probe, "_run_tests_scoped",
                        lambda project_path, test_paths: (True, "smoke ok"))
    monkeypatch.setattr(subprocess, "Popen", _make_popen_recorder())
    monkeypatch.setattr("time.sleep", lambda s: None)

    probe.run_probe("task-docs", "jkl012", tmp_path, delay=0)

    assert captured.get("mode") == "smoke", (
        f"SCOPE_MAP 미매치 PR은 smoke 모드여야 함, 실제: {captured.get('mode')}"
    )
    smoke_expected = getattr(probe, "SMOKE_TEST_PATHS", ["tests/smoke/"])
    assert captured.get("test_paths") == smoke_expected, (
        f"smoke 모드의 test_paths는 SMOKE_TEST_PATHS여야 함: {captured.get('test_paths')}"
    )


# ---------------------------------------------------------------------------
# 시나리오 5 (★ full sweep 금지 회귀 가드):
#   _changed_paths 빈 list → mode='smoke', 절대 'full' 금지
# ---------------------------------------------------------------------------

def test_no_changed_paths_fallback_to_smoke_not_full(monkeypatch, tmp_path):
    """_changed_paths가 빈 list 반환(merge_sha 없음 또는 git diff 실패)할 때
    run_probe는 mode='smoke'로 fallback해야 한다. 'full' 모드는 절대 사용 금지.
    """
    probe = _fresh_probe_module()

    # _changed_paths는 빈 리스트 반환 (git diff 실패 시뮬레이션)
    monkeypatch.setattr(probe, "_changed_paths",
                        lambda project_path, merge_sha: [])

    # _run_tests_scoped는 호출 인자 캡처
    captured = {}

    def _capture_scoped(project_path, test_paths):
        captured["test_paths"] = list(test_paths)
        return (True, "smoke ok")

    monkeypatch.setattr(probe, "_run_tests_scoped", _capture_scoped)
    monkeypatch.setattr(subprocess, "Popen", _make_popen_recorder())
    monkeypatch.setattr("time.sleep", lambda s: None)

    result = probe.run_probe("task-no-diff", "xyz999", tmp_path, delay=0)

    mode = result.get("scope", {}).get("mode")
    assert mode == "smoke", (
        f"changed_paths 빈 경우 mode='smoke'여야 함 (full 금지), 실제: {mode!r}"
    )
    assert mode != "full", "full sweep은 절대 사용 금지"
    # _run_tests_scoped는 SMOKE_TEST_PATHS로 호출되어야 함
    assert captured["test_paths"] == probe.SMOKE_TEST_PATHS, (
        f"smoke fallback 시 SMOKE_TEST_PATHS로 호출되어야 함, 실제: {captured['test_paths']}"
    )


# ---------------------------------------------------------------------------
# 시나리오 6 (★ pytest 실제 호출 인자 검증):
#   _run_tests_scoped는 --rootdir, -p no:cacheprovider를 pytest에 전달해야 함
# ---------------------------------------------------------------------------

def test_run_tests_scoped_passes_rootdir_to_pytest(monkeypatch, tmp_path):
    """_run_tests_scoped가 subprocess.run 호출 시 --rootdir과 -p no:cacheprovider를
    전달해야 한다. (collection 부작용 차단 + scope 강제)
    """
    probe = _fresh_probe_module()

    # 실제 존재하는 test path 준비
    scoped_dir = tmp_path / "extension" / "__tests__"
    scoped_dir.mkdir(parents=True)

    captured = {}

    class _FakeRun:
        returncode = 0
        stdout = "1 passed"
        stderr = ""

    def _fake_run(args, **kwargs):
        captured["args"] = list(args)
        captured["kwargs"] = kwargs
        return _FakeRun()

    monkeypatch.setattr(subprocess, "run", _fake_run)

    ok, out = probe._run_tests_scoped(tmp_path, ["extension/__tests__/"])

    assert ok is True, f"테스트가 PASS여야 함, 실제 출력: {out}"
    args = captured.get("args", [])
    assert args, "subprocess.run이 호출되어야 함"
    assert args[0] == "pytest", f"첫 인자는 pytest여야 함: {args}"
    assert "--rootdir" in args, (
        f"--rootdir 옵션이 pytest 인자에 포함되어야 함: {args}"
    )
    # --rootdir 다음 인자는 project_path
    rootdir_idx = args.index("--rootdir")
    assert args[rootdir_idx + 1] == str(tmp_path), (
        f"--rootdir 값은 project_path여야 함: {args[rootdir_idx + 1]}"
    )
    # 캐시 부작용 차단
    assert "-p" in args and "no:cacheprovider" in args, (
        f"-p no:cacheprovider 옵션이 포함되어야 함: {args}"
    )
    # cwd도 project_path로 강제
    assert captured["kwargs"].get("cwd") == tmp_path, (
        f"cwd는 project_path여야 함: {captured['kwargs'].get('cwd')}"
    )
    # scoped path가 인자에 포함되어야 함
    assert "extension/__tests__/" in args, (
        f"scoped test path가 pytest 인자에 포함되어야 함: {args}"
    )


# ---------------------------------------------------------------------------
# 시나리오 7 (★ baseline 코드 부재 회귀 가드):
#   run_probe 결과 record에 baseline_check 키가 없어야 함
# ---------------------------------------------------------------------------

def test_run_probe_does_not_have_baseline_check(monkeypatch, tmp_path):
    """probe FAIL 시에도 record["baseline_check"] 키는 절대 존재하면 안 된다.
    baseline pre-flight는 별도 task scope이며, 본 task는 단일 책임만 가진다.
    또한 .probe-baseline-fail 마커도 생성되면 안 된다.
    """
    probe = _fresh_probe_module()

    events_dir = tmp_path / "memory" / "events"
    events_dir.mkdir(parents=True, exist_ok=True)
    monkeypatch.setattr(probe, "PROBE_MARK_DIR", events_dir)

    ext_tests = tmp_path / "extension" / "__tests__"
    ext_tests.mkdir(parents=True)

    popen_mock = _make_popen_recorder()
    monkeypatch.setattr(subprocess, "Popen", popen_mock)

    monkeypatch.setattr(probe, "_changed_paths",
                        lambda project_path, merge_sha: ["extension/content.js"])
    monkeypatch.setattr(probe, "_resolve_test_scope",
                        lambda changed: (["extension/__tests__/"], "scoped"))
    # FAIL 케이스
    monkeypatch.setattr(probe, "_run_tests_scoped",
                        lambda project_path, test_paths: (False, "fail in extension"))
    monkeypatch.setattr("time.sleep", lambda s: None)

    task_id = "task-no-baseline"
    result = probe.run_probe(task_id, "deadbeef", tmp_path, delay=0)

    # 1. record에 baseline_check 키 없음
    assert "baseline_check" not in result, (
        f"baseline_check 키는 본 task에서 제거되어야 함: {result.get('baseline_check')}"
    )
    # 2. .probe-baseline-fail 마커 생성 안 됨
    marker_file = events_dir / f"{task_id}.probe-baseline-fail"
    assert not marker_file.exists(), (
        f".probe-baseline-fail 마커는 본 task에서 절대 생성되면 안 됨: {marker_file}"
    )
    # 3. _baseline_test_check 함수 자체가 모듈에 없어야 함 (baseline 코드 완전 제거)
    assert not hasattr(probe, "_baseline_test_check"), (
        "_baseline_test_check 함수는 본 task에서 완전히 제거되어야 함"
    )
    # 4. probe FAIL이므로 auto_revert는 호출되어야 함 (baseline 게이트 없음)
    assert len(popen_mock.calls) == 1, (
        f"baseline 게이트가 없으니 probe FAIL은 즉시 auto_revert 트리거: {len(popen_mock.calls)}회"
    )


# ---------------------------------------------------------------------------
# 시나리오 6 (단위):
#   _resolve_test_scope — 여러 영역 union + 결정적 정렬
# ---------------------------------------------------------------------------

def test_resolve_test_scope_unions_multiple_areas():
    """changed_paths에 extension/과 server/ 파일이 모두 있으면
    두 영역의 test_paths를 union하고 결정적으로 정렬해야 한다.
    """
    try:
        probe = _fresh_probe_module()
        _resolve = probe._resolve_test_scope
        SCOPE_MAP = probe.SCOPE_MAP
    except (ImportError, AttributeError):
        pytest.skip("토르 구현 대기 — _resolve_test_scope 미존재")

    changed = ["extension/content.js", "server/routes.py"]
    test_paths, mode = _resolve(changed)

    expected_extension = SCOPE_MAP.get("extension/", [])
    expected_server = SCOPE_MAP.get("server/", [])
    expected_union = sorted(set(expected_extension + expected_server))

    assert mode == "scoped", f"mode는 'scoped'여야 함, 실제: {mode}"
    assert sorted(test_paths) == expected_union, (
        f"test_paths union이 잘못됨. 기대: {expected_union}, 실제: {sorted(test_paths)}"
    )
    # 결정적 정렬: 같은 입력에 대해 두 번 호출해도 동일 순서
    test_paths2, _ = _resolve(changed)
    assert test_paths == test_paths2, "동일 입력에 대해 test_paths 순서가 일관되어야 함"


# ---------------------------------------------------------------------------
# 시나리오 7 (단위):
#   _changed_paths — git diff 실패(rc != 0) 시 [] 반환, 예외 없음
# ---------------------------------------------------------------------------

def test_changed_paths_returns_empty_on_git_failure(monkeypatch):
    """git diff 명령이 rc != 0으로 종료하면 _changed_paths는 [] 반환 (예외 안 던짐)."""
    try:
        probe = _fresh_probe_module()
        _changed_paths = probe._changed_paths
    except (ImportError, AttributeError):
        pytest.skip("토르 구현 대기 — _changed_paths 미존재")

    class _FailedRun:
        returncode = 1
        stdout = ""
        stderr = "fatal: not a git repository"

    monkeypatch.setattr(subprocess, "run", lambda *a, **kw: _FailedRun())

    result = _changed_paths(Path("/tmp/nonexistent"), "badsha")

    assert result == [], (
        f"git diff 실패 시 빈 리스트를 반환해야 함, 실제: {result!r}"
    )


# ---------------------------------------------------------------------------
# 시나리오 10 (★ 통합형 — Codex medium 보강):
#   _resolve_test_scope + _run_tests_scoped를 monkeypatch 없이 직접 호출.
#   server/tests/와 extension/__tests__/를 함께 만들고 server 영역 fail이
#   extension PR scope 결정 결과에 포함되지 않는지 실 호출로 검증.
# ---------------------------------------------------------------------------

def test_integration_extension_pr_scope_excludes_server_tests(tmp_path):
    """통합형: 실제 _resolve_test_scope + _run_tests_scoped 동작 검증.

    - 임시 repo에 server/tests/ (의도적 fail 테스트) + extension/__tests__/ (PASS) 배치
    - extension/ 변경에 대해 _resolve_test_scope가 server/tests/를 포함하지 않음
    - _run_tests_scoped가 실제 pytest를 호출했을 때 server 영역 fail이 결과에 포함되지 않음
    """
    probe = _fresh_probe_module()

    # 가짜 repo 구조
    (tmp_path / "extension" / "__tests__").mkdir(parents=True)
    (tmp_path / "server" / "tests").mkdir(parents=True)

    # extension PASS 테스트
    (tmp_path / "extension" / "__tests__" / "test_ok.py").write_text(
        "def test_extension_ok(): assert True\n"
    )
    # server FAIL 테스트 (의도적 ModuleNotFoundError)
    (tmp_path / "server" / "tests" / "test_broken.py").write_text(
        "import nonexistent_module_xyz_abc  # noqa\n"
        "def test_server_fail(): assert False\n"
    )

    # 1) scope 결정 — extension/ 변경 → extension/__tests__/만
    test_paths, mode = probe._resolve_test_scope(
        ["extension/content.js", "extension/popup.js"]
    )
    assert mode == "scoped", f"scope mode 기대 'scoped', 실제 {mode!r}"
    assert test_paths == ["extension/__tests__/"], (
        f"server/tests/는 절대 포함되면 안 됨: {test_paths!r}"
    )

    # 2) 실제 pytest 호출 — server/tests/는 인자에 없으므로 fail이 집계되지 않음
    ok, output = probe._run_tests_scoped(tmp_path, test_paths)
    assert ok is True, (
        f"extension 테스트만 실행되었으니 PASS여야 함: ok={ok}, output={output[-300:]}"
    )
    # 출력에 server/tests/test_broken.py가 등장하면 안 됨
    assert "test_broken" not in output, (
        f"server/tests/ 의 broken 테스트가 출력에 나타나면 scope leak: {output[-500:]}"
    )
    # extension 테스트는 실행되었어야 함
    assert "test_ok" in output or "passed" in output.lower(), (
        f"extension 테스트가 실행되었어야 함: {output[-500:]}"
    )


# ---------------------------------------------------------------------------
# 시나리오 11 (★ smoke 디렉토리 보장 — Codex medium 보강):
#   tests/smoke/ 디렉토리가 실제 workspace에 존재해야 함.
#   docs-only PR 등 unmapped PR이 무검증 통과하지 않도록 회귀 가드.
# ---------------------------------------------------------------------------

def test_smoke_directory_exists_in_workspace():
    """SMOKE_TEST_PATHS가 가리키는 디렉토리가 실제 workspace에 존재해야 함.

    이 디렉토리가 없으면 unmapped PR이 _run_tests_scoped의
    "no scoped test target — skipped" 분기로 무검증 PASS 처리됨.
    """
    probe = _fresh_probe_module()
    smoke_paths = probe.SMOKE_TEST_PATHS
    for sp in smoke_paths:
        full = _WORKSPACE / sp
        assert full.is_dir(), (
            f"SMOKE_TEST_PATHS가 가리키는 디렉토리가 부재: {full} "
            f"— unmapped PR이 무검증 통과 위험"
        )
        # 최소 1개의 .py test 파일 존재 보장
        py_tests = list(full.glob("test_*.py"))
        assert len(py_tests) >= 1, (
            f"smoke 디렉토리에 최소 1개의 test_*.py가 필요: {full}"
        )
