0cbd97fcba
각 helper 가 자체 engine + NullPool 사용 (connection 격리). fixture chain 의 asyncpg "another operation in progress" race 회피. 호출 site 단순화. 같은 파일 sequential 실행 시 module-level app + global engine pool 충돌은 별 follow-up `PR-Worker-Pool-Test-Fixture-Isolation` (P3) 영역. 단독 PASS 검증: auth 5/5 + smoke 3/3 + ownership 1/1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
4.3 KiB
Python
145 lines
4.3 KiB
Python
"""PR-Worker-Pool-Registry-1B 정정 #3 — explicit /result failed 재시도.
|
|
|
|
3 case:
|
|
A. initial attempts=0, max=3 → claim 후 attempts=1 → failed → status='pending' 복귀.
|
|
B. initial attempts=2, max=3 → claim 후 attempts=3 → failed → final 'failed'.
|
|
C. failed 요청에 result={'partial':...} 포함 → DB result IS NULL 유지.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import uuid
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))
|
|
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from _worker_pool_helpers import (
|
|
cleanup_worker_capabilities,
|
|
cleanup_worker_jobs,
|
|
ensure_user,
|
|
fetch_worker_job,
|
|
insert_worker_capability,
|
|
insert_worker_job,
|
|
mint_access_token,
|
|
)
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def env_setup(monkeypatch):
|
|
monkeypatch.setenv("LAPTOP_WORKER_BOT_USERNAME", "laptop-worker-bot")
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def worker_token(env_setup):
|
|
await ensure_user("laptop-worker-bot")
|
|
return mint_access_token("laptop-worker-bot")
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def owner_id():
|
|
return await ensure_user("test-owner-retry-1b")
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def worker_id_unique(owner_id):
|
|
wid = f"test-retry-1b-{uuid.uuid4().hex[:8]}"
|
|
await insert_worker_capability(wid, owner_id)
|
|
yield wid
|
|
await cleanup_worker_jobs("test-retry-1b")
|
|
await cleanup_worker_capabilities(wid)
|
|
|
|
|
|
async def _seed_processing_job(
|
|
owner_id: int, worker_id: str, attempts: int, max_attempts: int = 3
|
|
) -> int:
|
|
return await insert_worker_job(
|
|
owner_id,
|
|
f"test-retry-1b-{uuid.uuid4().hex[:8]}",
|
|
status="processing",
|
|
worker_id=worker_id,
|
|
attempts=attempts,
|
|
max_attempts=max_attempts,
|
|
claimed=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_case_a_failed_then_pending(worker_token, worker_id_unique, owner_id):
|
|
from main import app
|
|
|
|
job_id = await _seed_processing_job(owner_id, worker_id_unique, attempts=1)
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=app), base_url="http://test"
|
|
) as c:
|
|
r = await c.post(
|
|
"/internal/worker/result",
|
|
json={
|
|
"job_id": job_id,
|
|
"worker_id": worker_id_unique,
|
|
"status": "failed",
|
|
"error_message": "case A error",
|
|
},
|
|
headers={"Authorization": f"Bearer {worker_token}"},
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
job = await fetch_worker_job(job_id)
|
|
assert job["status"] == "pending"
|
|
assert job["worker_id"] is None
|
|
assert job["claimed_at"] is None
|
|
assert job["completed_at"] is None
|
|
assert job["error_message"] == "case A error"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_case_b_failed_max_attempts_final(worker_token, worker_id_unique, owner_id):
|
|
from main import app
|
|
|
|
job_id = await _seed_processing_job(owner_id, worker_id_unique, attempts=3)
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=app), base_url="http://test"
|
|
) as c:
|
|
r = await c.post(
|
|
"/internal/worker/result",
|
|
json={
|
|
"job_id": job_id,
|
|
"worker_id": worker_id_unique,
|
|
"status": "failed",
|
|
"error_message": "case B final",
|
|
},
|
|
headers={"Authorization": f"Bearer {worker_token}"},
|
|
)
|
|
assert r.status_code == 200
|
|
job = await fetch_worker_job(job_id)
|
|
assert job["status"] == "failed"
|
|
assert job["completed_at"] is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_case_c_failed_ignores_result_field(worker_token, worker_id_unique, owner_id):
|
|
from main import app
|
|
|
|
job_id = await _seed_processing_job(owner_id, worker_id_unique, attempts=1)
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=app), base_url="http://test"
|
|
) as c:
|
|
r = await c.post(
|
|
"/internal/worker/result",
|
|
json={
|
|
"job_id": job_id,
|
|
"worker_id": worker_id_unique,
|
|
"status": "failed",
|
|
"result": {"partial": "should be ignored"},
|
|
"error_message": "case C",
|
|
},
|
|
headers={"Authorization": f"Bearer {worker_token}"},
|
|
)
|
|
assert r.status_code == 200
|
|
job = await fetch_worker_job(job_id)
|
|
assert job["result"] is None # 정정 #3 엄격 규칙
|