"""Fixtures dùng chung cho test app (ImplPlan Bước T) — 25/07/2026. Nguyên tắc: test kiểm CONTRACT (luật 9-ball, shape response, message), KHÔNG kiểm vật lý. pooltool thật nặng (~40s JIT Numba lần đầu) và không cần cho các gate này → nhét stub vào sys.modules trước khi import bất cứ thứ gì. """ from __future__ import annotations import sys import types from pathlib import Path # Repo KHÔNG cài package: phải tự đẩy src/ vào sys.path TRƯỚC mọi import # poolcoach_rl. Đừng dựa vào app/main.py (nó cũng chèn src, nhưng chỉ khi # được import → pass/fail sẽ phụ thuộc thứ tự collect của pytest). ROOT = Path(__file__).resolve().parents[1] for _p in (ROOT / "src", ROOT): if str(_p) not in sys.path: sys.path.insert(0, str(_p)) import numpy as np # noqa: E402 import pytest # noqa: E402 def _install_pooltool_stub() -> None: """Stub `pooltool` + `pooltool.constants` (module rỗng). `envs/position_env.py` import pooltool ở top-level nhưng mọi chỗ dùng `pt.` đều nằm trong thân hàm (annotation lazy nhờ `from __future__ import annotations`) → stub rỗng vẫn import lọt, đủ để `validate_full` lấy BALL_R. Cần gymnasium THẬT (venv có sẵn). Cố ý để stub thiếu `System`/`simulate`/`EventType`: nếu test nào lỡ chạm vào tầng vật lý, nó sẽ ném AttributeError ngay thay vì âm thầm chạy sim. """ if "pooltool" in sys.modules: return pt = types.ModuleType("pooltool") constants = types.ModuleType("pooltool.constants") constants.pocketed = "pocketed" pt.constants = constants sys.modules["pooltool"] = pt sys.modules["pooltool.constants"] = constants _install_pooltool_stub() class EnvStub: """Env tối thiểu cho tầng luật + describe_full: hằng số bàn thật + Q cố định. Chỉ cần `w/l/_pockets/_bounds/_position_q` — `judge_shot` gọi `_position_q`, `pocket_name` đọc `_pockets/w/l`, `validate_full` đọc `_bounds`. """ w = 0.9906 l = 1.9812 # Envelope vị trí nghỉ hợp lệ — SỐ DẪN RA TỪ BÀN THẬT, không phải số chọn # tay: `table_bounds(w, l, pt.Table.default().pockets.values())` = # hộp bao của (mặt bàn ∪ 6 đường tròn lỗ). Chép cứng vào đây vì stub không # có `table`, và conftest cố ý không có pooltool thật để dựng bàn. # # ĐỔI SPECS BÀN THÌ PHẢI SINH LẠI 4 SỐ NÀY. `test_validate_full.py` khoá cả # hai vế: một test kiểm `table_bounds` thật sự DẪN RA hộp từ hình học lỗ # (nạp bàn giả vào, kiểm mọi đường tròn lỗ nằm trọn trong hộp), một test # kiểm bộ số dưới đây đúng bằng cái `table_bounds` trả về trên specs bàn # mặc định của pooltool. _bounds = (-0.133, 1.1236, -0.09148635277547904, 2.072686352775479) def __init__(self, q: float = 0.6): self.q = q # 6 lỗ đúng bố cục bàn thật (pocket_name suy tên từ toạ độ) self._pockets = [ np.array([0.0, 0.0]), # 0 góc dưới-trái np.array([0.0, self.l / 2]), # 1 giữa-trái np.array([0.0, self.l]), # 2 góc trên-trái np.array([self.w, 0.0]), # 3 góc dưới-phải np.array([self.w, self.l / 2]), # 4 giữa-phải np.array([self.w, self.l]), # 5 góc trên-phải ] def _position_q(self, cue_xy, nxt_xy) -> float: return self.q @pytest.fixture def env_stub() -> EnvStub: return EnvStub()