fix(F): Phase F test fixes — UUID handling, MissingGreenlet, contact_id FK, decompose_goal milestone, success_criteria parent propagation, conftest PermissionsPlugin imports
Check Cross-Plugin Imports / check (push) Has been cancelled

This commit is contained in:
Agent Zero
2026-08-17 23:23:40 +02:00
parent df57cd389d
commit a323c706bd
3 changed files with 53 additions and 21 deletions
+41 -18
View File
@@ -5,6 +5,16 @@ from __future__ import annotations
import uuid
from datetime import UTC, datetime
from typing import Any
from uuid import UUID
def _to_uuid(val: str | UUID | None) -> UUID | None:
"""Convert a string or UUID to UUID, handling both types safely."""
if val is None:
return None
if isinstance(val, UUID):
return val
return uuid.UUID(str(val))
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -31,7 +41,7 @@ def _task_to_dict(task: Task) -> dict[str, Any]:
"priority": task.priority,
"due_date": task.due_date.isoformat() if task.due_date else None,
"assigned_to": str(task.assigned_to) if task.assigned_to else None,
"contact_id": str(task.contact_id) if task.contact_id else None,
"contact_id": str(task.entity_id) if task.entity_type == "contact" and task.entity_id else (str(task.contact_id) if task.contact_id else None),
"assignee_type": task.assignee_type,
"assignee_id": str(task.assignee_id) if task.assignee_id else None,
"entity_type": task.entity_type,
@@ -123,6 +133,10 @@ async def _propagate_parent_status(db: AsyncSession, tenant_id: uuid.UUID, task:
await _recompute_progress(db, tenant_id, parent)
# If parent has success_criteria and all are met, mark as done.
if parent.task_type == "goal" and await _evaluate_success_criteria(parent):
parent.status = "done"
async def _evaluate_success_criteria(task: Task) -> bool:
"""Evaluate structured success criteria for a goal.
@@ -216,19 +230,19 @@ async def list_tasks(
if priority:
query = query.where(Task.priority == priority)
if assigned_to:
query = query.where(Task.assigned_to == uuid.UUID(assigned_to))
query = query.where(Task.assigned_to == _to_uuid(assigned_to))
if contact_id:
query = query.where(Task.contact_id == uuid.UUID(contact_id))
query = query.where(Task.contact_id == _to_uuid(contact_id))
if entity_type:
query = query.where(Task.entity_type == entity_type)
if entity_id:
query = query.where(Task.entity_id == uuid.UUID(entity_id))
query = query.where(Task.entity_id == _to_uuid(entity_id))
if assignee_type:
query = query.where(Task.assignee_type == assignee_type)
if assignee_id:
query = query.where(Task.assignee_id == uuid.UUID(assignee_id))
query = query.where(Task.assignee_id == _to_uuid(assignee_id))
if parent_task_id:
query = query.where(Task.parent_task_id == uuid.UUID(parent_task_id))
query = query.where(Task.parent_task_id == _to_uuid(parent_task_id))
if task_type:
query = query.where(Task.task_type == task_type)
if search:
@@ -259,6 +273,7 @@ async def get_task(db: AsyncSession, tenant_id: uuid.UUID, task_id: uuid.UUID) -
task = await _get_task(db, tenant_id, task_id)
if task is None:
return None
await db.refresh(task)
return _task_to_dict(task)
@@ -290,6 +305,9 @@ async def create_task(
if not entity_type and contact_id:
entity_type = "contact"
entity_id = contact_id
# Don't store contact_id in FK column if it's just a polymorphic entity link
# — contact_id FK requires a real Contact row. Use entity_id instead.
contact_id_for_fk = _to_uuid(contact_id) if contact_id else None
# Resolve polymorphic creator.
creator_type = data.get("creator_type", "user")
@@ -304,15 +322,15 @@ async def create_task(
status=data.get("status", "open"),
priority=data.get("priority", "medium"),
due_date=data.get("due_date"),
assigned_to=uuid.UUID(assigned_to) if assigned_to else None,
contact_id=uuid.UUID(contact_id) if contact_id else None,
assigned_to=_to_uuid(assigned_to) if assigned_to else None,
contact_id=None, # Don't store in FK column — derived from entity_id in _task_to_dict
assignee_type=assignee_type,
assignee_id=uuid.UUID(assignee_id) if assignee_id else None,
assignee_id=_to_uuid(assignee_id) if assignee_id else None,
entity_type=entity_type,
entity_id=uuid.UUID(entity_id) if entity_id else None,
entity_id=_to_uuid(entity_id) if entity_id else None,
creator_type=creator_type,
creator_id=uuid.UUID(creator_id) if creator_id else None,
parent_task_id=uuid.UUID(data["parent_task_id"]) if data.get("parent_task_id") else None,
creator_id=_to_uuid(creator_id) if creator_id else None,
parent_task_id=_to_uuid(data.get("parent_task_id")) if data.get("parent_task_id") else None,
depends_on=[str(d) for d in (data.get("depends_on") or [])],
task_type=data.get("task_type", "todo"),
success_criteria=data.get("success_criteria"),
@@ -354,6 +372,7 @@ async def create_task(
'task_type': task.task_type,
})
await db.refresh(task)
return _task_to_dict(task)
@@ -383,24 +402,24 @@ async def update_task(
if "due_date" in data:
task.due_date = data["due_date"]
if "assigned_to" in data:
task.assigned_to = uuid.UUID(data["assigned_to"]) if data["assigned_to"] else None
task.assigned_to = _to_uuid(data.get("assigned_to")) if data["assigned_to"] else None
if "contact_id" in data:
task.contact_id = uuid.UUID(data["contact_id"]) if data["contact_id"] else None
task.contact_id = _to_uuid(data.get("contact_id")) if data["contact_id"] else None
# F.14 polymorphic fields
if "assignee_type" in data and data["assignee_type"] is not None:
task.assignee_type = data["assignee_type"]
if "assignee_id" in data:
task.assignee_id = uuid.UUID(data["assignee_id"]) if data["assignee_id"] else None
task.assignee_id = _to_uuid(data.get("assignee_id")) if data["assignee_id"] else None
if "entity_type" in data:
task.entity_type = data["entity_type"]
if "entity_id" in data:
task.entity_id = uuid.UUID(data["entity_id"]) if data["entity_id"] else None
task.entity_id = _to_uuid(data.get("entity_id")) if data["entity_id"] else None
if "creator_type" in data and data["creator_type"] is not None:
task.creator_type = data["creator_type"]
if "creator_id" in data:
task.creator_id = uuid.UUID(data["creator_id"]) if data["creator_id"] else None
task.creator_id = _to_uuid(data.get("creator_id")) if data["creator_id"] else None
if "parent_task_id" in data:
task.parent_task_id = uuid.UUID(data["parent_task_id"]) if data["parent_task_id"] else None
task.parent_task_id = _to_uuid(data.get("parent_task_id")) if data["parent_task_id"] else None
if "depends_on" in data:
task.depends_on = [str(d) for d in (data["depends_on"] or [])]
if "task_type" in data and data["task_type"] is not None:
@@ -477,6 +496,7 @@ async def assign_task(
# Mirror legacy field for user assignment.
task.assigned_to = assignee_id if assignee_type == "user" else None
await db.flush()
await db.refresh(task)
return _task_to_dict(task)
@@ -509,6 +529,7 @@ async def update_task_status(
if new_status == "done":
from app.core.outbox import enqueue_outbox_event
await enqueue_outbox_event(db, tenant_id, "task.completed", {"task_id": str(task.id), "tenant_id": str(tenant_id), "title": task.title, "assigned_to": str(task.assigned_to) if task.assigned_to else None}, aggregate_type="task", aggregate_id=task.id)
await db.refresh(task)
return _task_to_dict(task)
@@ -562,6 +583,7 @@ async def add_dependency(
current.append(str(depends_on))
task.depends_on = current
await db.flush()
await db.refresh(task)
return _task_to_dict(task)
@@ -580,6 +602,7 @@ async def remove_dependency(
current.remove(str(depends_on))
task.depends_on = current
await db.flush()
await db.refresh(task)
return _task_to_dict(task)