reel-studio-2 / tests /test_server.py
xvepkj's picture
initial commit
3d2098f
Raw
History Blame
7.66 kB
"""Server integration test: the full wizard flow over HTTP with mock adapters.
Covers the job lifecycle (submit -> poll -> result), artifact endpoints,
static file serving of the rendered MP4, and the clone route.
"""
from __future__ import annotations
import os
import time
import pytest
# Mock adapters must be selected BEFORE server.app builds its Pipeline.
os.environ.update(
ADAPTER_LLM="mock", ADAPTER_TTS="mock", ADAPTER_STOCK="mock",
ADAPTER_SFX="mock", ADAPTER_MUSIC="mock", ADAPTER_REFDATA="mock",
ADAPTER_TRANSCRIPT="mock",
)
from tests.mocks import register_mocks # noqa: E402
register_mocks()
from fastapi.testclient import TestClient # noqa: E402
from core.stages import topics as topics_stage # noqa: E402
from server.app import app # noqa: E402
client = TestClient(app)
def wait_job(job_id: str, timeout: float = 120.0) -> dict:
deadline = time.time() + timeout
while time.time() < deadline:
job = client.get(f"/api/jobs/{job_id}").json()
if job["status"] == "done":
return job["result"]
if job["status"] == "error":
raise AssertionError(f"job {job['kind']} failed: {job['error']}")
time.sleep(0.2)
raise AssertionError("job timed out")
@pytest.fixture(scope="module")
def project_id(request):
resp = client.post("/api/projects", json={
"name": "Server Smoke",
"niche": {"topic": "deep sea creatures", "keywords": ["ocean"],
"subreddits": ["ocean"], "audience": "everyone", "tone": "fun"},
"format": "youtube_short",
"references": [
{"kind": "youtube", "name": "@deepseadaily"},
{"kind": "instagram_style", "name": "oceanreels",
"url": "https://instagram.com/oceanreels", "notes": "big captions"},
],
})
assert resp.status_code == 200, resp.text
pid = resp.json()["id"]
request.addfinalizer(lambda: client.delete(f"/api/projects/{pid}"))
return pid
def test_health():
body = client.get("/api/health").json()
assert body["ok"] and "caption_presets" in body
assert "tts_providers" in body
def test_wizard_flow(project_id, monkeypatch):
monkeypatch.setattr(topics_stage, "_news_signals", lambda p: [])
monkeypatch.setattr(topics_stage, "_reddit_signals", lambda p: [])
# style: prompt prepare (job) then refresh, then edit-and-save the output
job = client.post(f"/api/projects/{project_id}/style/prompt").json()
assert "STYLE CARD" in wait_job(job["job_id"])["prompt"]
job = client.post(f"/api/projects/{project_id}/style/refresh", json={}).json()
style = wait_job(job["job_id"])
assert style["style_card"] and style["prompt_used"]
style["style_card"] = "EDITED style card"
assert client.put(f"/api/projects/{project_id}/style", json=style).json()["style_card"] == "EDITED style card"
# suggest references (job-based)
job = client.post(f"/api/projects/{project_id}/references/suggest", json={"n": 3}).json()
assert isinstance(wait_job(job["job_id"]), list)
# topics: prompt prepare + generate + inline edit
job = client.post(f"/api/projects/{project_id}/topics/prompt", json={"n": 5}).json()
assert wait_job(job["job_id"])["prompt"]
job = client.post(f"/api/projects/{project_id}/topics/generate", json={"n": 5}).json()
topics = wait_job(job["job_id"])["topics"]
assert len(topics) == 5
topics[0]["title"] = "Edited topic title"
resp = client.put(f"/api/projects/{project_id}/topics", json={"topics": topics})
assert resp.json()["topics"][0]["title"] == "Edited topic title"
# create a reel for the chosen topic
reel = client.post(f"/api/projects/{project_id}/reels",
json={"topic": "Edited topic title"}).json()
rid = reel["id"]
base = f"/api/projects/{project_id}/reels/{rid}"
# script: prompt prepare + generate + patch + scene regen
assert "scripts" in client.post(f"{base}/script/prompt", json={"topic": "Edited topic title"}).json()["prompt"].lower()
job = client.post(f"{base}/script/generate", json={"topic": "Edited topic title"}).json()
script = wait_job(job["job_id"])
assert len(script["hook_options"]) == 3 and script["prompt_used"]
script["selected_hook"] = 1
assert client.patch(f"{base}/script", json=script).json()["selected_hook"] == 1
job = client.post(f"{base}/script/regenerate-scene", json={"scene": 1}).json()
assert "Rewritten" in wait_job(job["job_id"])["scenes"][0]["narration"]
# voices listing (with providers) + voice synthesis
vinfo = client.get("/api/voices").json()
assert vinfo["voices"] and "providers" in vinfo
job = client.post(f"{base}/voice", json={"voice": "", "provider": "mock"}).json()
assert wait_job(job["job_id"])["total_duration_sec"] > 3
# media + swap
job = client.post(f"{base}/media").json()
media = wait_job(job["job_id"])
assert media["scenes"] and media["scenes"][0]["candidates"]
before = media["scenes"][0]["selected"]
swapped = client.post(f"{base}/media/swap", json={"line_index": 0}).json()
assert swapped["scenes"][0]["selected"] != before
# render one variant, then fetch the file over HTTP
job = client.post(f"{base}/render", json={
"variants": [{"name": "Variant A", "caption_preset": "bold-center-karaoke", "clip_offset": 0}],
}).json()
renders = wait_job(job["job_id"], timeout=300)
assert renders["results"]
url = renders["results"][0]["url"]
assert url.startswith("/files/")
video = client.get(url)
assert video.status_code == 200 and len(video.content) > 50_000
# reel payload includes everything
payload = client.get(base).json()
assert payload["script"] and payload["voice"] and payload["media"] and payload["renders"]
# project payload lists the reel
proj = client.get(f"/api/projects/{project_id}").json()
assert any(r["id"] == rid for r in proj["reels"])
def test_multiple_reels_via_api(project_id):
r1 = client.post(f"/api/projects/{project_id}/reels", json={"topic": "alpha"}).json()
r2 = client.post(f"/api/projects/{project_id}/reels", json={"topic": "beta"}).json()
reels = client.get(f"/api/projects/{project_id}/reels").json()
ids = {r["id"] for r in reels}
assert {r1["id"], r2["id"]} <= ids
assert client.delete(f"/api/projects/{project_id}/reels/{r1['id']}").json()["ok"]
remaining = {r["id"] for r in client.get(f"/api/projects/{project_id}/reels").json()}
assert r1["id"] not in remaining and r2["id"] in remaining
def test_clone_route(project_id):
job = client.post(f"/api/projects/{project_id}/clone",
json={"url": "https://www.youtube.com/shorts/abcdefghijk"}).json()
result = wait_job(job["job_id"])
assert result["reel"]["clone_source"].endswith("abcdefghijk")
assert result["script"]["clone_source"].endswith("abcdefghijk")
job = client.post(f"/api/projects/{project_id}/clone",
json={"url": "https://example.com/nope"}).json()
status = None
for _ in range(50):
status = client.get(f"/api/jobs/{job['job_id']}").json()
if status["status"] in ("done", "error"):
break
time.sleep(0.1)
assert status["status"] == "error" and "YouTube" in status["error"]
def test_bad_render_preset(project_id):
reel = client.post(f"/api/projects/{project_id}/reels", json={"topic": "x"}).json()
resp = client.post(f"/api/projects/{project_id}/reels/{reel['id']}/render", json={
"variants": [{"name": "X", "caption_preset": "nope", "clip_offset": 0}],
})
assert resp.status_code == 400