Implement FastAPI Temporal MVP pipeline
This commit is contained in:
44
tests/conftest.py
Normal file
44
tests/conftest.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Test fixtures for the Temporal demo."""
|
||||
|
||||
from contextlib import AsyncExitStack
|
||||
|
||||
import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from temporalio.testing import WorkflowEnvironment
|
||||
|
||||
from app.config.settings import get_settings
|
||||
from app.infra.db.session import dispose_database, init_database
|
||||
from app.infra.temporal.client import set_temporal_client
|
||||
from app.main import create_app
|
||||
from app.workers.runner import build_workers
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def api_runtime(tmp_path, monkeypatch):
|
||||
"""Provide an API client and in-memory Temporal test environment."""
|
||||
|
||||
db_path = tmp_path / "test.db"
|
||||
monkeypatch.setenv("DATABASE_URL", f"sqlite+aiosqlite:///{db_path.as_posix()}")
|
||||
monkeypatch.setenv("AUTO_CREATE_TABLES", "true")
|
||||
|
||||
get_settings.cache_clear()
|
||||
await dispose_database()
|
||||
await init_database()
|
||||
|
||||
app = create_app()
|
||||
|
||||
async with await WorkflowEnvironment.start_time_skipping() as env:
|
||||
set_temporal_client(env.client)
|
||||
async with AsyncExitStack() as stack:
|
||||
for worker in build_workers(env.client):
|
||||
await stack.enter_async_context(worker)
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=app),
|
||||
base_url="http://testserver",
|
||||
) as client:
|
||||
yield client, env
|
||||
|
||||
set_temporal_client(None)
|
||||
await dispose_database()
|
||||
get_settings.cache_clear()
|
||||
|
||||
178
tests/test_api.py
Normal file
178
tests/test_api.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""Integration tests for the FastAPI + Temporal MVP."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
async def wait_for_workflow_status(client, order_id: int, expected_status: str, attempts: int = 120):
|
||||
"""Poll the workflow status endpoint until it reaches a target status."""
|
||||
|
||||
last_payload = None
|
||||
for _ in range(attempts):
|
||||
response = await client.get(f"/api/v1/workflows/{order_id}")
|
||||
if response.status_code == 200:
|
||||
last_payload = response.json()
|
||||
if last_payload["workflow_status"] == expected_status:
|
||||
return last_payload
|
||||
await asyncio.sleep(0.05)
|
||||
raise AssertionError(f"Workflow {order_id} never reached status {expected_status!r}: {last_payload}")
|
||||
|
||||
|
||||
async def wait_for_step_count(client, order_id: int, step_name: str, minimum_count: int, attempts: int = 120):
|
||||
"""Poll until a workflow step has been recorded a minimum number of times."""
|
||||
|
||||
last_payload = None
|
||||
for _ in range(attempts):
|
||||
response = await client.get(f"/api/v1/workflows/{order_id}")
|
||||
if response.status_code == 200:
|
||||
last_payload = response.json()
|
||||
count = sum(1 for step in last_payload["steps"] if step["step_name"] == step_name)
|
||||
if count >= minimum_count:
|
||||
return last_payload
|
||||
await asyncio.sleep(0.05)
|
||||
raise AssertionError(
|
||||
f"Workflow {order_id} never recorded step {step_name!r} {minimum_count} times: {last_payload}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthcheck(api_runtime):
|
||||
"""The health endpoint should always respond successfully."""
|
||||
|
||||
client, _ = api_runtime
|
||||
response = await client.get("/healthz")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_low_end_order_completes(api_runtime):
|
||||
"""Low-end orders should run through the full automated pipeline."""
|
||||
|
||||
client, env = api_runtime
|
||||
response = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": 101,
|
||||
"pose_id": 3,
|
||||
"garment_asset_id": 9001,
|
||||
"scene_ref_asset_id": 8001,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
assert payload["workflow_id"] == f"order-{payload['order_id']}"
|
||||
|
||||
handle = env.client.get_workflow_handle(payload["workflow_id"])
|
||||
result = await handle.result()
|
||||
|
||||
assert result["status"] == "succeeded"
|
||||
|
||||
order_response = await client.get(f"/api/v1/orders/{payload['order_id']}")
|
||||
assert order_response.status_code == 200
|
||||
assert order_response.json()["status"] == "succeeded"
|
||||
|
||||
assets_response = await client.get(f"/api/v1/orders/{payload['order_id']}/assets")
|
||||
assert assets_response.status_code == 200
|
||||
assert any(asset["asset_type"] == "final" for asset in assets_response.json())
|
||||
|
||||
workflow_response = await client.get(f"/api/v1/workflows/{payload['order_id']}")
|
||||
assert workflow_response.status_code == 200
|
||||
assert workflow_response.json()["workflow_status"] == "succeeded"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mid_end_order_waits_review_then_approves(api_runtime):
|
||||
"""Mid-end orders should pause for review and continue after approval."""
|
||||
|
||||
client, env = api_runtime
|
||||
response = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "mid",
|
||||
"service_mode": "semi_pro",
|
||||
"model_id": 101,
|
||||
"pose_id": 3,
|
||||
"garment_asset_id": 9001,
|
||||
"scene_ref_asset_id": 8001,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
|
||||
await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
|
||||
pending_response = await client.get("/api/v1/reviews/pending")
|
||||
assert pending_response.status_code == 200
|
||||
assert any(item["order_id"] == payload["order_id"] for item in pending_response.json())
|
||||
|
||||
review_response = await client.post(
|
||||
f"/api/v1/reviews/{payload['order_id']}/submit",
|
||||
json={"decision": "approve", "reviewer_id": 77, "comment": "通过"},
|
||||
)
|
||||
assert review_response.status_code == 200
|
||||
|
||||
handle = env.client.get_workflow_handle(payload["workflow_id"])
|
||||
result = await handle.result()
|
||||
assert result["status"] == "succeeded"
|
||||
|
||||
order_response = await client.get(f"/api/v1/orders/{payload['order_id']}")
|
||||
assert order_response.status_code == 200
|
||||
assert order_response.json()["status"] == "succeeded"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("decision", "expected_step"),
|
||||
[
|
||||
("rerun_scene", "scene"),
|
||||
("rerun_face", "face"),
|
||||
("rerun_fusion", "fusion"),
|
||||
],
|
||||
)
|
||||
async def test_mid_end_rerun_paths_return_to_review(api_runtime, decision: str, expected_step: str):
|
||||
"""Each rerun decision should branch back to the correct step and pause again for review."""
|
||||
|
||||
client, env = api_runtime
|
||||
response = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "mid",
|
||||
"service_mode": "semi_pro",
|
||||
"model_id": 101,
|
||||
"pose_id": 3,
|
||||
"garment_asset_id": 9001,
|
||||
"scene_ref_asset_id": 8001,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
|
||||
await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
|
||||
review_response = await client.post(
|
||||
f"/api/v1/reviews/{payload['order_id']}/submit",
|
||||
json={"decision": decision, "reviewer_id": 77, "comment": f"trigger {decision}"},
|
||||
)
|
||||
assert review_response.status_code == 200
|
||||
|
||||
workflow_payload = await wait_for_step_count(client, payload["order_id"], expected_step, 2)
|
||||
workflow_payload = await wait_for_step_count(client, payload["order_id"], "review", 2)
|
||||
assert workflow_payload["workflow_status"] == "waiting_review"
|
||||
|
||||
approve_response = await client.post(
|
||||
f"/api/v1/reviews/{payload['order_id']}/submit",
|
||||
json={"decision": "approve", "reviewer_id": 77, "comment": "批准最终结果"},
|
||||
)
|
||||
assert approve_response.status_code == 200
|
||||
|
||||
handle = env.client.get_workflow_handle(payload["workflow_id"])
|
||||
result = await handle.result()
|
||||
assert result["status"] == "succeeded"
|
||||
Reference in New Issue
Block a user