from __future__ import annotations from dataclasses import replace import unittest from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, sessionmaker from govoplan_core.auth import ApiPrincipal 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 ( 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, ) from govoplan_workflow_engine.backend.db.models import ( WorkflowDefinition, WorkflowDefinitionRevision, WorkflowInstance, WorkflowInstanceEvent, 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, WorkflowEdge, WorkflowGraph, WorkflowInstanceStartRequest, WorkflowNode, ) from govoplan_workflow_engine.backend.service import ( WorkflowConflictError, definition_response, derive_definition, update_definition, ) def contribution_graph(*, label: str = "Approve request") -> WorkflowGraph: return WorkflowGraph( nodes=[ WorkflowNode( id="start", type="workflow.start.manual", label="Start", config={"input_schema_ref": ""}, ), WorkflowNode( id="review", type="workflow.activity", label=label, config={ "title": label, "instructions": "", "assignee": "", "due_after": "", }, ), WorkflowNode( id="complete", type="workflow.end.completed", label="Complete", config={"output_mapping": {}}, ), ], edges=[ WorkflowEdge(id="start-review", source="start", target="review"), WorkflowEdge(id="review-complete", source="review", target="complete"), ], ) def contribution( *, version: str = "1.0.0", label: str = "Approve request", ) -> WorkflowDefinitionContribution: return WorkflowDefinitionContribution( origin_module_id="permits", origin_module_version=version, definition_key="application-approval", name="Application approval", description="Module-provided approval baseline.", graph=contribution_graph(label=label).model_dump(mode="json"), allow_automation=False, metadata={"domain": "permits"}, ) def registry_for(item: WorkflowDefinitionContribution) -> PlatformRegistry: registry = PlatformRegistry() registry.register( ModuleManifest( id="permits", name="Permits", version=item.origin_module_version, workflow_definitions=(item,), ) ) registry.validate() return registry def principal() -> ApiPrincipal: return ApiPrincipal( principal=PrincipalRef( account_id="account-1", membership_id="membership-1", tenant_id="tenant-1", scopes=frozenset( { "workflow:definition:read", "workflow:definition:write", "workflow:instance:admin", } ), ), account=object(), user=object(), ) class _DefinitionPolicy: def resolve_definition_action(self, *, request): return PolicyDecision( allowed=( (request.action in {"view", "derive", "reuse"} and request.allow_reuse) or ( request.action == "run" and request.allow_run and request.status == "active" ) ), reason=None, ) class _PolicyRegistry: def has_capability(self, name: str) -> bool: return name == "policy.definitionGovernance" def capability(self, name: str): if not self.has_capability(name): raise KeyError(name) return _DefinitionPolicy() class WorkflowContributionTests(unittest.TestCase): def setUp(self) -> None: self.engine = create_engine("sqlite:///:memory:") Base.metadata.create_all( self.engine, tables=[ WorkflowDefinition.__table__, WorkflowDefinitionRevision.__table__, WorkflowInstance.__table__, WorkflowInstanceStep.__table__, WorkflowInstanceEvent.__table__, ], ) self.Session = sessionmaker(bind=self.engine) self.session: Session = self.Session() def tearDown(self) -> None: self.session.close() Base.metadata.drop_all( self.engine, tables=[ WorkflowInstanceEvent.__table__, WorkflowInstanceStep.__table__, WorkflowInstance.__table__, WorkflowDefinitionRevision.__table__, WorkflowDefinition.__table__, ], ) self.engine.dispose() def test_reconciliation_is_idempotent_and_does_not_auto_activate_updates(self) -> None: first = reconcile_workflow_definition_contributions( self.session, registry=registry_for(contribution()), ) repeated = reconcile_workflow_definition_contributions( self.session, registry=registry_for(contribution()), ) upgraded = reconcile_workflow_definition_contributions( self.session, registry=registry_for( contribution(version="1.1.0", label="Approve corrected request") ), ) self.session.commit() definition = self.session.scalar(select(WorkflowDefinition)) self.assertIsNotNone(definition) assert definition is not None self.assertEqual(1, first["created"]) self.assertEqual(1, repeated["unchanged"]) self.assertEqual(1, upgraded["updated"]) self.assertEqual(2, definition.current_revision) self.assertEqual(1, definition.active_revision) self.assertEqual("permits", definition.standard_origin_module_id) self.assertEqual("1.1.0", definition.standard_origin_module_version) self.assertEqual(2, len(definition.revisions)) self.assertNotEqual( definition.revisions[0].contribution_hash, 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, registry=registry_for(contribution()), ) baseline = self.session.scalar(select(WorkflowDefinition)) assert baseline is not None with self.assertRaisesRegex(WorkflowConflictError, "immutable"): update_definition( self.session, tenant_id="tenant-1", definition_id=baseline.id, actor_id="account-1", payload=WorkflowDefinitionUpdateRequest( name=baseline.name, graph=contribution_graph(), expected_revision=baseline.current_revision, scope_type="system", ), ) 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", ), ) initial = definition_response( self.session, override, principal=principal(), registry=_PolicyRegistry(), ) self.assertIsNotNone(initial.standard) assert initial.standard is not None self.assertFalse(initial.standard.update_available) reconcile_workflow_definition_contributions( self.session, registry=registry_for( contribution(version="1.1.0", label="Approve corrected request") ), ) drifted = definition_response( self.session, override, principal=principal(), registry=_PolicyRegistry(), ) self.assertIsNotNone(drifted.standard) assert drifted.standard is not None self.assertTrue(drifted.standard.update_available) restored = reset_workflow_override_to_standard( self.session, tenant_id="tenant-1", definition_id=override.id, actor_id="account-1", ) self.session.commit() self.assertEqual(baseline.id, restored.id) self.assertEqual("archived", override.status) self.assertEqual("active", baseline.status) self.assertEqual( baseline.id, override.metadata_["standard_reset"]["baseline_definition_id"], ) def test_missing_requirements_fail_closed_without_creating_a_baseline(self) -> None: item = replace( contribution(), required_capabilities=("permits.approvalAction",), ) result = reconcile_workflow_definition_contributions( self.session, registry=registry_for(item), ) self.assertEqual(1, result["blocked"]) self.assertIsNone(self.session.scalar(select(WorkflowDefinition))) self.assertEqual( ["capability:permits.approvalAction"], 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(), scope_type="tenant", inherit_to_lower_scopes=False, ) registry = registry_for(item) result = reconcile_workflow_definition_contributions( self.session, registry=registry, tenant_ids=("tenant-1",), ) definition = self.session.scalar(select(WorkflowDefinition)) assert definition is not None instance, replayed = start_instance( self.session, tenant_id="tenant-1", definition_id=definition.id, actor_id="account-1", principal=principal(), registry=registry, payload=WorkflowInstanceStartRequest( idempotency_key="headless-domain-run", input={"request_id": "request-1"}, ), ) self.assertEqual(1, result["created"]) self.assertFalse(replayed) 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()