feat: add approval-task links model and related functionality for task associations
This commit is contained in:
147
backend/tests/test_approval_task_links.py
Normal file
147
backend/tests/test_approval_task_links.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||
from sqlmodel import SQLModel
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.approval_task_links import ApprovalTaskLink
|
||||
from app.models.approvals import Approval
|
||||
from app.models.boards import Board
|
||||
from app.models.organizations import Organization
|
||||
from app.models.tasks import Task
|
||||
from app.services.approval_task_links import (
|
||||
load_task_ids_by_approval,
|
||||
normalize_task_ids,
|
||||
task_counts_for_board,
|
||||
)
|
||||
|
||||
|
||||
async def _make_engine() -> AsyncEngine:
|
||||
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
|
||||
async with engine.connect() as conn, conn.begin():
|
||||
await conn.run_sync(SQLModel.metadata.create_all)
|
||||
return engine
|
||||
|
||||
|
||||
async def _make_session(engine: AsyncEngine) -> AsyncSession:
|
||||
return AsyncSession(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
async def _seed_board(session: AsyncSession) -> tuple[UUID, UUID, UUID, UUID]:
|
||||
org_id = uuid4()
|
||||
board_id = uuid4()
|
||||
task_a = uuid4()
|
||||
task_b = uuid4()
|
||||
task_c = uuid4()
|
||||
|
||||
session.add(Organization(id=org_id, name=f"org-{org_id}"))
|
||||
session.add(Board(id=board_id, organization_id=org_id, name="b", slug="b"))
|
||||
session.add(Task(id=task_a, board_id=board_id, title="a"))
|
||||
session.add(Task(id=task_b, board_id=board_id, title="b"))
|
||||
session.add(Task(id=task_c, board_id=board_id, title="c"))
|
||||
await session.commit()
|
||||
return board_id, task_a, task_b, task_c
|
||||
|
||||
|
||||
def test_normalize_task_ids_dedupes_and_merges_sources() -> None:
|
||||
task_a = uuid4()
|
||||
task_b = uuid4()
|
||||
task_c = uuid4()
|
||||
|
||||
payload = {
|
||||
"task_id": str(task_a),
|
||||
"task_ids": [str(task_b), str(task_a)],
|
||||
"taskIds": [str(task_c), "not-a-uuid"],
|
||||
}
|
||||
result = normalize_task_ids(
|
||||
task_id=task_b,
|
||||
task_ids=[task_a],
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
assert result == [task_a, task_b, task_c]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_counts_for_board_supports_multi_task_links_and_legacy_rows() -> None:
|
||||
engine = await _make_engine()
|
||||
async with await _make_session(engine) as session:
|
||||
board_id, task_a, task_b, task_c = await _seed_board(session)
|
||||
|
||||
approval_pending_multi = Approval(
|
||||
board_id=board_id,
|
||||
task_id=task_a,
|
||||
action_type="task.update",
|
||||
confidence=80,
|
||||
status="pending",
|
||||
)
|
||||
approval_approved = Approval(
|
||||
board_id=board_id,
|
||||
task_id=task_a,
|
||||
action_type="task.complete",
|
||||
confidence=90,
|
||||
status="approved",
|
||||
)
|
||||
approval_pending_two = Approval(
|
||||
board_id=board_id,
|
||||
task_id=task_b,
|
||||
action_type="task.assign",
|
||||
confidence=75,
|
||||
status="pending",
|
||||
)
|
||||
approval_legacy = Approval(
|
||||
board_id=board_id,
|
||||
task_id=task_c,
|
||||
action_type="task.comment",
|
||||
confidence=65,
|
||||
status="pending",
|
||||
)
|
||||
session.add(approval_pending_multi)
|
||||
session.add(approval_approved)
|
||||
session.add(approval_pending_two)
|
||||
session.add(approval_legacy)
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
ApprovalTaskLink(approval_id=approval_pending_multi.id, task_id=task_a),
|
||||
)
|
||||
session.add(
|
||||
ApprovalTaskLink(approval_id=approval_pending_multi.id, task_id=task_b),
|
||||
)
|
||||
session.add(ApprovalTaskLink(approval_id=approval_approved.id, task_id=task_a))
|
||||
session.add(ApprovalTaskLink(approval_id=approval_pending_two.id, task_id=task_b))
|
||||
session.add(ApprovalTaskLink(approval_id=approval_pending_two.id, task_id=task_c))
|
||||
await session.commit()
|
||||
|
||||
counts = await task_counts_for_board(session, board_id=board_id)
|
||||
|
||||
assert counts[task_a] == (2, 1)
|
||||
assert counts[task_b] == (2, 2)
|
||||
assert counts[task_c] == (2, 2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_task_ids_by_approval_preserves_insert_order() -> None:
|
||||
engine = await _make_engine()
|
||||
async with await _make_session(engine) as session:
|
||||
board_id, task_a, task_b, task_c = await _seed_board(session)
|
||||
|
||||
approval = Approval(
|
||||
board_id=board_id,
|
||||
task_id=task_a,
|
||||
action_type="task.update",
|
||||
confidence=88,
|
||||
status="pending",
|
||||
)
|
||||
session.add(approval)
|
||||
await session.flush()
|
||||
session.add(ApprovalTaskLink(approval_id=approval.id, task_id=task_a))
|
||||
session.add(ApprovalTaskLink(approval_id=approval.id, task_id=task_b))
|
||||
session.add(ApprovalTaskLink(approval_id=approval.id, task_id=task_c))
|
||||
await session.commit()
|
||||
|
||||
mapping = await load_task_ids_by_approval(session, approval_ids=[approval.id])
|
||||
assert mapping[approval.id] == [task_a, task_b, task_c]
|
||||
@@ -10,7 +10,7 @@ from app.api import approvals
|
||||
from app.models.agents import Agent
|
||||
from app.models.approvals import Approval
|
||||
from app.models.boards import Board
|
||||
from app.schemas.approvals import ApprovalUpdate
|
||||
from app.schemas.approvals import ApprovalRead, ApprovalUpdate
|
||||
from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig
|
||||
|
||||
|
||||
@@ -120,6 +120,25 @@ async def test_update_approval_notifies_lead_when_approved(
|
||||
_fake_try_send_agent_message,
|
||||
)
|
||||
|
||||
async def _fake_load_task_ids_by_approval(
|
||||
_session: object,
|
||||
*,
|
||||
approval_ids: list[UUID],
|
||||
) -> dict[UUID, list[UUID]]:
|
||||
_ = approval_ids
|
||||
return {approval.id: []}
|
||||
|
||||
monkeypatch.setattr(approvals, "load_task_ids_by_approval", _fake_load_task_ids_by_approval)
|
||||
|
||||
async def _fake_reads(_session: object, _approvals: list[Approval]) -> list[ApprovalRead]:
|
||||
return [ApprovalRead.model_validate(approval, from_attributes=True)]
|
||||
|
||||
monkeypatch.setattr(
|
||||
approvals,
|
||||
"_approval_reads",
|
||||
_fake_reads,
|
||||
)
|
||||
|
||||
updated = await approvals.update_approval(
|
||||
approval_id=str(approval.id),
|
||||
payload=ApprovalUpdate(status="approved"),
|
||||
@@ -155,6 +174,15 @@ async def test_update_approval_skips_notify_when_status_not_resolved(
|
||||
|
||||
monkeypatch.setattr(approvals, "_notify_lead_on_approval_resolution", _fake_notify)
|
||||
|
||||
async def _fake_reads(_session: object, _approvals: list[Approval]) -> list[ApprovalRead]:
|
||||
return [ApprovalRead.model_validate(approval, from_attributes=True)]
|
||||
|
||||
monkeypatch.setattr(
|
||||
approvals,
|
||||
"_approval_reads",
|
||||
_fake_reads,
|
||||
)
|
||||
|
||||
updated = await approvals.update_approval(
|
||||
approval_id=str(approval.id),
|
||||
payload=ApprovalUpdate(status="pending"),
|
||||
|
||||
@@ -56,6 +56,7 @@ async def test_delete_my_org_cleans_dependents_before_organization_delete() -> N
|
||||
"activity_events",
|
||||
"task_dependencies",
|
||||
"task_fingerprints",
|
||||
"approval_task_links",
|
||||
"approvals",
|
||||
"board_memory",
|
||||
"board_onboarding_sessions",
|
||||
|
||||
Reference in New Issue
Block a user