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
+4 -3
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
@@ -242,7 +242,7 @@ async def remove_dependency(
@router.post("/{task_id}/decompose", dependencies=[Depends(require_permission("tasks:write"))])
async def decompose_goal(
task_id: str,
body: list[TaskCreate],
request: Request,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
@@ -250,8 +250,9 @@ async def decompose_goal(
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
tid = _parse_uuid(task_id, "task_id")
body = await request.json()
result = await services.decompose_goal(
db, tenant_id, user_id, tid, [item.model_dump() for item in body]
db, tenant_id, user_id, tid, body
)
if result is None:
raise HTTPException(404, detail={"detail": "Goal not found", "code": "not_found"})
+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)