feat(workbench): add runtime GIS and sample integrations
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from app.domain.domain_errors import DomainError
|
||||
from app.domain.schemas.task_alert import TaskAlertEvent
|
||||
from app.services.task_alerts import TaskAlertService
|
||||
|
||||
|
||||
NOW = datetime(2026, 9, 18, 2, 30, tzinfo=timezone.utc)
|
||||
USER_ID = uuid4()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
return "asyncio"
|
||||
|
||||
|
||||
class Repository:
|
||||
def __init__(self):
|
||||
self.events = {}
|
||||
self.alerts = {}
|
||||
|
||||
@asynccontextmanager
|
||||
async def transaction(self):
|
||||
yield self
|
||||
|
||||
async def lock_alert(self, _alert_id):
|
||||
return None
|
||||
|
||||
async def get_event(self, producer_id, event_id):
|
||||
return deepcopy(self.events.get((producer_id, event_id)))
|
||||
|
||||
async def get_alert(self, alert_id):
|
||||
value = self.alerts.get(alert_id)
|
||||
return (value["revision"], deepcopy(value["document"])) if value else None
|
||||
|
||||
async def save_event(self, event, resulting_state):
|
||||
self.events[(event["producer_id"], event["event_id"])] = deepcopy(
|
||||
event | {"resulting_state": resulting_state}
|
||||
)
|
||||
|
||||
async def save_alert(self, state):
|
||||
self.alerts[state["alert_id"]] = {
|
||||
"revision": state["revision"],
|
||||
"document": deepcopy({key: value for key, value in state.items() if key != "revision"}),
|
||||
}
|
||||
|
||||
|
||||
def command(
|
||||
event_type="raised",
|
||||
*,
|
||||
event_id=None,
|
||||
title="Low pressure",
|
||||
producer_id=f"principal:{USER_ID}",
|
||||
):
|
||||
return TaskAlertEvent(
|
||||
event_id=event_id or uuid4(),
|
||||
producer_id=producer_id,
|
||||
alert_id="schedule-1:2026-09-18T02:30:00Z",
|
||||
event_type=event_type,
|
||||
occurred_at=NOW,
|
||||
condition_id=None,
|
||||
source={
|
||||
"task_type": "scheduled_task",
|
||||
"task_id": "schedule-1",
|
||||
"run_id": "occurrence-1",
|
||||
},
|
||||
title=title,
|
||||
description="Scheduled task reported a failure.",
|
||||
severity="warning",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_alert_events_are_idempotent_and_transitions_are_monotonic():
|
||||
repository = Repository()
|
||||
service = TaskAlertService(
|
||||
repository,
|
||||
SimpleNamespace(project_role="member", user_id=USER_ID),
|
||||
clock=lambda: NOW,
|
||||
)
|
||||
raised_command = command()
|
||||
raised = await service.push(raised_command)
|
||||
repeated = await service.push(raised_command)
|
||||
assert raised.status == repeated.status == "active"
|
||||
assert raised.revision == repeated.revision == 1
|
||||
|
||||
acknowledged = await service.push(command("acknowledged"))
|
||||
resolved = await service.push(command("resolved"))
|
||||
assert acknowledged.status == "acknowledged" and acknowledged.revision == 2
|
||||
assert resolved.status == "resolved" and resolved.revision == 3
|
||||
|
||||
with pytest.raises(DomainError) as invalid:
|
||||
await service.push(command("acknowledged"))
|
||||
assert invalid.value.code == "INVALID_TRANSITION"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_alert_event_reuse_and_fact_rewrites_are_rejected():
|
||||
repository = Repository()
|
||||
service = TaskAlertService(
|
||||
repository,
|
||||
SimpleNamespace(project_role="member", user_id=USER_ID),
|
||||
clock=lambda: NOW,
|
||||
)
|
||||
event_id = uuid4()
|
||||
await service.push(command(event_id=event_id))
|
||||
|
||||
with pytest.raises(DomainError) as reused:
|
||||
await service.push(command(event_id=event_id, title="Changed"))
|
||||
assert reused.value.code == "IDEMPOTENCY_CONFLICT"
|
||||
|
||||
with pytest.raises(DomainError) as rewritten:
|
||||
await service.push(command("resolved", title="Changed"))
|
||||
assert rewritten.value.code == "IDEMPOTENCY_CONFLICT"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_internal_alert_producer_is_bound_without_user_membership():
|
||||
repository = Repository()
|
||||
service = TaskAlertService(
|
||||
repository,
|
||||
None,
|
||||
clock=lambda: NOW,
|
||||
internal_producer_id="scheduler:demo",
|
||||
)
|
||||
|
||||
state = await service.push(command(producer_id="scheduler:demo"))
|
||||
assert state.status == "active"
|
||||
|
||||
mismatched = command(producer_id="scheduler:demo")
|
||||
mismatched.producer_id = "another-task"
|
||||
with pytest.raises(DomainError) as denied:
|
||||
await service.push(mismatched)
|
||||
assert denied.value.code == "PERMISSION_DENIED"
|
||||
Reference in New Issue
Block a user