"""PR-Worker-Pool-Registry-1B — worker_jobs ORM/schema 단위 검증. 3 항목 (endpoint test 와 중복 회피, schema-level 만): 1. CHECK constraint — status 가 enum 4 외 값일 때 INSERT 거부 (정정 #5) 2. partial unique-index 동작 검증 — pending row 만 색인 (idx_worker_jobs_pending_type) 3. ON DELETE SET NULL — worker_capabilities 삭제 시 worker_jobs.worker_id 자동 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 sqlalchemy import text from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from _worker_pool_helpers import ( cleanup_worker_capabilities, cleanup_worker_jobs, ensure_user, get_database_url, ) @pytest_asyncio.fixture async def db_session(): engine = create_async_engine(get_database_url()) sm = async_sessionmaker(engine, expire_on_commit=False) async with sm() as session: yield session await engine.dispose() @pytest_asyncio.fixture async def owner_id(db_session): return await ensure_user(db_session, "test-owner-smoke-1b") @pytest_asyncio.fixture async def worker_id_unique(db_session, owner_id): wid = f"test-smoke-1b-{uuid.uuid4().hex[:8]}" await db_session.execute( text( "INSERT INTO worker_capabilities (worker_id, user_id, device_label, " "worker_class, tier) VALUES (:w, :u, 'lbl', 'laptop', 'laptop_small')" ), {"w": wid, "u": owner_id}, ) await db_session.commit() yield wid await cleanup_worker_jobs(db_session, "test-smoke-1b") await cleanup_worker_capabilities(db_session, wid) @pytest.mark.asyncio async def test_check_constraint_rejects_unknown_status(db_session, owner_id, worker_id_unique): """정정 #5: status 가 enum 4 외 값이면 IntegrityError.""" with pytest.raises(IntegrityError): await db_session.execute( text( "INSERT INTO worker_jobs (user_id, job_type, status) " "VALUES (:u, 'test-smoke-1b-bad', 'running')" ), {"u": owner_id}, ) await db_session.commit() await db_session.rollback() @pytest.mark.asyncio async def test_partial_pending_index_used_for_claim_query(db_session, owner_id, worker_id_unique): """partial index idx_worker_jobs_pending_type 가 pending claim 쿼리 실행계획에 사용.""" # seed 2 rows: pending + completed await db_session.execute( text( "INSERT INTO worker_jobs (user_id, job_type, status) " "VALUES (:u, 'test-smoke-1b-idx', 'pending'), " " (:u, 'test-smoke-1b-idx', 'completed')" ), {"u": owner_id}, ) await db_session.commit() # EXPLAIN ANALYZE 가 partial index 사용하는지 확인 (운영 환경에선 seq scan 가능 — 본 테스트는 인덱스 정의 존재만 보장) res = await db_session.execute( text( "SELECT indexname FROM pg_indexes " "WHERE tablename = 'worker_jobs' AND indexname = 'idx_worker_jobs_pending_type'" ) ) assert res.scalar_one_or_none() == "idx_worker_jobs_pending_type" # pending row 만 SELECT 됨 (실제 동작 검증) res = await db_session.execute( text( "SELECT count(*) FROM worker_jobs " "WHERE status = 'pending' AND job_type = 'test-smoke-1b-idx'" ) ) assert res.scalar_one() == 1 @pytest.mark.asyncio async def test_on_delete_set_null_when_capability_dropped(db_session, owner_id): """worker_capabilities 삭제 시 worker_jobs.worker_id 자동 NULL (ON DELETE SET NULL).""" wid = f"test-smoke-1b-del-{uuid.uuid4().hex[:8]}" await db_session.execute( text( "INSERT INTO worker_capabilities (worker_id, user_id, device_label, " "worker_class, tier) VALUES (:w, :u, 'lbl', 'laptop', 'laptop_small')" ), {"w": wid, "u": owner_id}, ) job_id = ( await db_session.execute( text( "INSERT INTO worker_jobs (user_id, job_type, status, worker_id, attempts) " "VALUES (:u, 'test-smoke-1b-del', 'completed', :w, 1) RETURNING id" ), {"u": owner_id, "w": wid}, ) ).scalar_one() await db_session.commit() # worker_heartbeats CASCADE 가 worker_capabilities 삭제 차단할 수 있으니 사전 정리 await db_session.execute( text("DELETE FROM worker_heartbeats WHERE worker_id = :w"), {"w": wid} ) await db_session.execute(text("DELETE FROM worker_capabilities WHERE worker_id = :w"), {"w": wid}) await db_session.commit() res = await db_session.execute( text("SELECT worker_id FROM worker_jobs WHERE id = :i"), {"i": job_id} ) assert res.scalar_one() is None # cleanup await db_session.execute(text("DELETE FROM worker_jobs WHERE id = :i"), {"i": job_id}) await db_session.commit()