241 lines
8.1 KiB
Python
241 lines
8.1 KiB
Python
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()
|