chore(pm): vendor the Aramesh/PM platform under pm/ (squash of Aramesh/PM @ 91f3271)
The PM panel (PmPanelPage -> iframe /pm/) needs the project-management platform served at /pm/ on the ERP domain. Source: org repo Aramesh/PM, branch feature/project-management-platform. Update via: git subtree pull --prefix=pm aramesh-pm feature/project-management-platform --squash
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user