from __future__ import annotations import unittest from govoplan_core.core.automation import ( ActionDefinition, ActionEffectProvider, ActionExecutionRequest, ActionExecutionResult, ActionPreview, CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER, AutomationInvocation, AutomationPrincipalProvider, AutomationPrincipalRequest, AutomationPrincipalResolution, EffectDefinition, EffectPreview, ObservedEffect, action_effect_provider, automation_principal_provider, ) from govoplan_core.core.modules import ModuleContext, ModuleManifest from govoplan_core.core.registry import PlatformRegistry class _Provider: def resolve_automation_principal(self, session, *, request): del session return AutomationPrincipalResolution( allowed=True, principal={"account_id": request.account_id}, granted_scopes=request.grant_scopes, ) class _ActionProvider: action = ActionDefinition( action_key="postbox.message.deliver", owner_module="postbox", description="Deliver one governed Postbox message.", input_schema_ref="schema:postbox.message.deliver@1", required_scopes=("postbox:message:write",), required_capabilities=("postbox.delivery",), risk_level="high", reversibility="compensatable", expected_effect_keys=("postbox.message.created",), audit_event_types=("postbox.message.delivered",), ) effect = EffectDefinition( effect_key="postbox.message.created", owner_module="postbox", operation="created", description="A durable Postbox message was created.", resource_types=("postbox_message",), ) def action_definitions(self): return (self.action,) def effect_definitions(self): return (self.effect,) def preview_action(self, session, principal, *, request): del session, principal return ActionPreview( action_key=request.action_key, allowed=True, summary="One Postbox message will be created.", risk_level=self.action.risk_level, reversibility=self.action.reversibility, effects=( EffectPreview( effect_key=self.effect.effect_key, summary="Create message.", ), ), preview_ref="preview:1", ) def execute_action(self, session, principal, *, request): del session, principal return ActionExecutionResult( state="completed", output={"message_ref": "postbox-message:1"}, observed_effects=( ObservedEffect( effect_key=self.effect.effect_key, operation="created", resource_ref="postbox-message:1", ), ), audit_event_refs=("audit-event:1",), ) class AutomationContractTests(unittest.TestCase): def test_principal_request_has_explicit_subject_contracts(self) -> None: service = AutomationPrincipalRequest.service_account( tenant_id="tenant-1", service_account_id="service-1", authorization_ref="trigger:1", grant_scopes=("dataflow:pipeline:run",), ) self.assertEqual("service_account", service.subject_kind) self.assertEqual("service-1", service.service_account_id) self.assertIsNone(service.account_id) with self.assertRaisesRegex( ValueError, "Delegated-user automation", ): AutomationPrincipalRequest( tenant_id="tenant-1", authorization_ref="trigger:1", grant_scopes=("dataflow:pipeline:run",), ) def test_invocation_records_stable_trigger_provenance(self) -> None: invocation = AutomationInvocation( kind="event", trigger_ref="dataflow-trigger:1", event_id="event-1", event_type="files.uploaded", ) self.assertEqual("event", invocation.kind) self.assertEqual("event-1", invocation.event_id) def test_principal_provider_is_runtime_resolved(self) -> None: provider = _Provider() self.assertIsInstance(provider, AutomationPrincipalProvider) registry = PlatformRegistry() registry.register( ModuleManifest( id="automation_contract_test", name="Automation contract test", version="test", capability_factories={ CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER: ( lambda context: provider ), }, ) ) registry.configure_capability_context( ModuleContext(registry=registry, settings=object()) ) self.assertIs(provider, automation_principal_provider(registry)) result = provider.resolve_automation_principal( object(), request=AutomationPrincipalRequest( tenant_id="tenant-1", account_id="account-1", membership_id="membership-1", authorization_ref="trigger:1", grant_scopes=("dataflow:pipeline:run",), ), ) self.assertTrue(result.allowed) self.assertEqual(("dataflow:pipeline:run",), result.granted_scopes) def test_action_effect_provider_is_previewable_and_idempotent_by_contract( self, ) -> None: provider = _ActionProvider() self.assertIsInstance(provider, ActionEffectProvider) request = ActionExecutionRequest( tenant_id="tenant-1", action_key=provider.action.action_key, input={"postbox_ref": "postbox:1"}, idempotency_key="workflow:instance-1:step-2:attempt-1", invocation=AutomationInvocation( kind="workflow", trigger_ref="workflow-instance:1", ), ) preview = provider.preview_action(object(), object(), request=request) result = provider.execute_action(object(), object(), request=request) self.assertTrue(preview.allowed) self.assertEqual("compensatable", preview.reversibility) self.assertEqual("completed", result.state) self.assertEqual( "postbox-message:1", result.observed_effects[0].resource_ref, ) def test_action_effect_provider_is_resolved_by_capability_name(self) -> None: provider = _ActionProvider() registry = PlatformRegistry() registry.register( ModuleManifest( id="action_contract_test", name="Action contract test", version="test", capability_factories={ "postbox.actions": lambda context: provider, "invalid.actions": lambda context: object(), }, ) ) registry.configure_capability_context( ModuleContext(registry=registry, settings=object()) ) self.assertIs( provider, action_effect_provider(registry, "postbox.actions"), ) self.assertIsNone( action_effect_provider(registry, "invalid.actions") ) self.assertIsNone( action_effect_provider(registry, "missing.actions") ) def test_action_contract_rejects_unversioned_or_incomplete_definitions( self, ) -> None: with self.assertRaisesRegex(ValueError, "input schema"): ActionDefinition( action_key="invalid", owner_module="test", description="Invalid action", input_schema_ref="", ) with self.assertRaisesRegex(ValueError, "contract version"): EffectDefinition( effect_key="test.effect", owner_module="test", operation="changed", description="Test effect", contract_version="2", ) if __name__ == "__main__": unittest.main()