festifun/backend/tests/test_phase5_api.py
type-two 0314daec1a lane G: CR-4 — retire the obsolete capsule stub assertion (M17 landed; CR-1 precedent)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:42:19 +10:00

109 lines
4.1 KiB
Python

"""Phase-5 foundation2 contract tests: the new route shapes lanes E/F/G depend on
(plan/20-phase5.md "Contracts"). Lane G deepens paths coverage in test_paths.py;
lane E owns the real beats/director behavior tests.
"""
from __future__ import annotations
import json
import shutil
import pytest
pytestmark = pytest.mark.skipif(
shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None,
reason="ffmpeg/ffprobe required to build the API test fixture",
)
CAMPATH = {
"version": 1,
"keyframes": [
{"t_global": 1.0, "pos": [0, 1, 5], "quat": [0, 0, 0, 1], "fov": 50},
{"t_global": 4.0, "pos": [2, 1, 4], "quat": [0, 0.3826834, 0, 0.9238795], "fov": 42},
],
}
@pytest.fixture(scope="module")
def client():
from fastapi.testclient import TestClient
from festival4d import synthetic
synthetic.build(duration_s=1.0, run_ffmpeg=True) # into the temp data dir (conftest)
from festival4d import api
with TestClient(api.app) as c:
yield c
def test_beats_404_then_serves_file(client):
from festival4d import config
config.BEATS_JSON.unlink(missing_ok=True)
assert client.get("/api/beats").status_code == 404
body = {"tempo_bpm": 120.0, "beats_s": [0.5, 1.0], "onsets": [], "generated_by": "festival4d.audio_features"}
config.BEATS_JSON.parent.mkdir(parents=True, exist_ok=True)
config.BEATS_JSON.write_text(json.dumps(body))
try:
assert client.get("/api/beats").json() == body
finally:
config.BEATS_JSON.unlink()
def test_director_degrades_to_valid_empty_campath(client):
# While lane E is stubbed the response must still round-trip through camPath.fromJSON:
# a valid version-1 path with zero keyframes, plus a "note" marking the degradation.
data = client.post("/api/director", json={}).json()
assert data["version"] == 1
assert data["keyframes"] == []
assert "note" in data
assert client.post("/api/director", json={"top_n": 3, "lead_s": 1.0}).status_code == 200
def test_paths_crud_roundtrip(client):
assert client.get("/api/paths").json() == []
created = client.post(
"/api/paths", json={"name": "sweep", "json": json.dumps(CAMPATH)}
).json()
assert set(created) == {"id", "name", "created_at", "json"}
assert created["json"] == CAMPATH # deep-equal roundtrip
pid = created["id"]
listing = client.get("/api/paths").json()
assert [set(p) for p in listing] == [{"id", "name", "created_at"}]
assert client.get(f"/api/paths/{pid}").json()["json"] == CAMPATH
assert client.delete(f"/api/paths/{pid}").json() == {"deleted": pid}
assert client.get(f"/api/paths/{pid}").status_code == 404
assert client.delete(f"/api/paths/{pid}").status_code == 404
def test_paths_post_validates_campath(client):
post = lambda body: client.post("/api/paths", json={"name": "bad", "json": body}).status_code
assert post("not json at all {") == 422
assert post(json.dumps({"version": 2, "keyframes": []})) == 422
assert post(json.dumps({"version": 1})) == 422
assert post(json.dumps({"version": 1, "keyframes": [{"t_global": 0}]})) == 422
def test_anchor_patch_label_and_color(client):
a = client.post("/api/anchors", json={"label": "F", "x": 0, "y": 0, "z": 0}).json()
patched = client.patch(f"/api/anchors/{a['id']}", json={"label": "Fred", "color": "#4dd0ff"}).json()
assert patched["label"] == "Fred" and patched["color"] == "#4dd0ff"
assert (patched["x"], patched["y"], patched["z"]) == (0, 0, 0) # position untouched
# partial patch: color-only leaves the label alone
assert client.patch(f"/api/anchors/{a['id']}", json={"color": "#fff"}).json()["label"] == "Fred"
assert client.patch("/api/anchors/99999", json={"label": "x"}).status_code == 404
def test_cli_dispatches_stubs_gracefully():
# The new subcommands exist and degrade per the house pattern: exit code 2,
# no traceback, while their lane entrypoints are stubs. `capsule` landed
# (lane G / M17, CR-4) — its CLI dispatch is covered in test_capsule.py.
from festival4d import cli
for cmd in (["features"], ["direct"]):
assert cli.main(cmd) == 2