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
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -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"})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -521,6 +521,9 @@ async def dms_app(engine: AsyncEngine, redis_client):
|
||||
container = get_container()
|
||||
await container.initialize()
|
||||
|
||||
from app.plugins.builtins.permissions.plugin import PermissionsPlugin
|
||||
from app.plugins.builtins.dms.plugin import DmsPlugin
|
||||
from app.plugins.builtins.tasks.plugin import TasksPlugin
|
||||
registry.register_plugin(PermissionsPlugin())
|
||||
registry.register_plugin(DmsPlugin())
|
||||
registry.register_plugin(TasksPlugin())
|
||||
@@ -633,6 +636,9 @@ async def mcp_app(engine: AsyncEngine, redis_client):
|
||||
container = get_container()
|
||||
await container.initialize()
|
||||
|
||||
from app.plugins.builtins.permissions.plugin import PermissionsPlugin
|
||||
from app.plugins.builtins.mcp_server.plugin import McpServerPlugin
|
||||
from app.plugins.builtins.mcp_client.plugin import McpClientPlugin
|
||||
registry.register_plugin(PermissionsPlugin())
|
||||
registry.register_plugin(McpServerPlugin())
|
||||
registry.register_plugin(McpClientPlugin())
|
||||
@@ -692,6 +698,8 @@ async def tasks_app(engine: AsyncEngine, redis_client):
|
||||
container = get_container()
|
||||
await container.initialize()
|
||||
|
||||
from app.plugins.builtins.permissions.plugin import PermissionsPlugin
|
||||
from app.plugins.builtins.tasks.plugin import TasksPlugin
|
||||
registry.register_plugin(PermissionsPlugin())
|
||||
registry.register_plugin(TasksPlugin())
|
||||
reset_plugin_service_for_testing(registry)
|
||||
|
||||
Reference in New Issue
Block a user