generated from coulomb/repo-seed
Add admin sync hot reload path
This commit is contained in:
114
tests/test_admin_sync_api.py
Normal file
114
tests/test_admin_sync_api.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from activity_core import api
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_sync_definitions_only_does_not_require_temporal(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
seen: dict[str, Any] = {}
|
||||
|
||||
async def fake_run_sync(**kwargs: Any) -> dict[str, Any]:
|
||||
seen.update(kwargs)
|
||||
return {"ok": True, "ran": {"definitions": True}}
|
||||
|
||||
monkeypatch.setattr(api, "_session_factory", object())
|
||||
monkeypatch.setattr(api, "_temporal_client", None)
|
||||
monkeypatch.setattr(api, "run_sync", fake_run_sync)
|
||||
|
||||
result = await api.admin_sync(
|
||||
definitions=True,
|
||||
schedules=False,
|
||||
event_types=False,
|
||||
)
|
||||
|
||||
assert result == {"ok": True, "ran": {"definitions": True}}
|
||||
assert seen["session_factory"] is api._session_factory
|
||||
assert seen["temporal_client"] is None
|
||||
assert seen["definitions"] is True
|
||||
assert seen["schedules"] is False
|
||||
assert seen["event_types"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_sync_schedules_only_passes_temporal(monkeypatch) -> None:
|
||||
temporal = object()
|
||||
seen: dict[str, Any] = {}
|
||||
|
||||
async def fake_run_sync(**kwargs: Any) -> dict[str, Any]:
|
||||
seen.update(kwargs)
|
||||
return {
|
||||
"ok": True,
|
||||
"schedules": {
|
||||
"upserted": 1,
|
||||
"paused": 0,
|
||||
"deleted_orphans": 0,
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(api, "_session_factory", object())
|
||||
monkeypatch.setattr(api, "_temporal_client", temporal)
|
||||
monkeypatch.setattr(api, "run_sync", fake_run_sync)
|
||||
|
||||
result = await api.admin_sync(
|
||||
definitions=False,
|
||||
schedules=True,
|
||||
event_types=False,
|
||||
)
|
||||
|
||||
assert result["schedules"]["upserted"] == 1
|
||||
assert seen["temporal_client"] is temporal
|
||||
assert seen["definitions"] is False
|
||||
assert seen["schedules"] is True
|
||||
assert seen["event_types"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_sync_all_sync_returns_failure_result(monkeypatch) -> None:
|
||||
async def fake_run_sync(**kwargs: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"ok": False,
|
||||
"ran": {
|
||||
"definitions": kwargs["definitions"],
|
||||
"schedules": kwargs["schedules"],
|
||||
"event_types": kwargs["event_types"],
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"stage": "event_types",
|
||||
"type": "RuntimeError",
|
||||
"message": "bad event type",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(api, "_session_factory", object())
|
||||
monkeypatch.setattr(api, "_temporal_client", object())
|
||||
monkeypatch.setattr(api, "run_sync", fake_run_sync)
|
||||
|
||||
result = await api.admin_sync(
|
||||
definitions=True,
|
||||
schedules=True,
|
||||
event_types=True,
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"ok": False,
|
||||
"ran": {
|
||||
"definitions": True,
|
||||
"schedules": True,
|
||||
"event_types": True,
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"stage": "event_types",
|
||||
"type": "RuntimeError",
|
||||
"message": "bad event type",
|
||||
}
|
||||
],
|
||||
}
|
||||
126
tests/test_sync_schedules.py
Normal file
126
tests/test_sync_schedules.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from activity_core import sync_schedules
|
||||
|
||||
|
||||
def _row(
|
||||
*,
|
||||
activity_id: uuid.UUID,
|
||||
enabled: bool,
|
||||
trigger_config: dict[str, Any],
|
||||
) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
id=activity_id,
|
||||
name=f"definition-{activity_id}",
|
||||
enabled=enabled,
|
||||
trigger_config=trigger_config,
|
||||
context_sources=[],
|
||||
task_templates=[],
|
||||
dedupe_key_strategy="skip",
|
||||
version=1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_schedule_rows_reports_drift_counts_and_preserves_one_shots(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
new_id = uuid.uuid4()
|
||||
disabled_old_id = uuid.uuid4()
|
||||
one_shot_id = uuid.uuid4()
|
||||
orphan_id = uuid.uuid4()
|
||||
upserted: list[tuple[uuid.UUID, bool, str]] = []
|
||||
deleted: list[str] = []
|
||||
|
||||
async def fake_upsert_schedule(client: object, defn: object) -> None:
|
||||
upserted.append((
|
||||
defn.id,
|
||||
defn.enabled,
|
||||
defn.trigger_config.trigger_type,
|
||||
))
|
||||
|
||||
async def fake_list_schedules(client: object) -> list[dict[str, str]]:
|
||||
return [
|
||||
{
|
||||
"schedule_id": f"activity-schedule-{disabled_old_id}",
|
||||
"activity_id": str(disabled_old_id),
|
||||
},
|
||||
{
|
||||
"schedule_id": f"activity-schedule-{one_shot_id}-once",
|
||||
"activity_id": f"{one_shot_id}-once",
|
||||
},
|
||||
{
|
||||
"schedule_id": f"activity-schedule-{orphan_id}",
|
||||
"activity_id": str(orphan_id),
|
||||
},
|
||||
]
|
||||
|
||||
async def fake_delete_schedule(client: object, activity_id: str) -> None:
|
||||
deleted.append(activity_id)
|
||||
|
||||
monkeypatch.setattr(sync_schedules, "upsert_schedule", fake_upsert_schedule)
|
||||
monkeypatch.setattr(sync_schedules, "list_schedules", fake_list_schedules)
|
||||
monkeypatch.setattr(sync_schedules, "delete_schedule", fake_delete_schedule)
|
||||
|
||||
result = await sync_schedules.sync_schedule_rows(
|
||||
object(),
|
||||
[
|
||||
_row(
|
||||
activity_id=new_id,
|
||||
enabled=True,
|
||||
trigger_config={
|
||||
"trigger_type": "cron",
|
||||
"cron_expression": "20 7 * * *",
|
||||
"timezone": "Europe/Berlin",
|
||||
"misfire_policy": "skip",
|
||||
},
|
||||
),
|
||||
_row(
|
||||
activity_id=disabled_old_id,
|
||||
enabled=False,
|
||||
trigger_config={
|
||||
"trigger_type": "cron",
|
||||
"cron_expression": "20 * * * *",
|
||||
"timezone": "Europe/Berlin",
|
||||
"misfire_policy": "skip",
|
||||
},
|
||||
),
|
||||
_row(
|
||||
activity_id=one_shot_id,
|
||||
enabled=True,
|
||||
trigger_config={
|
||||
"trigger_type": "scheduled",
|
||||
"at": datetime(2026, 6, 19, 8, 0, tzinfo=timezone.utc),
|
||||
"timezone": "UTC",
|
||||
},
|
||||
),
|
||||
_row(
|
||||
activity_id=uuid.uuid4(),
|
||||
enabled=True,
|
||||
trigger_config={
|
||||
"trigger_type": "event",
|
||||
"event_type": "kaizen.metrics.recorded",
|
||||
"filters": {},
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.to_dict() == {
|
||||
"upserted": 2,
|
||||
"paused": 1,
|
||||
"deleted_orphans": 1,
|
||||
}
|
||||
assert upserted == [
|
||||
(new_id, True, "cron"),
|
||||
(disabled_old_id, False, "cron"),
|
||||
(one_shot_id, True, "scheduled"),
|
||||
]
|
||||
assert deleted == [str(orphan_id)]
|
||||
134
tests/test_sync_service.py
Normal file
134
tests/test_sync_service.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from activity_core import sync_service
|
||||
from activity_core.sync_schedules import ScheduleSyncResult
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_sync_runs_requested_sections(monkeypatch) -> None:
|
||||
calls: list[str] = []
|
||||
|
||||
async def fake_definitions(session_factory: object) -> int:
|
||||
calls.append("definitions")
|
||||
return 2
|
||||
|
||||
async def fake_event_types(session_factory: object) -> int:
|
||||
calls.append("event_types")
|
||||
return 5
|
||||
|
||||
async def fake_schedules(
|
||||
temporal_client: object,
|
||||
session_factory: object,
|
||||
) -> ScheduleSyncResult:
|
||||
calls.append("schedules")
|
||||
return ScheduleSyncResult(upserted=3, paused=1, deleted_orphans=2)
|
||||
|
||||
monkeypatch.setattr(sync_service, "sync_activity_definitions", fake_definitions)
|
||||
monkeypatch.setattr(sync_service, "sync_event_types", fake_event_types)
|
||||
monkeypatch.setattr(sync_service, "sync_with_session_factory", fake_schedules)
|
||||
|
||||
result = await sync_service.run_sync(
|
||||
session_factory=object(),
|
||||
temporal_client=object(),
|
||||
definitions=True,
|
||||
schedules=True,
|
||||
event_types=True,
|
||||
)
|
||||
|
||||
assert calls == ["definitions", "event_types", "schedules"]
|
||||
assert result["ok"] is True
|
||||
assert result["ran"] == {
|
||||
"definitions": True,
|
||||
"schedules": True,
|
||||
"event_types": True,
|
||||
}
|
||||
assert result["definitions"] == {"synced": 2}
|
||||
assert result["event_types"] == {"synced": 5}
|
||||
assert result["schedules"] == {
|
||||
"upserted": 3,
|
||||
"paused": 1,
|
||||
"deleted_orphans": 2,
|
||||
}
|
||||
assert result["errors"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_sync_collects_errors_and_continues(monkeypatch) -> None:
|
||||
calls: list[str] = []
|
||||
|
||||
async def failing_definitions(session_factory: object) -> int:
|
||||
calls.append("definitions")
|
||||
raise RuntimeError("definition parse failed")
|
||||
|
||||
async def fake_schedules(
|
||||
temporal_client: object,
|
||||
session_factory: object,
|
||||
) -> ScheduleSyncResult:
|
||||
calls.append("schedules")
|
||||
return ScheduleSyncResult(upserted=1)
|
||||
|
||||
monkeypatch.setattr(
|
||||
sync_service,
|
||||
"sync_activity_definitions",
|
||||
failing_definitions,
|
||||
)
|
||||
monkeypatch.setattr(sync_service, "sync_with_session_factory", fake_schedules)
|
||||
|
||||
result = await sync_service.run_sync(
|
||||
session_factory=object(),
|
||||
temporal_client=object(),
|
||||
definitions=True,
|
||||
schedules=True,
|
||||
event_types=False,
|
||||
)
|
||||
|
||||
assert calls == ["definitions", "schedules"]
|
||||
assert result["ok"] is False
|
||||
assert result["definitions"] == {"synced": 0}
|
||||
assert result["schedules"]["upserted"] == 1
|
||||
assert result["errors"] == [
|
||||
{
|
||||
"stage": "definitions",
|
||||
"type": "RuntimeError",
|
||||
"message": "definition parse failed",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_sync_reports_missing_temporal_client_for_schedules() -> None:
|
||||
result = await sync_service.run_sync(
|
||||
session_factory=object(),
|
||||
temporal_client=None,
|
||||
definitions=False,
|
||||
schedules=True,
|
||||
event_types=False,
|
||||
)
|
||||
|
||||
assert result["ok"] is False
|
||||
assert result["errors"] == [
|
||||
{
|
||||
"stage": "schedules",
|
||||
"type": "RuntimeError",
|
||||
"message": "Temporal client is required for schedule sync",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_record_error_bounds_error_count() -> None:
|
||||
result: dict[str, Any] = {
|
||||
"ok": True,
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
for i in range(25):
|
||||
sync_service._record_error(result, "stage", RuntimeError(f"boom {i}"))
|
||||
|
||||
assert result["ok"] is False
|
||||
assert len(result["errors"]) == 20
|
||||
assert result["errors"][0]["message"] == "boom 0"
|
||||
assert result["errors"][-1]["message"] == "boom 19"
|
||||
Reference in New Issue
Block a user