Project durable workflow handoffs as work items

This commit is contained in:
2026-08-06 16:06:17 +02:00
parent 0259eea702
commit 9174e07118
10 changed files with 850 additions and 49 deletions
+104 -3
View File
@@ -43,6 +43,7 @@ from govoplan_core.core.runtime_coordination import (
RuntimeIdentity,
bind_process_runtime_identity,
)
from govoplan_core.core.tasks import WorkItemQuery
from govoplan_core.db.base import Base
from govoplan_core.db.base import utcnow
from govoplan_workflow_engine.backend.db.models import (
@@ -84,6 +85,7 @@ from govoplan_workflow_engine.backend.service import (
create_definition,
)
from govoplan_workflow_engine.backend.service_launcher import WorkflowServiceLauncher
from govoplan_workflow_engine.backend.work_items import WorkflowWorkItemProvider
try:
from test_bpmn import NATIVE_BPMN
@@ -687,6 +689,107 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
start_origin="api",
)
def test_human_handoff_projects_typed_due_work_and_disappears_on_completion(
self,
) -> None:
definition = create_definition(
self.session,
tenant_id="tenant-1",
actor_id="account-1",
payload=WorkflowDefinitionCreateRequest(
name="Guided case review",
graph=WorkflowGraph(
nodes=[
WorkflowNode(
id="start",
type="workflow.start.manual",
config={"input_schema_ref": ""},
),
WorkflowNode(
id="activity",
type="workflow.activity",
config={
"title": "Assess the application",
"instructions": "Record the assessment evidence.",
"assignee": "account:account-1",
"due_after": "2h",
},
),
WorkflowNode(
id="done",
type="workflow.end.completed",
),
],
edges=[
WorkflowEdge(
id="start-activity", source="start", target="activity"
),
WorkflowEdge(
id="activity-done", source="activity", target="done"
),
],
),
execution_mode="guided",
),
)
activate_definition(
self.session,
tenant_id="tenant-1",
definition_id=definition.id,
actor_id="account-1",
)
before = utcnow()
instance, _replayed = start_instance(
self.session,
tenant_id="tenant-1",
definition_id=definition.id,
actor_id="account-1",
principal=principal(),
registry=self.registry,
payload=WorkflowInstanceStartRequest(idempotency_key="guided-work-1"),
)
step = self.session.get(WorkflowInstanceStep, instance.current_step_id)
self.assertIsNotNone(step)
assert step is not None
self.assertEqual("account", step.work_assignment_kind)
self.assertEqual("account-1", step.work_assignment_id)
self.assertIsNotNone(step.work_due_at)
assert step.work_due_at is not None
due_at = (
step.work_due_at.replace(tzinfo=UTC)
if step.work_due_at.tzinfo is None
else step.work_due_at
)
self.assertGreaterEqual(due_at, before + timedelta(hours=1, minutes=59))
provider = WorkflowWorkItemProvider(registry=self.registry)
page = provider.list_items(
self.session,
principal(),
query=WorkItemQuery(tenant_id="tenant-1"),
)
self.assertEqual(1, page.total)
self.assertEqual("Assess the application", page.items[0].title)
self.assertEqual("account-1", page.items[0].assignments[0].id)
self.assertEqual(step.id, page.items[0].id)
resolve_step(
self.session,
tenant_id="tenant-1",
instance_id=instance.id,
step_id=step.id,
actor_id="account-1",
principal=principal(),
registry=self.registry,
payload=WorkflowStepActionRequest(action="complete"),
)
closed_page = provider.list_items(
self.session,
principal(),
query=WorkItemQuery(tenant_id="tenant-1"),
)
self.assertEqual(0, closed_page.total)
def test_module_action_records_effects_and_completes_idempotently(
self,
) -> None:
@@ -990,9 +1093,7 @@ class WorkflowInstanceServiceTests(unittest.TestCase):
request=request,
)
checkpoint = session.scalar(
select(RecoveryCheckpoint).order_by(
RecoveryCheckpoint.sequence
)
select(RecoveryCheckpoint).order_by(RecoveryCheckpoint.sequence)
)
assert checkpoint is not None
checkpoint.summary = "tampered provider evidence"