Spaces:
Paused
Paused
File size: 7,664 Bytes
3d2098f | 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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | """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
|