Files
ERP/backend/tests/test_projects_and_tasks.py
T
MMD 934b74ebbd Squashed 'pm/' content from commit 91f3271
git-subtree-dir: pm
git-subtree-split: 91f3271d866fa8ee471cfb6793e334b83e6d2b6e
2026-10-07 15:14:20 +03:30

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