feat: add manual revision and dashboard list apis
This commit is contained in:
@@ -36,6 +36,25 @@ 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."""
|
||||
|
||||
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
|
||||
return response.json()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthcheck(api_runtime):
|
||||
"""The health endpoint should always respond successfully."""
|
||||
@@ -91,20 +110,7 @@ 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()
|
||||
payload = await create_mid_end_order(client)
|
||||
|
||||
await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
|
||||
@@ -140,20 +146,7 @@ async def test_mid_end_rerun_paths_return_to_review(api_runtime, decision: 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()
|
||||
payload = await create_mid_end_order(client)
|
||||
|
||||
await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
|
||||
@@ -163,8 +156,9 @@ async def test_mid_end_rerun_paths_return_to_review(api_runtime, decision: str,
|
||||
)
|
||||
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)
|
||||
await wait_for_step_count(client, payload["order_id"], expected_step, 2)
|
||||
await wait_for_step_count(client, payload["order_id"], "review", 2)
|
||||
workflow_payload = await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
assert workflow_payload["workflow_status"] == "waiting_review"
|
||||
|
||||
approve_response = await client.post(
|
||||
@@ -176,3 +170,303 @@ async def test_mid_end_rerun_paths_return_to_review(api_runtime, decision: str,
|
||||
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_registers_manual_revision_and_updates_pending_queue(api_runtime):
|
||||
"""Registering a manual revision should keep the workflow paused and mark the queue item accordingly."""
|
||||
|
||||
client, _ = api_runtime
|
||||
payload = await create_mid_end_order(client)
|
||||
|
||||
workflow_payload = await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
parent_asset_id = workflow_payload["steps"][-2]["output_json"]["candidate_asset_ids"][0]
|
||||
|
||||
register_response = await client.post(
|
||||
f"/api/v1/orders/{payload['order_id']}/revisions",
|
||||
json={
|
||||
"parent_asset_id": parent_asset_id,
|
||||
"uploaded_uri": "mock://manual-revision-v1",
|
||||
"reviewer_id": 88,
|
||||
"comment": "人工修订第一版",
|
||||
},
|
||||
)
|
||||
|
||||
assert register_response.status_code == 201
|
||||
register_payload = register_response.json()
|
||||
assert register_payload["order_id"] == payload["order_id"]
|
||||
assert register_payload["parent_asset_id"] == parent_asset_id
|
||||
assert register_payload["root_asset_id"] == parent_asset_id
|
||||
assert register_payload["version_no"] == 1
|
||||
assert register_payload["review_task_status"] == "revision_uploaded"
|
||||
|
||||
pending_response = await client.get("/api/v1/reviews/pending")
|
||||
assert pending_response.status_code == 200
|
||||
queue_item = next(item for item in pending_response.json() if item["order_id"] == payload["order_id"])
|
||||
assert queue_item["review_task_status"] == "revision_uploaded"
|
||||
assert queue_item["latest_revision_asset_id"] == register_payload["asset_id"]
|
||||
assert queue_item["revision_count"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mid_end_order_lists_single_line_revision_chain(api_runtime):
|
||||
"""Listing revisions should return the uploaded manual revision chain in version order."""
|
||||
|
||||
client, _ = api_runtime
|
||||
payload = await create_mid_end_order(client)
|
||||
|
||||
workflow_payload = await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
root_asset_id = workflow_payload["steps"][-2]["output_json"]["candidate_asset_ids"][0]
|
||||
|
||||
first_response = await client.post(
|
||||
f"/api/v1/orders/{payload['order_id']}/revisions",
|
||||
json={
|
||||
"parent_asset_id": root_asset_id,
|
||||
"uploaded_uri": "mock://manual-revision-v1",
|
||||
"reviewer_id": 88,
|
||||
"comment": "人工修订第一版",
|
||||
},
|
||||
)
|
||||
assert first_response.status_code == 201
|
||||
first_payload = first_response.json()
|
||||
|
||||
second_response = await client.post(
|
||||
f"/api/v1/orders/{payload['order_id']}/revisions",
|
||||
json={
|
||||
"parent_asset_id": first_payload["asset_id"],
|
||||
"uploaded_uri": "mock://manual-revision-v2",
|
||||
"reviewer_id": 88,
|
||||
"comment": "人工修订第二版",
|
||||
},
|
||||
)
|
||||
assert second_response.status_code == 201
|
||||
second_payload = second_response.json()
|
||||
|
||||
chain_response = await client.get(f"/api/v1/orders/{payload['order_id']}/revisions")
|
||||
assert chain_response.status_code == 200
|
||||
chain_payload = chain_response.json()
|
||||
|
||||
assert chain_payload["order_id"] == payload["order_id"]
|
||||
assert [item["asset_id"] for item in chain_payload["items"]] == [
|
||||
first_payload["asset_id"],
|
||||
second_payload["asset_id"],
|
||||
]
|
||||
assert [item["version_no"] for item in chain_payload["items"]] == [1, 2]
|
||||
assert chain_payload["items"][0]["parent_asset_id"] == root_asset_id
|
||||
assert chain_payload["items"][1]["parent_asset_id"] == first_payload["asset_id"]
|
||||
assert chain_payload["items"][-1]["is_current_version"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mid_end_order_confirms_manual_revision_and_exports_revision_asset(api_runtime):
|
||||
"""Confirming a manual revision should resume the workflow and export the revision asset."""
|
||||
|
||||
client, env = api_runtime
|
||||
payload = await create_mid_end_order(client)
|
||||
|
||||
workflow_payload = await wait_for_workflow_status(client, payload["order_id"], "waiting_review")
|
||||
parent_asset_id = workflow_payload["steps"][-2]["output_json"]["candidate_asset_ids"][0]
|
||||
|
||||
register_response = await client.post(
|
||||
f"/api/v1/orders/{payload['order_id']}/revisions",
|
||||
json={
|
||||
"parent_asset_id": parent_asset_id,
|
||||
"uploaded_uri": "mock://manual-revision-v1",
|
||||
"reviewer_id": 88,
|
||||
"comment": "人工修订第一版",
|
||||
},
|
||||
)
|
||||
assert register_response.status_code == 201
|
||||
register_payload = register_response.json()
|
||||
|
||||
confirm_response = await client.post(
|
||||
f"/api/v1/reviews/{payload['order_id']}/confirm-revision",
|
||||
json={
|
||||
"reviewer_id": 88,
|
||||
"comment": "确认继续流水线",
|
||||
},
|
||||
)
|
||||
|
||||
assert confirm_response.status_code == 200
|
||||
confirm_payload = confirm_response.json()
|
||||
assert confirm_payload["revision_asset_id"] == register_payload["asset_id"]
|
||||
assert confirm_payload["decision"] == "approve"
|
||||
assert confirm_payload["status"] == "submitted"
|
||||
|
||||
handle = env.client.get_workflow_handle(payload["workflow_id"])
|
||||
result = await handle.result()
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["final_asset_id"] is not None
|
||||
|
||||
order_response = await client.get(f"/api/v1/orders/{payload['order_id']}")
|
||||
assert order_response.status_code == 200
|
||||
order_payload = order_response.json()
|
||||
assert order_payload["status"] == "succeeded"
|
||||
assert order_payload["final_asset"]["asset_type"] == "final"
|
||||
assert order_payload["final_asset"]["metadata_json"]["source_asset_id"] == register_payload["asset_id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orders_list_returns_recent_orders_with_revision_summary(api_runtime):
|
||||
"""Orders list should expose pagination metadata, filtering, and revision summary fields."""
|
||||
|
||||
client, env = api_runtime
|
||||
|
||||
low_order = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": 201,
|
||||
"pose_id": 11,
|
||||
"garment_asset_id": 9101,
|
||||
"scene_ref_asset_id": 8101,
|
||||
},
|
||||
)
|
||||
assert low_order.status_code == 201
|
||||
low_payload = low_order.json()
|
||||
await env.client.get_workflow_handle(low_payload["workflow_id"]).result()
|
||||
|
||||
mid_payload = await create_mid_end_order(client)
|
||||
workflow_payload = await wait_for_workflow_status(client, mid_payload["order_id"], "waiting_review")
|
||||
parent_asset_id = workflow_payload["steps"][-2]["output_json"]["candidate_asset_ids"][0]
|
||||
|
||||
register_response = await client.post(
|
||||
f"/api/v1/orders/{mid_payload['order_id']}/revisions",
|
||||
json={
|
||||
"parent_asset_id": parent_asset_id,
|
||||
"uploaded_uri": "mock://manual-revision-v1",
|
||||
"reviewer_id": 99,
|
||||
"comment": "人工修订第一版",
|
||||
},
|
||||
)
|
||||
assert register_response.status_code == 201
|
||||
|
||||
list_response = await client.get("/api/v1/orders", params={"page": 1, "limit": 1})
|
||||
assert list_response.status_code == 200
|
||||
first_page = list_response.json()
|
||||
|
||||
assert first_page["page"] == 1
|
||||
assert first_page["limit"] == 1
|
||||
assert first_page["total"] == 2
|
||||
assert first_page["total_pages"] == 2
|
||||
assert [item["order_id"] for item in first_page["items"]] == [mid_payload["order_id"]]
|
||||
assert first_page["items"][0]["workflow_id"] == mid_payload["workflow_id"]
|
||||
assert first_page["items"][0]["review_task_status"] == "revision_uploaded"
|
||||
assert first_page["items"][0]["latest_revision_version"] == 1
|
||||
assert first_page["items"][0]["revision_count"] == 1
|
||||
assert first_page["items"][0]["pending_manual_confirm"] is True
|
||||
|
||||
second_page_response = await client.get("/api/v1/orders", params={"page": 2, "limit": 1})
|
||||
assert second_page_response.status_code == 200
|
||||
second_page = second_page_response.json()
|
||||
assert second_page["page"] == 2
|
||||
assert second_page["limit"] == 1
|
||||
assert second_page["total"] == 2
|
||||
assert second_page["total_pages"] == 2
|
||||
assert [item["order_id"] for item in second_page["items"]] == [low_payload["order_id"]]
|
||||
assert second_page["items"][0]["status"] == "succeeded"
|
||||
|
||||
filtered_response = await client.get(
|
||||
"/api/v1/orders", params={"page": 1, "limit": 10, "status": "waiting_review"}
|
||||
)
|
||||
assert filtered_response.status_code == 200
|
||||
filtered_payload = filtered_response.json()
|
||||
assert filtered_payload["page"] == 1
|
||||
assert filtered_payload["limit"] == 10
|
||||
assert filtered_payload["total"] == 1
|
||||
assert filtered_payload["total_pages"] == 1
|
||||
assert [item["order_id"] for item in filtered_payload["items"]] == [mid_payload["order_id"]]
|
||||
|
||||
query_response = await client.get(
|
||||
"/api/v1/orders",
|
||||
params={"page": 1, "limit": 10, "query": mid_payload["workflow_id"]},
|
||||
)
|
||||
assert query_response.status_code == 200
|
||||
query_payload = query_response.json()
|
||||
assert query_payload["total"] == 1
|
||||
assert [item["order_id"] for item in query_payload["items"]] == [mid_payload["order_id"]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflows_list_returns_recent_runs_with_failure_count(api_runtime):
|
||||
"""Workflow list should expose pagination metadata, filtering, and revision summary."""
|
||||
|
||||
client, env = api_runtime
|
||||
|
||||
low_order = await client.post(
|
||||
"/api/v1/orders",
|
||||
json={
|
||||
"customer_level": "low",
|
||||
"service_mode": "auto_basic",
|
||||
"model_id": 301,
|
||||
"pose_id": 21,
|
||||
"garment_asset_id": 9201,
|
||||
"scene_ref_asset_id": 8201,
|
||||
},
|
||||
)
|
||||
assert low_order.status_code == 201
|
||||
low_payload = low_order.json()
|
||||
await env.client.get_workflow_handle(low_payload["workflow_id"]).result()
|
||||
|
||||
mid_payload = await create_mid_end_order(client)
|
||||
workflow_payload = await wait_for_workflow_status(client, mid_payload["order_id"], "waiting_review")
|
||||
parent_asset_id = workflow_payload["steps"][-2]["output_json"]["candidate_asset_ids"][0]
|
||||
|
||||
register_response = await client.post(
|
||||
f"/api/v1/orders/{mid_payload['order_id']}/revisions",
|
||||
json={
|
||||
"parent_asset_id": parent_asset_id,
|
||||
"uploaded_uri": "mock://manual-revision-v1",
|
||||
"reviewer_id": 77,
|
||||
"comment": "人工修订第一版",
|
||||
},
|
||||
)
|
||||
assert register_response.status_code == 201
|
||||
|
||||
list_response = await client.get("/api/v1/workflows", params={"page": 1, "limit": 1})
|
||||
assert list_response.status_code == 200
|
||||
first_page = list_response.json()
|
||||
|
||||
assert first_page["page"] == 1
|
||||
assert first_page["limit"] == 1
|
||||
assert first_page["total"] == 2
|
||||
assert first_page["total_pages"] == 2
|
||||
assert [item["order_id"] for item in first_page["items"]] == [mid_payload["order_id"]]
|
||||
assert first_page["items"][0]["workflow_id"] == mid_payload["workflow_id"]
|
||||
assert first_page["items"][0]["workflow_status"] == "waiting_review"
|
||||
assert first_page["items"][0]["review_task_status"] == "revision_uploaded"
|
||||
assert first_page["items"][0]["latest_revision_version"] == 1
|
||||
assert first_page["items"][0]["revision_count"] == 1
|
||||
assert first_page["items"][0]["pending_manual_confirm"] is True
|
||||
assert first_page["items"][0]["failure_count"] == 0
|
||||
|
||||
second_page_response = await client.get("/api/v1/workflows", params={"page": 2, "limit": 1})
|
||||
assert second_page_response.status_code == 200
|
||||
second_page = second_page_response.json()
|
||||
assert second_page["page"] == 2
|
||||
assert second_page["limit"] == 1
|
||||
assert second_page["total"] == 2
|
||||
assert second_page["total_pages"] == 2
|
||||
assert [item["order_id"] for item in second_page["items"]] == [low_payload["order_id"]]
|
||||
assert second_page["items"][0]["workflow_status"] == "succeeded"
|
||||
|
||||
filtered_response = await client.get(
|
||||
"/api/v1/workflows", params={"page": 1, "limit": 10, "status": "waiting_review"}
|
||||
)
|
||||
assert filtered_response.status_code == 200
|
||||
filtered_payload = filtered_response.json()
|
||||
assert filtered_payload["page"] == 1
|
||||
assert filtered_payload["limit"] == 10
|
||||
assert filtered_payload["total"] == 1
|
||||
assert filtered_payload["total_pages"] == 1
|
||||
assert [item["order_id"] for item in filtered_payload["items"]] == [mid_payload["order_id"]]
|
||||
|
||||
query_response = await client.get(
|
||||
"/api/v1/workflows",
|
||||
params={"page": 1, "limit": 10, "query": str(low_payload["order_id"])},
|
||||
)
|
||||
assert query_response.status_code == 200
|
||||
query_payload = query_response.json()
|
||||
assert query_payload["total"] == 1
|
||||
assert [item["order_id"] for item in query_payload["items"]] == [low_payload["order_id"]]
|
||||
|
||||
Reference in New Issue
Block a user