poolcoach / tests /test_api_trajectory.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
5.63 kB
"""Contract của `POST /api/trajectory` — render lười (30/07/2026).
Cùng nguyên tắc mọi test app khác: kiểm SHAPE / STATUS / MESSAGE, KHÔNG kiểm
vật lý. `simulate_shot_multi` bị monkeypatch (conftest cố ý nhét stub pooltool
rỗng — chạm tầng sim thật sẽ ném AttributeError ngay).
Phép kiểm "quỹ đạo endpoint này trả ĐÚNG BẰNG quỹ đạo `n_render = 1 +
alternatives` trả cho cùng cú" (gate G6.2) KHÔNG nằm ở đây được: nó cần sim
thật. Nó chạy bằng `scripts/check_lazy_render.py`, cùng lý do `check_api_
drift.py` tồn tại ngoài `tests/`.
"""
from __future__ import annotations
import numpy as np
import pytest
from fastapi.testclient import TestClient
from poolcoach_rl import recommend as rec_pkg
from app import main as app_main
BALLS = {"cue": {"x": 0.45, "y": 0.30}, "1": {"x": 0.32, "y": 1.09},
"2": {"x": 0.74, "y": 0.95}, "9": {"x": 0.50, "y": 1.29}}
REQ = {"balls": BALLS, "phi": 45.0, "v0": 2.0, "side": 0.0, "vert": 0.0}
@pytest.fixture
def client(monkeypatch, env_stub):
monkeypatch.setattr(app_main.state, "env_h", env_stub)
monkeypatch.setattr(app_main.state, "jit_ready", True)
monkeypatch.setattr(app_main.state, "boot_error", None)
return TestClient(app_main.app)
def patch_sim(monkeypatch, result, seen=None):
"""Thay `simulate_shot_multi` — `_trajectory_sync` import LAZY từ
`poolcoach_rl.recommend`, nên patch ở package là đúng chỗ (cùng cách
`test_api_v2.patch_full` làm với `recommend_full`)."""
def fake(env_h, balls, phi, v0, side, vert, render=False):
if seen is not None:
seen.update(phi=phi, v0=v0, side=side, vert=vert, render=render,
balls=balls)
return result
monkeypatch.setattr(rec_pkg, "simulate_shot_multi", fake)
monkeypatch.setattr(rec_pkg, "extract_trajectories",
lambda system, min_disp=None: {
"cue": [[0.45, 0.30], [0.5, 1.0]],
"1": [[0.32, 1.09], [0.33, 1.10]]})
def sim_ok():
return {"system": object(), "potted": ["1"], "scratch": False,
"first_contact": "1",
"balls_final": {"cue": np.array([0.5, 1.0]), "1": None,
"2": np.array([0.74, 0.95]),
"9": np.array([0.50, 1.29])}}
# ------------------------------------------------------------------- shape
def test_tra_dung_hai_khoa_trajectories_va_balls_final(client, monkeypatch):
seen = {}
patch_sim(monkeypatch, sim_ok(), seen)
r = client.post("/api/trajectory", json=REQ)
assert r.status_code == 200
data = r.json()
assert set(data) == {"trajectories", "balls_final"}
assert data["trajectories"]["cue"][0] == [0.45, 0.30]
assert data["balls_final"]["1"] is None # bi đã vào lỗ
assert data["balls_final"]["cue"] == {"x": 0.5, "y": 1.0}
def test_render_true_va_tham_so_cu_di_nguyen_xuong_sim(client, monkeypatch):
"""Endpoint là MỘT lần sim, không phải một lần search: 4 tham số cú phải
tới `simulate_shot_multi` nguyên xi, và `render=True` (không có nó thì
`system is None` và quỹ đạo rỗng)."""
seen = {}
patch_sim(monkeypatch, sim_ok(), seen)
client.post("/api/trajectory", json={"balls": BALLS, "phi": 132.5,
"v0": 3.25, "side": -0.4,
"vert": 0.2})
assert seen["render"] is True
assert (seen["phi"], seen["v0"], seen["side"], seen["vert"]) == (
132.5, 3.25, -0.4, 0.2)
assert set(seen["balls"]) == set(BALLS)
def test_spin_mac_dinh_0_khi_request_khong_gui(client, monkeypatch):
seen = {}
patch_sim(monkeypatch, sim_ok(), seen)
r = client.post("/api/trajectory",
json={"balls": BALLS, "phi": 45.0, "v0": 2.0})
assert r.status_code == 200
assert (seen["side"], seen["vert"]) == (0.0, 0.0)
# ------------------------------------------------------------------- lỗi
def test_the_ban_hong_tra_422_string_y_nhu_api_recommend(client, monkeypatch):
"""Cùng `validate_full` với `/api/recommend` → cùng message, không phải
một bộ message thứ hai. `detail` phải là STRING (FE toast chỉ đọc string).
"""
patch_sim(monkeypatch, sim_ok())
bad = dict(BALLS, **{"2": {"x": 0.32, "y": 1.09}}) # trùng chỗ bi 1
r = client.post("/api/trajectory", json={**REQ, "balls": bad})
assert r.status_code == 422
detail = r.json()["detail"]
assert isinstance(detail, str)
assert "chồng lên nhau" in detail
def test_sim_tra_none_thi_422_chu_khong_phai_200_quy_dao_rong(client,
monkeypatch):
"""pooltool ném → `simulate_shot_multi` trả None. Trả 200 kèm quỹ đạo rỗng
thì FE vẽ bàn trống và không ai biết vì sao — phải nói thẳng là sim hỏng."""
patch_sim(monkeypatch, None)
r = client.post("/api/trajectory", json=REQ)
assert r.status_code == 422
assert "Không mô phỏng được" in r.json()["detail"]
def test_chua_jit_xong_tra_503(client, monkeypatch):
monkeypatch.setattr(app_main.state, "jit_ready", False)
r = client.post("/api/trajectory", json=REQ)
assert r.status_code == 503
def test_boot_error_tra_500(client, monkeypatch):
monkeypatch.setattr(app_main.state, "boot_error", "RuntimeError: hong")
r = client.post("/api/trajectory", json=REQ)
assert r.status_code == 500