Install and reconcile module workflow standards

This commit is contained in:
2026-07-31 19:40:04 +02:00
parent 9bccbd68da
commit 43c1f3fb72
8 changed files with 1289 additions and 5 deletions
+118 -3
View File
@@ -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()