from __future__ import annotations import unittest from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, sessionmaker from govoplan_core.db.base import Base from govoplan_workflow.backend.db.models import ( WorkflowDefinition, WorkflowDefinitionRevision, ) from govoplan_workflow.backend.schemas import ( WorkflowDefinitionCreateRequest, WorkflowDefinitionUpdateRequest, WorkflowEdge, WorkflowGraph, WorkflowNode, WorkflowPosition, ) from govoplan_workflow.backend.service import ( WorkflowConflictError, WorkflowNotFoundError, activate_definition, create_definition, delete_definition, get_definition, list_definition_revisions, list_definitions, update_definition, ) def sample_graph(*, title: str = "Review request") -> WorkflowGraph: return WorkflowGraph( nodes=[ WorkflowNode( id="start", type="workflow.start.manual", label="Start", position=WorkflowPosition(x=40, y=100), config={"input_schema_ref": ""}, ), WorkflowNode( id="activity", type="workflow.activity", label="Review", position=WorkflowPosition(x=280, y=100), config={ "title": title, "instructions": "", "assignee": "", "due_after": "", }, ), WorkflowNode( id="complete", type="workflow.end.completed", label="Completed", position=WorkflowPosition(x=520, y=100), config={"output_mapping": {}}, ), ], edges=[ WorkflowEdge(id="start-activity", source="start", target="activity"), WorkflowEdge(id="activity-complete", source="activity", target="complete"), ], ) class WorkflowServiceTests(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() def tearDown(self) -> None: self.session.close() Base.metadata.drop_all( self.engine, tables=[ WorkflowDefinitionRevision.__table__, WorkflowDefinition.__table__, ], ) self.engine.dispose() def _create(self, *, tenant_id: str = "tenant-1") -> WorkflowDefinition: definition = create_definition( self.session, tenant_id=tenant_id, actor_id="user-1", payload=WorkflowDefinitionCreateRequest( name="Monthly case handling", graph=sample_graph(), ), ) self.session.commit() return definition def test_create_update_and_activate_pin_immutable_revisions(self) -> None: definition = self._create() updated = update_definition( self.session, tenant_id="tenant-1", definition_id=definition.id, actor_id="user-2", payload=WorkflowDefinitionUpdateRequest( name="Monthly case handling", graph=sample_graph(title="Review corrected request"), expected_revision=1, ), ) activate_definition( self.session, tenant_id="tenant-1", definition_id=definition.id, actor_id="user-2", revision=1, ) self.session.commit() revisions = list_definition_revisions( self.session, definition=updated, ) self.assertEqual(2, updated.current_revision) self.assertEqual(1, updated.active_revision) self.assertEqual("active", updated.status) self.assertEqual([2, 1], [item.revision for item in revisions]) self.assertNotEqual(revisions[0].content_hash, revisions[1].content_hash) historical = next(item for item in revisions if item.revision == 1) self.assertEqual( "Review request", historical.graph["nodes"][1]["config"]["title"], ) def test_metadata_update_does_not_create_graph_revision(self) -> None: definition = self._create() updated = update_definition( self.session, tenant_id="tenant-1", definition_id=definition.id, actor_id="user-2", payload=WorkflowDefinitionUpdateRequest( name="Renamed workflow", description="Updated metadata only", graph=sample_graph(), metadata={"owner": "finance"}, expected_revision=1, ), ) self.session.commit() self.assertEqual(1, updated.current_revision) self.assertEqual( 1, len( list( self.session.scalars( select(WorkflowDefinitionRevision).where( WorkflowDefinitionRevision.definition_id == definition.id ) ) ) ), ) def test_stale_update_and_cross_tenant_access_are_rejected(self) -> None: definition = self._create() with self.assertRaises(WorkflowConflictError): update_definition( self.session, tenant_id="tenant-1", definition_id=definition.id, actor_id="user-2", payload=WorkflowDefinitionUpdateRequest( name="Stale", graph=sample_graph(), expected_revision=2, ), ) with self.assertRaises(WorkflowNotFoundError): get_definition( self.session, tenant_id="tenant-2", definition_id=definition.id, ) def test_soft_delete_preserves_revisions_and_hides_definition(self) -> None: definition = self._create() delete_definition( self.session, tenant_id="tenant-1", definition_id=definition.id, actor_id="user-2", ) self.session.commit() self.assertEqual([], list_definitions(self.session, tenant_id="tenant-1")) self.assertEqual( 1, len( list( self.session.scalars( select(WorkflowDefinitionRevision).where( WorkflowDefinitionRevision.definition_id == definition.id ) ) ) ), ) if __name__ == "__main__": unittest.main()