"""Integration tests for native Projects, Tasks hierarchy, and QC gating invariants.""" import uuid from datetime import date import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker import pytest_asyncio from app.models import ( Base, PMUser, Project, ProjectMember, ProjectStatus, QCAnswer, QCQuestionPhase, Task, TaskPriority, TaskQCDefinition, TaskQCQuestion, TaskQCResult, TaskStatus, ) from app.qc_service import build_state, load_definitions @pytest_asyncio.fixture async def async_db(): engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with async_session() as session: yield session await engine.dispose() @pytest.mark.asyncio async def test_project_and_members_creation(async_db: AsyncSession): # 1. Create a user user = PMUser( username="manager1", password_hash="fake", display_name="مهندس مدیر", role="project_manager", ) async_db.add(user) await async_db.commit() # 2. Create a project project = Project( name="پروژه برج بهارستان", description="ساخت مجتمع ۲۰ طبقه", location="تهران", project_manager="مهندس مدیر", status=ProjectStatus.ACTIVE.value, planned_start=date(2026, 9, 1), planned_end=date(2027, 9, 1), ) async_db.add(project) await async_db.flush() # 3. Add a project member member = ProjectMember( project_id=project.id, user_id=user.id, role="project_manager", ) async_db.add(member) await async_db.commit() # Verify project loaded = await async_db.scalar(select(Project).where(Project.id == project.id)) assert loaded is not None assert loaded.name == "پروژه برج بهارستان" assert loaded.status == "ACTIVE" @pytest.mark.asyncio async def test_hierarchical_tasks(async_db: AsyncSession): # Setup project project = Project(name="پروژه تست ساختار شکست") async_db.add(project) await async_db.commit() # 1. Phase 1 (Root Task) phase1 = Task( project_id=project.id, parent_id=None, title="فاز ۱: اسکلت فلزی", status=TaskStatus.TODO.value, ) async_db.add(phase1) await async_db.flush() # 2. Activity (Subtask under Phase 1) activity1 = Task( project_id=project.id, parent_id=phase1.id, title="فعالیت ۱.۱: نصب ستون‌های طبقه اول", status=TaskStatus.TODO.value, ) async_db.add(activity1) await async_db.flush() # 3. Subtask under Activity subtask1 = Task( project_id=project.id, parent_id=activity1.id, title="زیرتسک ۱.۱.۱: بولت‌گذاری و تراز پایه", status=TaskStatus.TODO.value, ) async_db.add(subtask1) await async_db.commit() # Query hierarchy root = await async_db.scalar(select(Task).where(Task.id == phase1.id)) assert root is not None assert root.parent_id is None leaf = await async_db.scalar(select(Task).where(Task.id == subtask1.id)) assert leaf is not None assert leaf.parent_id == activity1.id @pytest.mark.asyncio async def test_qc_gating_invariants(async_db: AsyncSession): project = Project(name="پروژه تست کیفیت") async_db.add(project) await async_db.commit() task = Task( project_id=project.id, title="بتن‌ریزی فونداسیون", status=TaskStatus.TODO.value, ) async_db.add(task) await async_db.flush() # Create task QC definition with 1 pre-execution question and 1 execution question qc_def = TaskQCDefinition(task_id=task.id, name="چک‌لیست بتن‌ریزی") async_db.add(qc_def) await async_db.flush() q_pre = TaskQCQuestion( definition_id=qc_def.id, phase=QCQuestionPhase.pre, text="آیا آرماتوربندی و قالب‌بندی بازرسی و تأیید شد؟", required=True, sort_order=1, ) q_exec = TaskQCQuestion( definition_id=qc_def.id, phase=QCQuestionPhase.during, text="آیا اسلامپ بتن در محدوده استاندارد است؟", required=True, sort_order=2, ) async_db.add_all([q_pre, q_exec]) await async_db.commit() task_id = task.id # Invariant Rule 1 check: Before answering pre-QC, can_start must be FALSE defs = await load_definitions(async_db, task_id) state = build_state(defs) assert state.can_start is False assert state.can_complete is False # Now pass the pre-QC question res_pre = TaskQCResult(question_id=q_pre.id, question=q_pre, answer=QCAnswer.pass_) async_db.add(res_pre) await async_db.commit() # Load fresh definitions in new query defs = await load_definitions(async_db, task_id) state = build_state(defs) # can_start is now TRUE! Progress can become > 0 assert state.can_start is True # But can_complete is still FALSE because execution question is not yet answered! assert state.can_complete is False # Now pass the execution question as well res_exec = TaskQCResult(question_id=q_exec.id, question=q_exec, answer=QCAnswer.pass_) async_db.add(res_exec) await async_db.commit() defs = await load_definitions(async_db, task_id) state = build_state(defs) # Both can_start and can_complete are now TRUE! assert state.can_start is True assert state.can_complete is True