feat(api): add previous_in_progress_at tracking and update task logic for review status
This commit is contained in:
@@ -1405,6 +1405,7 @@ async def update_task(
|
|||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
previous_status=previous_status,
|
previous_status=previous_status,
|
||||||
previous_assigned=previous_assigned,
|
previous_assigned=previous_assigned,
|
||||||
|
previous_in_progress_at=task.in_progress_at,
|
||||||
status_requested=(requested_status is not None and requested_status != previous_status),
|
status_requested=(requested_status is not None and requested_status != previous_status),
|
||||||
updates=updates,
|
updates=updates,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
@@ -1669,6 +1670,7 @@ class _TaskUpdateInput:
|
|||||||
tag_ids: list[UUID] | None
|
tag_ids: list[UUID] | None
|
||||||
custom_field_values: TaskCustomFieldValues
|
custom_field_values: TaskCustomFieldValues
|
||||||
custom_field_values_set: bool
|
custom_field_values_set: bool
|
||||||
|
previous_in_progress_at: datetime | None = None
|
||||||
normalized_tag_ids: list[UUID] | None = None
|
normalized_tag_ids: list[UUID] | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -2136,6 +2138,9 @@ async def _apply_non_lead_agent_task_rules(
|
|||||||
if status_value == "inbox":
|
if status_value == "inbox":
|
||||||
update.task.assigned_agent_id = None
|
update.task.assigned_agent_id = None
|
||||||
update.task.in_progress_at = None
|
update.task.in_progress_at = None
|
||||||
|
elif status_value == "review":
|
||||||
|
update.task.assigned_agent_id = None
|
||||||
|
update.task.in_progress_at = None
|
||||||
else:
|
else:
|
||||||
update.task.assigned_agent_id = update.actor.agent.id if update.actor.agent else None
|
update.task.assigned_agent_id = update.actor.agent.id if update.actor.agent else None
|
||||||
if status_value == "in_progress":
|
if status_value == "in_progress":
|
||||||
@@ -2346,11 +2351,17 @@ async def _finalize_updated_task(
|
|||||||
# ensure reviewers get context on readiness.
|
# ensure reviewers get context on readiness.
|
||||||
if status_raw == "review":
|
if status_raw == "review":
|
||||||
comment_text = (update.comment or "").strip()
|
comment_text = (update.comment or "").strip()
|
||||||
|
review_comment_author = update.task.assigned_agent_id or update.previous_assigned
|
||||||
|
review_comment_since = (
|
||||||
|
update.task.in_progress_at
|
||||||
|
if update.task.in_progress_at is not None
|
||||||
|
else update.previous_in_progress_at
|
||||||
|
)
|
||||||
if not comment_text and not await has_valid_recent_comment(
|
if not comment_text and not await has_valid_recent_comment(
|
||||||
session,
|
session,
|
||||||
update.task,
|
update.task,
|
||||||
update.task.assigned_agent_id,
|
review_comment_author,
|
||||||
update.task.in_progress_at,
|
review_comment_since,
|
||||||
):
|
):
|
||||||
raise _comment_validation_error()
|
raise _comment_validation_error()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|||||||
|
|
||||||
from app.api import tasks as tasks_api
|
from app.api import tasks as tasks_api
|
||||||
from app.api.deps import ActorContext
|
from app.api.deps import ActorContext
|
||||||
|
from app.core.time import utcnow
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.boards import Board
|
from app.models.boards import Board
|
||||||
from app.models.gateways import Gateway
|
from app.models.gateways import Gateway
|
||||||
@@ -327,3 +328,226 @@ async def test_non_lead_agent_forbidden_for_lead_only_patch_fields() -> None:
|
|||||||
assert exc.value.detail["code"] == "task_update_field_forbidden"
|
assert exc.value.detail["code"] == "task_update_field_forbidden"
|
||||||
finally:
|
finally:
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_lead_agent_moves_task_to_review_and_task_unassigns() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
try:
|
||||||
|
async with await _make_session(engine) as session:
|
||||||
|
org_id = uuid4()
|
||||||
|
board_id = uuid4()
|
||||||
|
gateway_id = uuid4()
|
||||||
|
worker_id = uuid4()
|
||||||
|
task_id = uuid4()
|
||||||
|
|
||||||
|
session.add(Organization(id=org_id, name="org"))
|
||||||
|
session.add(
|
||||||
|
Gateway(
|
||||||
|
id=gateway_id,
|
||||||
|
organization_id=org_id,
|
||||||
|
name="gateway",
|
||||||
|
url="https://gateway.local",
|
||||||
|
workspace_root="/tmp/workspace",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Board(
|
||||||
|
id=board_id,
|
||||||
|
organization_id=org_id,
|
||||||
|
name="board",
|
||||||
|
slug="board",
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Agent(
|
||||||
|
id=worker_id,
|
||||||
|
name="worker",
|
||||||
|
board_id=board_id,
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
status="online",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Task(
|
||||||
|
id=task_id,
|
||||||
|
board_id=board_id,
|
||||||
|
title="assigned task",
|
||||||
|
description="",
|
||||||
|
status="in_progress",
|
||||||
|
assigned_agent_id=worker_id,
|
||||||
|
in_progress_at=utcnow(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first()
|
||||||
|
assert task is not None
|
||||||
|
actor = (await session.exec(select(Agent).where(col(Agent.id) == worker_id))).first()
|
||||||
|
assert actor is not None
|
||||||
|
|
||||||
|
updated = await tasks_api.update_task(
|
||||||
|
payload=TaskUpdate(status="review", comment="Moving to review."),
|
||||||
|
task=task,
|
||||||
|
session=session,
|
||||||
|
actor=ActorContext(actor_type="agent", agent=actor),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated.status == "review"
|
||||||
|
assert updated.assigned_agent_id is None
|
||||||
|
assert updated.in_progress_at is None
|
||||||
|
finally:
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_lead_agent_comment_in_review_without_status_does_not_reassign() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
try:
|
||||||
|
async with await _make_session(engine) as session:
|
||||||
|
org_id = uuid4()
|
||||||
|
board_id = uuid4()
|
||||||
|
gateway_id = uuid4()
|
||||||
|
assignee_id = uuid4()
|
||||||
|
commentator_id = uuid4()
|
||||||
|
task_id = uuid4()
|
||||||
|
|
||||||
|
session.add(Organization(id=org_id, name="org"))
|
||||||
|
session.add(
|
||||||
|
Gateway(
|
||||||
|
id=gateway_id,
|
||||||
|
organization_id=org_id,
|
||||||
|
name="gateway",
|
||||||
|
url="https://gateway.local",
|
||||||
|
workspace_root="/tmp/workspace",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Board(
|
||||||
|
id=board_id,
|
||||||
|
organization_id=org_id,
|
||||||
|
name="board",
|
||||||
|
slug="board",
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Agent(
|
||||||
|
id=assignee_id,
|
||||||
|
name="assignee",
|
||||||
|
board_id=board_id,
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
status="online",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Agent(
|
||||||
|
id=commentator_id,
|
||||||
|
name="commentator",
|
||||||
|
board_id=board_id,
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
status="online",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Task(
|
||||||
|
id=task_id,
|
||||||
|
board_id=board_id,
|
||||||
|
title="review task",
|
||||||
|
description="",
|
||||||
|
status="review",
|
||||||
|
assigned_agent_id=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first()
|
||||||
|
assert task is not None
|
||||||
|
commentator = (
|
||||||
|
await session.exec(select(Agent).where(col(Agent.id) == commentator_id))
|
||||||
|
).first()
|
||||||
|
assert commentator is not None
|
||||||
|
|
||||||
|
updated = await tasks_api.update_task(
|
||||||
|
payload=TaskUpdate(comment="I can help with this."),
|
||||||
|
task=task,
|
||||||
|
session=session,
|
||||||
|
actor=ActorContext(actor_type="agent", actor=commentator),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert updated.status == "review"
|
||||||
|
assert updated.assigned_agent_id is None
|
||||||
|
finally:
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_lead_agent_moves_to_review_without_comment_or_recent_comment_fails() -> None:
|
||||||
|
engine = await _make_engine()
|
||||||
|
try:
|
||||||
|
async with await _make_session(engine) as session:
|
||||||
|
org_id = uuid4()
|
||||||
|
board_id = uuid4()
|
||||||
|
gateway_id = uuid4()
|
||||||
|
worker_id = uuid4()
|
||||||
|
task_id = uuid4()
|
||||||
|
|
||||||
|
session.add(Organization(id=org_id, name="org"))
|
||||||
|
session.add(
|
||||||
|
Gateway(
|
||||||
|
id=gateway_id,
|
||||||
|
organization_id=org_id,
|
||||||
|
name="gateway",
|
||||||
|
url="https://gateway.local",
|
||||||
|
workspace_root="/tmp/workspace",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Board(
|
||||||
|
id=board_id,
|
||||||
|
organization_id=org_id,
|
||||||
|
name="board",
|
||||||
|
slug="board",
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Agent(
|
||||||
|
id=worker_id,
|
||||||
|
name="worker",
|
||||||
|
board_id=board_id,
|
||||||
|
gateway_id=gateway_id,
|
||||||
|
status="online",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
Task(
|
||||||
|
id=task_id,
|
||||||
|
board_id=board_id,
|
||||||
|
title="assigned task",
|
||||||
|
description="",
|
||||||
|
status="in_progress",
|
||||||
|
assigned_agent_id=worker_id,
|
||||||
|
in_progress_at=utcnow(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
task = (await session.exec(select(Task).where(col(Task.id) == task_id))).first()
|
||||||
|
assert task is not None
|
||||||
|
actor = (await session.exec(select(Agent).where(col(Agent.id) == worker_id))).first()
|
||||||
|
assert actor is not None
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await tasks_api.update_task(
|
||||||
|
payload=TaskUpdate(status="review"),
|
||||||
|
task=task,
|
||||||
|
session=session,
|
||||||
|
actor=ActorContext(actor_type="agent", agent=actor),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc.value.status_code == 422
|
||||||
|
assert exc.value.detail == "Comment is required."
|
||||||
|
finally:
|
||||||
|
await engine.dispose()
|
||||||
|
|||||||
Reference in New Issue
Block a user