Install and reconcile module workflow standards
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from sqlalchemy import create_engine, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from govoplan_core.core.configuration_packages import (
|
||||
ConfigurationExportSelection,
|
||||
ConfigurationPackageFragment,
|
||||
ConfigurationPreflightContext,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_workflow_engine.backend.configuration_provider import (
|
||||
WORKFLOW_DEFINITIONS_FRAGMENT,
|
||||
apply_workflow_definitions,
|
||||
export_workflow_definitions,
|
||||
preflight_workflow_definitions,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.db.models import (
|
||||
WorkflowDefinition,
|
||||
WorkflowDefinitionRevision,
|
||||
)
|
||||
|
||||
|
||||
def fragment(*, label: str = "Review request") -> ConfigurationPackageFragment:
|
||||
return ConfigurationPackageFragment(
|
||||
module_id="workflow_engine",
|
||||
fragment_type=WORKFLOW_DEFINITIONS_FRAGMENT,
|
||||
fragment_id="permit-workflows",
|
||||
payload={
|
||||
"origin_module_id": "permits",
|
||||
"origin_module_version": "1.0.0",
|
||||
"items": [
|
||||
{
|
||||
"definition_key": "permit-review",
|
||||
"name": "Permit review",
|
||||
"scope_type": "system",
|
||||
"graph": {
|
||||
"schema_version": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "workflow.start.manual",
|
||||
"label": "Start",
|
||||
"config": {"input_schema_ref": ""},
|
||||
},
|
||||
{
|
||||
"id": "review",
|
||||
"type": "workflow.activity",
|
||||
"label": label,
|
||||
"config": {
|
||||
"title": label,
|
||||
"instructions": "",
|
||||
"assignee": "",
|
||||
"due_after": "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "done",
|
||||
"type": "workflow.end.completed",
|
||||
"label": "Done",
|
||||
"config": {"output_mapping": {}},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "start-review",
|
||||
"source": "start",
|
||||
"target": "review",
|
||||
},
|
||||
{"id": "review-done", "source": "review", "target": "done"},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class WorkflowConfigurationProviderTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(
|
||||
self.engine,
|
||||
tables=[
|
||||
WorkflowDefinition.__table__,
|
||||
WorkflowDefinitionRevision.__table__,
|
||||
],
|
||||
)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
self.session: Session = self.Session()
|
||||
self.context = ConfigurationPreflightContext(
|
||||
installed_modules={"workflow_engine": "0.1.14"},
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.session.close()
|
||||
Base.metadata.drop_all(
|
||||
self.engine,
|
||||
tables=[
|
||||
WorkflowDefinitionRevision.__table__,
|
||||
WorkflowDefinition.__table__,
|
||||
],
|
||||
)
|
||||
self.engine.dispose()
|
||||
|
||||
def test_fragment_preflight_apply_update_and_export(self) -> None:
|
||||
initial = preflight_workflow_definitions(
|
||||
self.session,
|
||||
fragment(),
|
||||
self.context,
|
||||
)
|
||||
created = apply_workflow_definitions(
|
||||
self.session,
|
||||
fragment(),
|
||||
self.context,
|
||||
registry=None,
|
||||
)
|
||||
repeated = apply_workflow_definitions(
|
||||
self.session,
|
||||
fragment(),
|
||||
self.context,
|
||||
registry=None,
|
||||
)
|
||||
updated = apply_workflow_definitions(
|
||||
self.session,
|
||||
fragment(label="Review corrected request"),
|
||||
self.context,
|
||||
registry=None,
|
||||
)
|
||||
exported = export_workflow_definitions(
|
||||
self.session,
|
||||
ConfigurationExportSelection(scopes=("system",)),
|
||||
)
|
||||
|
||||
definition = self.session.scalar(select(WorkflowDefinition))
|
||||
assert definition is not None
|
||||
self.assertEqual("create", initial.plan[0].action)
|
||||
self.assertIn("permit-review", created.created_refs)
|
||||
self.assertFalse(repeated.created_refs)
|
||||
self.assertFalse(repeated.updated_refs)
|
||||
self.assertIn("permit-review", updated.updated_refs)
|
||||
self.assertEqual(2, definition.current_revision)
|
||||
self.assertEqual("workflow_definitions", exported.fragments[0].fragment_type)
|
||||
self.assertEqual(
|
||||
"permit-review",
|
||||
exported.fragments[0].payload["items"][0]["definition_key"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+118
-3
@@ -11,9 +11,15 @@ from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.modules import ModuleManifest
|
||||
from govoplan_core.core.policy import PolicyDecision
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.workflows import WorkflowDefinitionContribution
|
||||
from govoplan_core.core.workflows import (
|
||||
WorkflowCurrentStepResolution,
|
||||
WorkflowDefinitionContribution,
|
||||
WorkflowStandardStartRequest,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_workflow_engine.backend.contributions import (
|
||||
SqlWorkflowDefinitionContributionProvider,
|
||||
compare_workflow_override_to_standard,
|
||||
reconcile_workflow_definition_contributions,
|
||||
reset_workflow_override_to_standard,
|
||||
)
|
||||
@@ -25,6 +31,9 @@ from govoplan_workflow_engine.backend.db.models import (
|
||||
WorkflowInstanceStep,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.instance_service import start_instance
|
||||
from govoplan_workflow_engine.backend.orchestration import (
|
||||
SqlWorkflowOrchestrationProvider,
|
||||
)
|
||||
from govoplan_workflow_engine.backend.schemas import (
|
||||
WorkflowDefinitionDeriveRequest,
|
||||
WorkflowDefinitionUpdateRequest,
|
||||
@@ -129,8 +138,12 @@ class _DefinitionPolicy:
|
||||
def resolve_definition_action(self, *, request):
|
||||
return PolicyDecision(
|
||||
allowed=(
|
||||
request.action in {"view", "derive", "reuse"}
|
||||
and request.allow_reuse
|
||||
(request.action in {"view", "derive", "reuse"} and request.allow_reuse)
|
||||
or (
|
||||
request.action == "run"
|
||||
and request.allow_run
|
||||
and request.status == "active"
|
||||
)
|
||||
),
|
||||
reason=None,
|
||||
)
|
||||
@@ -209,6 +222,17 @@ class WorkflowContributionTests(unittest.TestCase):
|
||||
definition.revisions[1].contribution_hash,
|
||||
)
|
||||
|
||||
def test_provider_defers_until_engine_schema_is_available(self) -> None:
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with Session(engine) as empty_session:
|
||||
result = SqlWorkflowDefinitionContributionProvider(
|
||||
registry=registry_for(contribution())
|
||||
).reconcile(empty_session)
|
||||
engine.dispose()
|
||||
|
||||
self.assertTrue(result["skipped"])
|
||||
self.assertEqual("schema_unavailable", result["reason"])
|
||||
|
||||
def test_standard_is_immutable_and_reset_archives_only_the_override(self) -> None:
|
||||
reconcile_workflow_definition_contributions(
|
||||
self.session,
|
||||
@@ -300,6 +324,61 @@ class WorkflowContributionTests(unittest.TestCase):
|
||||
result["items"][0]["missing"], # type: ignore[index]
|
||||
)
|
||||
|
||||
def test_semantic_three_way_diff_identifies_overlapping_node_changes(self) -> None:
|
||||
reconcile_workflow_definition_contributions(
|
||||
self.session,
|
||||
registry=registry_for(contribution()),
|
||||
)
|
||||
baseline = self.session.scalar(select(WorkflowDefinition))
|
||||
assert baseline is not None
|
||||
override = derive_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
principal=principal(),
|
||||
registry=_PolicyRegistry(),
|
||||
source_definition_id=baseline.id,
|
||||
payload=WorkflowDefinitionDeriveRequest(
|
||||
name="Tenant approval override",
|
||||
scope_type="tenant",
|
||||
),
|
||||
)
|
||||
local_graph = contribution_graph(label="Local approval wording")
|
||||
update_definition(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=override.id,
|
||||
actor_id="account-1",
|
||||
payload=WorkflowDefinitionUpdateRequest(
|
||||
name=override.name,
|
||||
graph=local_graph,
|
||||
expected_revision=override.current_revision,
|
||||
),
|
||||
)
|
||||
reconcile_workflow_definition_contributions(
|
||||
self.session,
|
||||
registry=registry_for(
|
||||
contribution(version="1.1.0", label="Upstream approval wording")
|
||||
),
|
||||
)
|
||||
|
||||
comparison = compare_workflow_override_to_standard(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
definition_id=override.id,
|
||||
)
|
||||
|
||||
review = next(
|
||||
item
|
||||
for item in comparison.items
|
||||
if item.resource_type == "node" and item.resource_id == "review"
|
||||
)
|
||||
self.assertEqual("conflict", review.state)
|
||||
self.assertEqual("manual_resolution", review.recommended_action)
|
||||
self.assertIn("label", review.changed_fields)
|
||||
self.assertEqual(1, comparison.conflict_count)
|
||||
self.assertFalse(comparison.auto_mergeable)
|
||||
|
||||
def test_domain_contribution_runs_without_the_editor_module(self) -> None:
|
||||
item = replace(
|
||||
contribution(),
|
||||
@@ -333,6 +412,42 @@ class WorkflowContributionTests(unittest.TestCase):
|
||||
self.assertEqual("waiting", instance.status)
|
||||
self.assertEqual(definition.revisions[0].id, instance.definition_revision_id)
|
||||
|
||||
def test_headless_orchestration_starts_and_advances_a_standard(self) -> None:
|
||||
registry = registry_for(contribution())
|
||||
reconcile_workflow_definition_contributions(
|
||||
self.session,
|
||||
registry=registry,
|
||||
)
|
||||
provider = SqlWorkflowOrchestrationProvider(registry=_PolicyRegistry())
|
||||
|
||||
started = provider.start_standard(
|
||||
self.session,
|
||||
principal(),
|
||||
request=WorkflowStandardStartRequest(
|
||||
tenant_id="tenant-1",
|
||||
origin_module_id="permits",
|
||||
definition_key="application-approval",
|
||||
idempotency_key="approval-1",
|
||||
actor_id="account-1",
|
||||
input={"request_id": "request-1"},
|
||||
),
|
||||
)
|
||||
completed = provider.resolve_current_step(
|
||||
self.session,
|
||||
principal(),
|
||||
tenant_id="tenant-1",
|
||||
instance_id=started.id,
|
||||
resolution=WorkflowCurrentStepResolution(
|
||||
action="complete",
|
||||
expected_step_id=started.current_step_id,
|
||||
actor_id="account-1",
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual("review", started.current_node_id)
|
||||
self.assertEqual("completed", completed.status)
|
||||
self.assertIsNone(completed.current_step_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user