feat: add resource library and real image workflow
This commit is contained in:
@@ -20,6 +20,13 @@ async def api_runtime(tmp_path, monkeypatch):
|
||||
db_path = tmp_path / "test.db"
|
||||
monkeypatch.setenv("DATABASE_URL", f"sqlite+aiosqlite:///{db_path.as_posix()}")
|
||||
monkeypatch.setenv("AUTO_CREATE_TABLES", "true")
|
||||
monkeypatch.setenv("S3_ACCESS_KEY", "test-access")
|
||||
monkeypatch.setenv("S3_SECRET_KEY", "test-secret")
|
||||
monkeypatch.setenv("S3_BUCKET", "test-bucket")
|
||||
monkeypatch.setenv("S3_REGION", "ap-southeast-1")
|
||||
monkeypatch.setenv("S3_ENDPOINT", "https://s3.example.com")
|
||||
monkeypatch.setenv("S3_CNAME", "images.example.com")
|
||||
monkeypatch.setenv("IMAGE_GENERATION_PROVIDER", "mock")
|
||||
|
||||
get_settings.cache_clear()
|
||||
await dispose_database()
|
||||
@@ -41,4 +48,3 @@ async def api_runtime(tmp_path, monkeypatch):
|
||||
set_temporal_client(None)
|
||||
await dispose_database()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
@@ -39,15 +39,15 @@ async def wait_for_step_count(client, order_id: int, step_name: str, minimum_cou
|
||||
async def create_mid_end_order(client):
|
||||
"""Create a standard semi-pro order for review-path tests."""
|
||||
|
||||
resources = await create_workflow_resources(client, include_scene=True)
|
||||
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,
|
||||
"model_id": resources["model"]["id"],
|
||||
"garment_asset_id": resources["garment"]["id"],
|
||||
"scene_ref_asset_id": resources["scene"]["id"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -55,6 +55,115 @@ async def create_mid_end_order(client):
|
||||
return response.json()
|
||||
|
||||
|
||||
async def create_library_resource(client, payload: dict) -> dict:
|
||||
"""Create a resource-library item for workflow integration tests."""
|
||||
|
||||
response = await client.post("/api/v1/library/resources", json=payload)
|
||||
assert response.status_code == 201
|
||||
return response.json()
|
||||
|
||||
|
||||
async def create_workflow_resources(client, *, include_scene: bool) -> dict[str, dict]:
|
||||
"""Create a minimal set of real library resources for workflow tests."""
|
||||
|
||||
model_resource = await create_library_resource(
|
||||
client,
|
||||
{
|
||||
"resource_type": "model",
|
||||
"name": "Ava Studio",
|
||||
"description": "棚拍女模特",
|
||||
"tags": ["女装", "棚拍"],
|
||||
"gender": "female",
|
||||
"age_group": "adult",
|
||||
"files": [
|
||||
{
|
||||
"file_role": "original",
|
||||
"storage_key": "library/models/ava/original.png",
|
||||
"public_url": "https://images.example.com/library/models/ava/original.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 1024,
|
||||
"sort_order": 0,
|
||||
"width": 1200,
|
||||
"height": 1600,
|
||||
},
|
||||
{
|
||||
"file_role": "thumbnail",
|
||||
"storage_key": "library/models/ava/thumb.png",
|
||||
"public_url": "https://images.example.com/library/models/ava/thumb.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 256,
|
||||
"sort_order": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
garment_resource = await create_library_resource(
|
||||
client,
|
||||
{
|
||||
"resource_type": "garment",
|
||||
"name": "Cream Dress",
|
||||
"description": "米白色连衣裙",
|
||||
"tags": ["女装"],
|
||||
"category": "dress",
|
||||
"files": [
|
||||
{
|
||||
"file_role": "original",
|
||||
"storage_key": "library/garments/cream-dress/original.png",
|
||||
"public_url": "https://images.example.com/library/garments/cream-dress/original.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 1024,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"file_role": "thumbnail",
|
||||
"storage_key": "library/garments/cream-dress/thumb.png",
|
||||
"public_url": "https://images.example.com/library/garments/cream-dress/thumb.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 256,
|
||||
"sort_order": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
resources = {
|
||||
"model": model_resource,
|
||||
"garment": garment_resource,
|
||||
}
|
||||
|
||||
if include_scene:
|
||||
resources["scene"] = await create_library_resource(
|
||||
client,
|
||||
{
|
||||
"resource_type": "scene",
|
||||
"name": "Loft Window",
|
||||
"description": "暖调室内场景",
|
||||
"tags": ["室内"],
|
||||
"environment": "indoor",
|
||||
"files": [
|
||||
{
|
||||
"file_role": "original",
|
||||
"storage_key": "library/scenes/loft/original.png",
|
||||
"public_url": "https://images.example.com/library/scenes/loft/original.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 1024,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"file_role": "thumbnail",
|
||||
"storage_key": "library/scenes/loft/thumb.png",
|
||||
"public_url": "https://images.example.com/library/scenes/loft/thumb.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 256,
|
||||
"sort_order": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
return resources
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthcheck(api_runtime):
|
||||
"""The health endpoint should always respond successfully."""
|
||||
@@ -71,15 +180,15 @@ async def test_low_end_order_completes(api_runtime):
|
||||
"""Low-end orders should run through the full automated pipeline."""
|
||||
|
||||
client, env = api_runtime
|
||||
resources = await create_workflow_resources(client, include_scene=True)
|
||||
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,
|
||||
"model_id": resources["model"]["id"],
|
||||
"garment_asset_id": resources["garment"]["id"],
|
||||
"scene_ref_asset_id": resources["scene"]["id"],
|
||||
},
|
||||
)
|
||||
|
||||
@@ -105,6 +214,160 @@ async def test_low_end_order_completes(api_runtime):
|
||||
assert workflow_response.json()["workflow_status"] == "succeeded"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_model_step_uses_library_original_file(api_runtime):
|
||||
"""prepare_model should use the model resource's original file rather than a mock URI."""
|
||||
|
||||
def expected_input_snapshot(resource: dict) -> dict:
|
||||
original_file = next(file for file in resource["files"] if file["file_role"] == "original")
|
||||
snapshot = {
|
||||
"resource_id": resource["id"],
|
||||
"resource_name": resource["name"],
|
||||
"original_file_id": original_file["id"],
|
||||
"original_url": resource["original_url"],
|
||||
"mime_type": original_file["mime_type"],
|
||||
"width": original_file["width"],
|
||||
"height": original_file["height"],
|
||||
}
|
||||
return {key: value for key, value in snapshot.items() if value is not None}
|
||||
|
||||
client, env = api_runtime
|
||||
resources = await create_workflow_resources(client, include_scene=True)
|
||||
model_resource = resources["model"]
|
||||
garment_resource = resources["garment"]
|
||||
scene_resource = resources["scene"]
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": model_resource["id"],
|
||||
"garment_asset_id": garment_resource["id"],
|
||||
"scene_ref_asset_id": scene_resource["id"],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
|
||||
handle = env.client.get_workflow_handle(payload["workflow_id"])
|
||||
result = await handle.result()
|
||||
assert result["status"] == "succeeded"
|
||||
|
||||
assets_response = await client.get(f"/api/v1/orders/{payload['order_id']}/assets")
|
||||
assert assets_response.status_code == 200
|
||||
prepared_asset = next(
|
||||
asset for asset in assets_response.json() if asset["asset_type"] == "prepared_model"
|
||||
)
|
||||
assert prepared_asset["uri"] == model_resource["original_url"]
|
||||
assert prepared_asset["metadata_json"]["library_resource_id"] == model_resource["id"]
|
||||
assert prepared_asset["metadata_json"]["library_original_file_id"] == next(
|
||||
file["id"] for file in model_resource["files"] if file["file_role"] == "original"
|
||||
)
|
||||
assert prepared_asset["metadata_json"]["library_original_url"] == model_resource["original_url"]
|
||||
assert prepared_asset["metadata_json"]["model_input"] == expected_input_snapshot(model_resource)
|
||||
assert prepared_asset["metadata_json"]["garment_input"] == expected_input_snapshot(garment_resource)
|
||||
assert prepared_asset["metadata_json"]["scene_input"] == expected_input_snapshot(scene_resource)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_low_end_order_completes_without_scene(api_runtime):
|
||||
"""Low-end orders should still complete when scene input is omitted."""
|
||||
|
||||
client, env = api_runtime
|
||||
resources = await create_workflow_resources(client, include_scene=False)
|
||||
response = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": resources["model"]["id"],
|
||||
"garment_asset_id": resources["garment"]["id"],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
|
||||
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()["scene_ref_asset_id"] is None
|
||||
assert order_response.json()["status"] == "succeeded"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_low_end_order_reuses_tryon_uri_for_qc_and_final_without_scene(api_runtime):
|
||||
"""Without scene input, qc/export should keep the real try-on image URI."""
|
||||
|
||||
client, env = api_runtime
|
||||
resources = await create_workflow_resources(client, include_scene=False)
|
||||
response = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": resources["model"]["id"],
|
||||
"garment_asset_id": resources["garment"]["id"],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
payload = response.json()
|
||||
|
||||
handle = env.client.get_workflow_handle(payload["workflow_id"])
|
||||
result = await handle.result()
|
||||
assert result["status"] == "succeeded"
|
||||
|
||||
assets_response = await client.get(f"/api/v1/orders/{payload['order_id']}/assets")
|
||||
assert assets_response.status_code == 200
|
||||
assets = assets_response.json()
|
||||
|
||||
tryon_asset = next(asset for asset in assets if asset["asset_type"] == "tryon")
|
||||
qc_asset = next(asset for asset in assets if asset["asset_type"] == "qc_candidate")
|
||||
final_asset = next(asset for asset in assets if asset["asset_type"] == "final")
|
||||
|
||||
assert qc_asset["uri"] == tryon_asset["uri"]
|
||||
assert final_asset["uri"] == tryon_asset["uri"]
|
||||
assert final_asset["metadata_json"]["source_asset_id"] == qc_asset["id"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mid_end_order_waits_review_then_approves_without_scene(api_runtime):
|
||||
"""Mid-end orders should still reach review and approve when scene input is omitted."""
|
||||
|
||||
client, env = api_runtime
|
||||
resources = await create_workflow_resources(client, include_scene=False)
|
||||
response = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "mid",
|
||||
"service_mode": "semi_pro",
|
||||
"model_id": resources["model"]["id"],
|
||||
"garment_asset_id": resources["garment"]["id"],
|
||||
},
|
||||
)
|
||||
|
||||
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": "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"
|
||||
|
||||
|
||||
@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."""
|
||||
@@ -133,6 +396,162 @@ async def test_mid_end_order_waits_review_then_approves(api_runtime):
|
||||
assert order_response.json()["status"] == "succeeded"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_library_upload_presign_returns_direct_upload_metadata(api_runtime):
|
||||
"""Library upload presign should return S3 direct-upload metadata and a public URL."""
|
||||
|
||||
client, _ = api_runtime
|
||||
response = await client.post(
|
||||
"/api/v1/library/uploads/presign",
|
||||
json={
|
||||
"resource_type": "model",
|
||||
"file_role": "original",
|
||||
"file_name": "ava.png",
|
||||
"content_type": "image/png",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["method"] == "PUT"
|
||||
assert payload["storage_key"].startswith("library/models/")
|
||||
assert payload["public_url"].startswith("https://")
|
||||
assert "library/models/" in payload["upload_url"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_library_resource_can_be_created_and_listed(api_runtime):
|
||||
"""Creating a library resource should persist original, thumbnail, and gallery files."""
|
||||
|
||||
client, _ = api_runtime
|
||||
create_response = await client.post(
|
||||
"/api/v1/library/resources",
|
||||
json={
|
||||
"resource_type": "model",
|
||||
"name": "Ava Studio",
|
||||
"description": "棚拍女模特",
|
||||
"tags": ["女装", "棚拍"],
|
||||
"gender": "female",
|
||||
"age_group": "adult",
|
||||
"files": [
|
||||
{
|
||||
"file_role": "original",
|
||||
"storage_key": "library/models/ava/original.png",
|
||||
"public_url": "https://images.marcusd.me/library/models/ava/original.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 1024,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"file_role": "thumbnail",
|
||||
"storage_key": "library/models/ava/thumb.png",
|
||||
"public_url": "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 256,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"file_role": "gallery",
|
||||
"storage_key": "library/models/ava/gallery-1.png",
|
||||
"public_url": "https://images.marcusd.me/library/models/ava/gallery-1.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 2048,
|
||||
"sort_order": 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert create_response.status_code == 201
|
||||
created = create_response.json()
|
||||
assert created["resource_type"] == "model"
|
||||
assert created["pose_id"] is None
|
||||
assert created["cover_url"] == "https://images.marcusd.me/library/models/ava/thumb.png"
|
||||
assert created["original_url"] == "https://images.marcusd.me/library/models/ava/original.png"
|
||||
assert len(created["files"]) == 3
|
||||
|
||||
list_response = await client.get("/api/v1/library/resources", params={"resource_type": "model"})
|
||||
assert list_response.status_code == 200
|
||||
listing = list_response.json()
|
||||
assert listing["total"] == 1
|
||||
assert listing["items"][0]["name"] == "Ava Studio"
|
||||
assert listing["items"][0]["gender"] == "female"
|
||||
assert listing["items"][0]["pose_id"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_library_resource_list_supports_type_specific_filters(api_runtime):
|
||||
"""Library listing should support resource-type-specific filter fields."""
|
||||
|
||||
client, _ = api_runtime
|
||||
|
||||
payloads = [
|
||||
{
|
||||
"resource_type": "scene",
|
||||
"name": "Loft Window",
|
||||
"description": "暖调室内场景",
|
||||
"tags": ["室内"],
|
||||
"environment": "indoor",
|
||||
"files": [
|
||||
{
|
||||
"file_role": "original",
|
||||
"storage_key": "library/scenes/loft/original.png",
|
||||
"public_url": "https://images.marcusd.me/library/scenes/loft/original.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 1024,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"file_role": "thumbnail",
|
||||
"storage_key": "library/scenes/loft/thumb.png",
|
||||
"public_url": "https://images.marcusd.me/library/scenes/loft/thumb.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 256,
|
||||
"sort_order": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"resource_type": "scene",
|
||||
"name": "Garden Walk",
|
||||
"description": "自然光室外场景",
|
||||
"tags": ["室外"],
|
||||
"environment": "outdoor",
|
||||
"files": [
|
||||
{
|
||||
"file_role": "original",
|
||||
"storage_key": "library/scenes/garden/original.png",
|
||||
"public_url": "https://images.marcusd.me/library/scenes/garden/original.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 1024,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"file_role": "thumbnail",
|
||||
"storage_key": "library/scenes/garden/thumb.png",
|
||||
"public_url": "https://images.marcusd.me/library/scenes/garden/thumb.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 256,
|
||||
"sort_order": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
for payload in payloads:
|
||||
response = await client.post("/api/v1/library/resources", json=payload)
|
||||
assert response.status_code == 201
|
||||
|
||||
indoor_response = await client.get(
|
||||
"/api/v1/library/resources",
|
||||
params={"resource_type": "scene", "environment": "indoor"},
|
||||
)
|
||||
assert indoor_response.status_code == 200
|
||||
indoor_payload = indoor_response.json()
|
||||
assert indoor_payload["total"] == 1
|
||||
assert indoor_payload["items"][0]["name"] == "Loft Window"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("decision", "expected_step"),
|
||||
@@ -312,15 +731,16 @@ async def test_orders_list_returns_recent_orders_with_revision_summary(api_runti
|
||||
|
||||
client, env = api_runtime
|
||||
|
||||
low_resources = await create_workflow_resources(client, include_scene=True)
|
||||
low_order = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": 201,
|
||||
"model_id": low_resources["model"]["id"],
|
||||
"pose_id": 11,
|
||||
"garment_asset_id": 9101,
|
||||
"scene_ref_asset_id": 8101,
|
||||
"garment_asset_id": low_resources["garment"]["id"],
|
||||
"scene_ref_asset_id": low_resources["scene"]["id"],
|
||||
},
|
||||
)
|
||||
assert low_order.status_code == 201
|
||||
@@ -394,15 +814,16 @@ async def test_workflows_list_returns_recent_runs_with_failure_count(api_runtime
|
||||
|
||||
client, env = api_runtime
|
||||
|
||||
low_resources = await create_workflow_resources(client, include_scene=True)
|
||||
low_order = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": 301,
|
||||
"model_id": low_resources["model"]["id"],
|
||||
"pose_id": 21,
|
||||
"garment_asset_id": 9201,
|
||||
"scene_ref_asset_id": 8201,
|
||||
"garment_asset_id": low_resources["garment"]["id"],
|
||||
"scene_ref_asset_id": low_resources["scene"]["id"],
|
||||
},
|
||||
)
|
||||
assert low_order.status_code == 201
|
||||
|
||||
150
tests/test_gemini_provider.py
Normal file
150
tests/test_gemini_provider.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Tests for the Gemini image provider."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.infra.image_generation.base import SourceImage
|
||||
from app.infra.image_generation.gemini_provider import GeminiImageProvider
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_provider_uses_configured_base_url_and_model():
|
||||
"""The provider should call the configured endpoint and decode the returned image bytes."""
|
||||
|
||||
expected_url = "https://gemini.example.com/custom/models/gemini-test-model:generateContent"
|
||||
png_bytes = b"generated-png"
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
assert str(request.url) == expected_url
|
||||
assert request.headers["x-goog-api-key"] == "test-key"
|
||||
payload = request.read().decode("utf-8")
|
||||
assert "gemini-test-model" not in payload
|
||||
assert "Dress the person in the garment" in payload
|
||||
assert "cGVyc29uLWJ5dGVz" in payload
|
||||
assert "Z2FybWVudC1ieXRlcw==" in payload
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{"text": "done"},
|
||||
{
|
||||
"inlineData": {
|
||||
"mimeType": "image/png",
|
||||
"data": base64.b64encode(png_bytes).decode("utf-8"),
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
provider = GeminiImageProvider(
|
||||
api_key="test-key",
|
||||
base_url="https://gemini.example.com/custom",
|
||||
model="gemini-test-model",
|
||||
timeout_seconds=5,
|
||||
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
|
||||
)
|
||||
|
||||
try:
|
||||
result = await provider.generate_tryon_image(
|
||||
prompt="Dress the person in the garment",
|
||||
person_image=SourceImage(
|
||||
url="https://images.example.com/person.png",
|
||||
mime_type="image/png",
|
||||
data=b"person-bytes",
|
||||
),
|
||||
garment_image=SourceImage(
|
||||
url="https://images.example.com/garment.png",
|
||||
mime_type="image/png",
|
||||
data=b"garment-bytes",
|
||||
),
|
||||
)
|
||||
finally:
|
||||
await provider._http_client.aclose()
|
||||
|
||||
assert result.image_bytes == png_bytes
|
||||
assert result.mime_type == "image/png"
|
||||
assert result.provider == "gemini"
|
||||
assert result.model == "gemini-test-model"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_gemini_provider_retries_remote_disconnect_and_uses_request_timeout():
|
||||
"""The provider should retry transient transport failures and pass timeout per request."""
|
||||
|
||||
expected_url = "https://gemini.example.com/custom/models/gemini-test-model:generateContent"
|
||||
jpeg_bytes = b"generated-jpeg"
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self) -> None:
|
||||
self.attempts = 0
|
||||
self.timeouts: list[int | None] = []
|
||||
|
||||
async def post(self, url: str, **kwargs) -> httpx.Response:
|
||||
self.attempts += 1
|
||||
self.timeouts.append(kwargs.get("timeout"))
|
||||
assert url == expected_url
|
||||
if self.attempts == 1:
|
||||
raise httpx.RemoteProtocolError("Server disconnected without sending a response.")
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"inlineData": {
|
||||
"mimeType": "image/jpeg",
|
||||
"data": base64.b64encode(jpeg_bytes).decode("utf-8"),
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
request=httpx.Request("POST", url),
|
||||
)
|
||||
|
||||
async def aclose(self) -> None:
|
||||
return None
|
||||
|
||||
fake_client = FakeClient()
|
||||
|
||||
provider = GeminiImageProvider(
|
||||
api_key="test-key",
|
||||
base_url="https://gemini.example.com/custom",
|
||||
model="gemini-test-model",
|
||||
timeout_seconds=300,
|
||||
http_client=fake_client,
|
||||
)
|
||||
|
||||
result = await provider.generate_tryon_image(
|
||||
prompt="Dress the person in the garment",
|
||||
person_image=SourceImage(
|
||||
url="https://images.example.com/person.png",
|
||||
mime_type="image/png",
|
||||
data=b"person-bytes",
|
||||
),
|
||||
garment_image=SourceImage(
|
||||
url="https://images.example.com/garment.png",
|
||||
mime_type="image/png",
|
||||
data=b"garment-bytes",
|
||||
),
|
||||
)
|
||||
|
||||
assert fake_client.attempts == 2
|
||||
assert fake_client.timeouts == [300, 300]
|
||||
assert result.image_bytes == jpeg_bytes
|
||||
assert result.mime_type == "image/jpeg"
|
||||
91
tests/test_image_generation_service.py
Normal file
91
tests/test_image_generation_service.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Tests for application-level image-generation orchestration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.application.services.image_generation_service import ImageGenerationService
|
||||
from app.infra.image_generation.base import GeneratedImageResult, SourceImage
|
||||
|
||||
|
||||
class FakeProvider:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, str, str]] = []
|
||||
|
||||
async def generate_tryon_image(
|
||||
self,
|
||||
*,
|
||||
prompt: str,
|
||||
person_image: SourceImage,
|
||||
garment_image: SourceImage,
|
||||
) -> GeneratedImageResult:
|
||||
self.calls.append(("tryon", person_image.url, garment_image.url))
|
||||
return GeneratedImageResult(
|
||||
image_bytes=b"tryon",
|
||||
mime_type="image/png",
|
||||
provider="gemini",
|
||||
model="gemini-test",
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
async def generate_scene_image(
|
||||
self,
|
||||
*,
|
||||
prompt: str,
|
||||
source_image: SourceImage,
|
||||
scene_image: SourceImage,
|
||||
) -> GeneratedImageResult:
|
||||
self.calls.append(("scene", source_image.url, scene_image.url))
|
||||
return GeneratedImageResult(
|
||||
image_bytes=b"scene",
|
||||
mime_type="image/jpeg",
|
||||
provider="gemini",
|
||||
model="gemini-test",
|
||||
prompt=prompt,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_image_generation_service_downloads_inputs_for_tryon_and_scene(monkeypatch):
|
||||
"""The service should download both inputs and dispatch them to the matching provider method."""
|
||||
|
||||
responses = {
|
||||
"https://images.example.com/person.png": (b"person-bytes", "image/png"),
|
||||
"https://images.example.com/garment.png": (b"garment-bytes", "image/png"),
|
||||
"https://images.example.com/source.jpg": (b"source-bytes", "image/jpeg"),
|
||||
"https://images.example.com/scene.jpg": (b"scene-bytes", "image/jpeg"),
|
||||
}
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
body, mime = responses[str(request.url)]
|
||||
return httpx.Response(200, content=body, headers={"content-type": mime}, request=request)
|
||||
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "test-key")
|
||||
|
||||
from app.config.settings import get_settings
|
||||
|
||||
get_settings.cache_clear()
|
||||
provider = FakeProvider()
|
||||
downloader = httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
||||
service = ImageGenerationService(provider=provider, downloader=downloader)
|
||||
|
||||
try:
|
||||
tryon = await service.generate_tryon_image(
|
||||
person_image_url="https://images.example.com/person.png",
|
||||
garment_image_url="https://images.example.com/garment.png",
|
||||
)
|
||||
scene = await service.generate_scene_image(
|
||||
source_image_url="https://images.example.com/source.jpg",
|
||||
scene_image_url="https://images.example.com/scene.jpg",
|
||||
)
|
||||
finally:
|
||||
await downloader.aclose()
|
||||
get_settings.cache_clear()
|
||||
|
||||
assert provider.calls == [
|
||||
("tryon", "https://images.example.com/person.png", "https://images.example.com/garment.png"),
|
||||
("scene", "https://images.example.com/source.jpg", "https://images.example.com/scene.jpg"),
|
||||
]
|
||||
assert tryon.image_bytes == b"tryon"
|
||||
assert scene.image_bytes == b"scene"
|
||||
63
tests/test_library_resource_actions.py
Normal file
63
tests/test_library_resource_actions.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_library_resource_can_be_updated_and_archived(api_runtime):
|
||||
client, _ = api_runtime
|
||||
|
||||
create_response = await client.post(
|
||||
"/api/v1/library/resources",
|
||||
json={
|
||||
"resource_type": "model",
|
||||
"name": "Ava Studio",
|
||||
"description": "棚拍女模特",
|
||||
"tags": ["女装", "棚拍"],
|
||||
"gender": "female",
|
||||
"age_group": "adult",
|
||||
"files": [
|
||||
{
|
||||
"file_role": "original",
|
||||
"storage_key": "library/models/ava/original.png",
|
||||
"public_url": "https://images.marcusd.me/library/models/ava/original.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 1024,
|
||||
"sort_order": 0,
|
||||
},
|
||||
{
|
||||
"file_role": "thumbnail",
|
||||
"storage_key": "library/models/ava/thumb.png",
|
||||
"public_url": "https://images.marcusd.me/library/models/ava/thumb.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 256,
|
||||
"sort_order": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert create_response.status_code == 201
|
||||
resource = create_response.json()
|
||||
|
||||
update_response = await client.patch(
|
||||
f"/api/v1/library/resources/{resource['id']}",
|
||||
json={
|
||||
"name": "Ava Studio Updated",
|
||||
"description": "新的描述",
|
||||
"tags": ["女装", "更新"],
|
||||
},
|
||||
)
|
||||
|
||||
assert update_response.status_code == 200
|
||||
updated = update_response.json()
|
||||
assert updated["name"] == "Ava Studio Updated"
|
||||
assert updated["description"] == "新的描述"
|
||||
assert updated["tags"] == ["女装", "更新"]
|
||||
|
||||
archive_response = await client.delete(f"/api/v1/library/resources/{resource['id']}")
|
||||
|
||||
assert archive_response.status_code == 200
|
||||
assert archive_response.json()["id"] == resource["id"]
|
||||
|
||||
list_response = await client.get("/api/v1/library/resources", params={"resource_type": "model"})
|
||||
assert list_response.status_code == 200
|
||||
assert list_response.json()["items"] == []
|
||||
15
tests/test_settings.py
Normal file
15
tests/test_settings.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Tests for application settings resolution."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from app.config.settings import Settings
|
||||
|
||||
|
||||
def test_settings_env_file_uses_backend_repo_path() -> None:
|
||||
"""Settings should resolve .env relative to the backend repo, not the launch cwd."""
|
||||
|
||||
env_file = Settings.model_config.get("env_file")
|
||||
|
||||
assert isinstance(env_file, Path)
|
||||
assert env_file.is_absolute()
|
||||
assert env_file.name == ".env"
|
||||
298
tests/test_tryon_activity.py
Normal file
298
tests/test_tryon_activity.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""Focused tests for the try-on activity implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from app.domain.enums import AssetType, LibraryFileRole, LibraryResourceStatus, LibraryResourceType, OrderStatus, WorkflowStepName
|
||||
from app.infra.db.models.asset import AssetORM
|
||||
from app.infra.db.models.library_resource import LibraryResourceORM
|
||||
from app.infra.db.models.library_resource_file import LibraryResourceFileORM
|
||||
from app.infra.db.models.order import OrderORM
|
||||
from app.infra.db.models.workflow_run import WorkflowRunORM
|
||||
from app.infra.db.session import get_session_factory
|
||||
from app.workers.activities import tryon_activities
|
||||
from app.workers.workflows.types import StepActivityInput
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FakeGeneratedImage:
|
||||
image_bytes: bytes
|
||||
mime_type: str
|
||||
provider: str
|
||||
model: str
|
||||
prompt: str
|
||||
|
||||
|
||||
class FakeImageGenerationService:
|
||||
async def generate_tryon_image(self, *, person_image_url: str, garment_image_url: str) -> FakeGeneratedImage:
|
||||
assert person_image_url == "https://images.example.com/orders/1/prepared-model.png"
|
||||
assert garment_image_url == "https://images.example.com/library/garments/cream-dress/original.png"
|
||||
return FakeGeneratedImage(
|
||||
image_bytes=b"fake-png-binary",
|
||||
mime_type="image/png",
|
||||
provider="gemini",
|
||||
model="gemini-test-image",
|
||||
prompt="test prompt",
|
||||
)
|
||||
|
||||
async def generate_scene_image(self, *, source_image_url: str, scene_image_url: str) -> FakeGeneratedImage:
|
||||
assert source_image_url == "https://images.example.com/orders/1/tryon/generated.png"
|
||||
assert scene_image_url == "https://images.example.com/library/scenes/studio/original.png"
|
||||
return FakeGeneratedImage(
|
||||
image_bytes=b"fake-scene-binary",
|
||||
mime_type="image/jpeg",
|
||||
provider="gemini",
|
||||
model="gemini-test-image",
|
||||
prompt="scene prompt",
|
||||
)
|
||||
|
||||
|
||||
class FakeOrderArtifactStorageService:
|
||||
async def upload_generated_image(
|
||||
self,
|
||||
*,
|
||||
order_id: int,
|
||||
step_name: WorkflowStepName,
|
||||
image_bytes: bytes,
|
||||
mime_type: str,
|
||||
) -> tuple[str, str]:
|
||||
assert order_id == 1
|
||||
assert step_name == WorkflowStepName.TRYON
|
||||
assert image_bytes == b"fake-png-binary"
|
||||
assert mime_type == "image/png"
|
||||
return (
|
||||
"orders/1/tryon/generated.png",
|
||||
"https://images.example.com/orders/1/tryon/generated.png",
|
||||
)
|
||||
|
||||
|
||||
class FakeSceneArtifactStorageService:
|
||||
async def upload_generated_image(
|
||||
self,
|
||||
*,
|
||||
order_id: int,
|
||||
step_name: WorkflowStepName,
|
||||
image_bytes: bytes,
|
||||
mime_type: str,
|
||||
) -> tuple[str, str]:
|
||||
assert order_id == 1
|
||||
assert step_name == WorkflowStepName.SCENE
|
||||
assert image_bytes == b"fake-scene-binary"
|
||||
assert mime_type == "image/jpeg"
|
||||
return (
|
||||
"orders/1/scene/generated.jpg",
|
||||
"https://images.example.com/orders/1/scene/generated.jpg",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_tryon_activity_persists_uploaded_asset_with_provider_metadata(api_runtime, monkeypatch):
|
||||
"""Gemini-mode try-on should persist the uploaded output URL instead of a mock URI."""
|
||||
|
||||
monkeypatch.setattr(
|
||||
tryon_activities,
|
||||
"get_image_generation_service",
|
||||
lambda: FakeImageGenerationService(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
tryon_activities,
|
||||
"get_order_artifact_storage_service",
|
||||
lambda: FakeOrderArtifactStorageService(),
|
||||
)
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
order = OrderORM(
|
||||
customer_level="low",
|
||||
service_mode="auto_basic",
|
||||
status=OrderStatus.CREATED,
|
||||
model_id=1,
|
||||
garment_asset_id=2,
|
||||
)
|
||||
session.add(order)
|
||||
await session.flush()
|
||||
|
||||
workflow_run = WorkflowRunORM(
|
||||
order_id=order.id,
|
||||
workflow_id=f"order-{order.id}",
|
||||
workflow_type="LowEndPipelineWorkflow",
|
||||
status=OrderStatus.CREATED,
|
||||
)
|
||||
session.add(workflow_run)
|
||||
await session.flush()
|
||||
|
||||
prepared_asset = AssetORM(
|
||||
order_id=order.id,
|
||||
asset_type=AssetType.PREPARED_MODEL,
|
||||
step_name=WorkflowStepName.PREPARE_MODEL,
|
||||
uri="https://images.example.com/orders/1/prepared-model.png",
|
||||
metadata_json={"library_resource_id": 1},
|
||||
)
|
||||
session.add(prepared_asset)
|
||||
|
||||
garment_resource = LibraryResourceORM(
|
||||
resource_type=LibraryResourceType.GARMENT,
|
||||
name="Cream Dress",
|
||||
description="米白色连衣裙",
|
||||
tags=["女装"],
|
||||
status=LibraryResourceStatus.ACTIVE,
|
||||
category="dress",
|
||||
)
|
||||
session.add(garment_resource)
|
||||
await session.flush()
|
||||
|
||||
garment_original = LibraryResourceFileORM(
|
||||
resource_id=garment_resource.id,
|
||||
file_role=LibraryFileRole.ORIGINAL,
|
||||
storage_key="library/garments/cream-dress/original.png",
|
||||
public_url="https://images.example.com/library/garments/cream-dress/original.png",
|
||||
bucket="test-bucket",
|
||||
mime_type="image/png",
|
||||
size_bytes=1024,
|
||||
sort_order=0,
|
||||
)
|
||||
garment_thumb = LibraryResourceFileORM(
|
||||
resource_id=garment_resource.id,
|
||||
file_role=LibraryFileRole.THUMBNAIL,
|
||||
storage_key="library/garments/cream-dress/thumb.png",
|
||||
public_url="https://images.example.com/library/garments/cream-dress/thumb.png",
|
||||
bucket="test-bucket",
|
||||
mime_type="image/png",
|
||||
size_bytes=256,
|
||||
sort_order=0,
|
||||
)
|
||||
session.add_all([garment_original, garment_thumb])
|
||||
await session.flush()
|
||||
garment_resource.original_file_id = garment_original.id
|
||||
garment_resource.cover_file_id = garment_thumb.id
|
||||
await session.commit()
|
||||
|
||||
payload = StepActivityInput(
|
||||
order_id=1,
|
||||
workflow_run_id=1,
|
||||
step_name=WorkflowStepName.TRYON,
|
||||
source_asset_id=1,
|
||||
garment_asset_id=1,
|
||||
)
|
||||
|
||||
result = await tryon_activities.run_tryon_activity(payload)
|
||||
|
||||
assert result.uri == "https://images.example.com/orders/1/tryon/generated.png"
|
||||
assert result.metadata["provider"] == "gemini"
|
||||
assert result.metadata["model"] == "gemini-test-image"
|
||||
assert result.metadata["prepared_asset_id"] == 1
|
||||
assert result.metadata["garment_resource_id"] == 1
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
assets = (await session.execute(
|
||||
AssetORM.__table__.select().where(AssetORM.order_id == 1, AssetORM.asset_type == AssetType.TRYON)
|
||||
)).mappings().all()
|
||||
|
||||
assert len(assets) == 1
|
||||
assert assets[0]["uri"] == "https://images.example.com/orders/1/tryon/generated.png"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_scene_activity_persists_uploaded_asset_with_provider_metadata(api_runtime, monkeypatch):
|
||||
"""Gemini-mode scene should persist the uploaded output URL instead of a mock URI."""
|
||||
|
||||
from app.workers.activities import scene_activities
|
||||
|
||||
monkeypatch.setattr(
|
||||
scene_activities,
|
||||
"get_image_generation_service",
|
||||
lambda: FakeImageGenerationService(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
scene_activities,
|
||||
"get_order_artifact_storage_service",
|
||||
lambda: FakeSceneArtifactStorageService(),
|
||||
)
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
order = OrderORM(
|
||||
customer_level="low",
|
||||
service_mode="auto_basic",
|
||||
status=OrderStatus.CREATED,
|
||||
model_id=1,
|
||||
garment_asset_id=2,
|
||||
scene_ref_asset_id=3,
|
||||
)
|
||||
session.add(order)
|
||||
await session.flush()
|
||||
|
||||
workflow_run = WorkflowRunORM(
|
||||
order_id=order.id,
|
||||
workflow_id=f"order-{order.id}",
|
||||
workflow_type="LowEndPipelineWorkflow",
|
||||
status=OrderStatus.CREATED,
|
||||
)
|
||||
session.add(workflow_run)
|
||||
await session.flush()
|
||||
|
||||
tryon_asset = AssetORM(
|
||||
order_id=order.id,
|
||||
asset_type=AssetType.TRYON,
|
||||
step_name=WorkflowStepName.TRYON,
|
||||
uri="https://images.example.com/orders/1/tryon/generated.png",
|
||||
metadata_json={"prepared_asset_id": 1},
|
||||
)
|
||||
session.add(tryon_asset)
|
||||
|
||||
scene_resource = LibraryResourceORM(
|
||||
resource_type=LibraryResourceType.SCENE,
|
||||
name="Studio Background",
|
||||
description="摄影棚背景",
|
||||
tags=["室内"],
|
||||
status=LibraryResourceStatus.ACTIVE,
|
||||
environment="indoor",
|
||||
)
|
||||
session.add(scene_resource)
|
||||
await session.flush()
|
||||
|
||||
scene_original = LibraryResourceFileORM(
|
||||
resource_id=scene_resource.id,
|
||||
file_role=LibraryFileRole.ORIGINAL,
|
||||
storage_key="library/scenes/studio/original.png",
|
||||
public_url="https://images.example.com/library/scenes/studio/original.png",
|
||||
bucket="test-bucket",
|
||||
mime_type="image/png",
|
||||
size_bytes=2048,
|
||||
sort_order=0,
|
||||
)
|
||||
session.add(scene_original)
|
||||
await session.flush()
|
||||
scene_resource.original_file_id = scene_original.id
|
||||
scene_resource.cover_file_id = scene_original.id
|
||||
await session.commit()
|
||||
|
||||
payload = StepActivityInput(
|
||||
order_id=1,
|
||||
workflow_run_id=1,
|
||||
step_name=WorkflowStepName.SCENE,
|
||||
source_asset_id=1,
|
||||
scene_ref_asset_id=1,
|
||||
)
|
||||
|
||||
result = await scene_activities.run_scene_activity(payload)
|
||||
|
||||
assert result.uri == "https://images.example.com/orders/1/scene/generated.jpg"
|
||||
assert result.metadata["provider"] == "gemini"
|
||||
assert result.metadata["model"] == "gemini-test-image"
|
||||
assert result.metadata["source_asset_id"] == 1
|
||||
assert result.metadata["scene_resource_id"] == 1
|
||||
|
||||
async with get_session_factory()() as session:
|
||||
assets = (
|
||||
await session.execute(
|
||||
AssetORM.__table__.select().where(
|
||||
AssetORM.order_id == 1,
|
||||
AssetORM.asset_type == AssetType.SCENE,
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
|
||||
assert len(assets) == 1
|
||||
assert assets[0]["uri"] == "https://images.example.com/orders/1/scene/generated.jpg"
|
||||
33
tests/test_workflow_timeouts.py
Normal file
33
tests/test_workflow_timeouts.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Tests for workflow activity timeout policies."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from app.infra.temporal.task_queues import (
|
||||
IMAGE_PIPELINE_CONTROL_TASK_QUEUE,
|
||||
IMAGE_PIPELINE_EXPORT_TASK_QUEUE,
|
||||
IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE,
|
||||
IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE,
|
||||
IMAGE_PIPELINE_QC_TASK_QUEUE,
|
||||
)
|
||||
from app.workers.workflows.timeout_policy import (
|
||||
DEFAULT_ACTIVITY_TIMEOUT,
|
||||
LONG_RUNNING_ACTIVITY_TIMEOUT,
|
||||
activity_timeout_for_task_queue,
|
||||
)
|
||||
|
||||
|
||||
def test_activity_timeout_for_task_queue_uses_long_timeout_for_image_work():
|
||||
"""Image generation and post-processing queues should get a longer timeout."""
|
||||
|
||||
assert DEFAULT_ACTIVITY_TIMEOUT == timedelta(seconds=30)
|
||||
assert LONG_RUNNING_ACTIVITY_TIMEOUT == timedelta(minutes=5)
|
||||
assert activity_timeout_for_task_queue(IMAGE_PIPELINE_IMAGE_GEN_TASK_QUEUE) == LONG_RUNNING_ACTIVITY_TIMEOUT
|
||||
assert activity_timeout_for_task_queue(IMAGE_PIPELINE_POST_PROCESS_TASK_QUEUE) == LONG_RUNNING_ACTIVITY_TIMEOUT
|
||||
|
||||
|
||||
def test_activity_timeout_for_task_queue_keeps_short_timeout_for_light_steps():
|
||||
"""Control, QC, and export queues should stay on the short timeout."""
|
||||
|
||||
assert activity_timeout_for_task_queue(IMAGE_PIPELINE_CONTROL_TASK_QUEUE) == DEFAULT_ACTIVITY_TIMEOUT
|
||||
assert activity_timeout_for_task_queue(IMAGE_PIPELINE_QC_TASK_QUEUE) == DEFAULT_ACTIVITY_TIMEOUT
|
||||
assert activity_timeout_for_task_queue(IMAGE_PIPELINE_EXPORT_TASK_QUEUE) == DEFAULT_ACTIVITY_TIMEOUT
|
||||
Reference in New Issue
Block a user