Spaces:
Sleeping
Sleeping
| """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} | |
| 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 | |