201 lines
5.8 KiB
Python
201 lines
5.8 KiB
Python
"""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
|
|
|
|
|
|
|