Spaces:
Sleeping
Sleeping
File size: 5,625 Bytes
78738de | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | """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
|