diff --git a/music-lab/app/main.py b/music-lab/app/main.py index a5e7954..abcb005 100644 --- a/music-lab/app/main.py +++ b/music-lab/app/main.py @@ -1128,6 +1128,25 @@ def cancel_pipeline(pid: int): return {"ok": True} +@app.post("/api/music/pipeline/{pid}/retry", status_code=202) +async def retry_pipeline(pid: int, bg: BackgroundTasks): + p = _db_module.get_pipeline(pid) + if not p: + raise HTTPException(404) + if p["state"] != "failed": + raise HTTPException(409, f"재개 불가 (state={p['state']})") + failed_step = _db_module.get_last_failed_step(pid) + if not failed_step: + reason = p.get("failed_reason") or "" + failed_step = reason.split(":", 1)[0].strip() or None + if not failed_step: + raise HTTPException(409, "실패 step을 판별할 수 없음") + if failed_step == "publish" and p.get("youtube_video_id"): + raise HTTPException(409, "이미 업로드됨 (중복 방지)") + bg.add_task(orchestrator.run_step, pid, failed_step) + return {"ok": True, "retrying_step": failed_step} + + @app.post("/api/music/pipeline/{pid}/publish", status_code=202) async def publish_pipeline(pid: int, bg: BackgroundTasks): p = _db_module.get_pipeline(pid) diff --git a/music-lab/pytest.ini b/music-lab/pytest.ini index 654b7eb..bd5846d 100644 --- a/music-lab/pytest.ini +++ b/music-lab/pytest.ini @@ -1,4 +1,4 @@ [pytest] testpaths = tests -pythonpath = . +pythonpath = . .. asyncio_mode = auto diff --git a/music-lab/tests/test_pipeline_retry.py b/music-lab/tests/test_pipeline_retry.py index bb3482b..e729ddf 100644 --- a/music-lab/tests/test_pipeline_retry.py +++ b/music-lab/tests/test_pipeline_retry.py @@ -1,4 +1,5 @@ import pytest +from fastapi.testclient import TestClient from app import db from app.pipeline import orchestrator @@ -81,3 +82,43 @@ async def test_publish_not_retried(fresh_db, monkeypatch): await orchestrator.run_step(pid, "publish") assert calls["n"] == 1 assert db.get_pipeline(pid)["state"] == "failed" + + +# ── Task 3: retry endpoint tests ───────────────────────────────────────────── + +@pytest.fixture +def client(fresh_db): + from app.main import app + return TestClient(app) + + +def test_retry_failed_pipeline_retriggers(fresh_db, client, monkeypatch): + pid = db.create_pipeline(track_id=1) + job = db.create_pipeline_job(pid, "video") + db.update_pipeline_job(job, status="failed", error="boom") + db.update_pipeline_state(pid, "failed", failed_reason="video: boom") + called = {} + + async def fake_run(p, step, *a): + called["pid"], called["step"] = p, step + + monkeypatch.setattr(orchestrator, "run_step", fake_run) + r = client.post(f"/api/music/pipeline/{pid}/retry") + assert r.status_code in (200, 202) + assert r.json()["retrying_step"] == "video" + + +def test_retry_non_failed_409(fresh_db, client): + pid = db.create_pipeline(track_id=1) # state='created' + r = client.post(f"/api/music/pipeline/{pid}/retry") + assert r.status_code == 409 + + +def test_retry_publish_with_video_id_rejected(fresh_db, client): + pid = db.create_pipeline(track_id=1) + job = db.create_pipeline_job(pid, "publish") + db.update_pipeline_job(job, status="failed", error="x") + db.update_pipeline_state(pid, "failed", failed_reason="publish: x", + youtube_video_id="abc123") + r = client.post(f"/api/music/pipeline/{pid}/retry") + assert r.status_code == 409