Files
EverOS/tests/unit/test_infra/test_ome/test_dispatcher.py
Elliot Chen 518b8eca85 chore: initialize EverOS 1.0.0
md-first memory extraction framework for AI agents.

Markdown is the single source of truth; SQLite holds state and LanceDB
provides the rebuildable vector + BM25 + scalar index. The codebase follows
a single-direction DDD layering (entrypoints -> service -> memory -> infra,
with component / core / config cross-cutting) enforced by import-linter.

Engineering surface:
- Coding conventions in .claude/rules/ (path-scoped) and workflows in
  .claude/skills/ (/commit, /new-branch, /pr).
- GitHub Actions CI runs make lint + test + integration; pre-commit mirrors
  the gates locally (ruff, hygiene hooks, gitlint commit-msg).
- Commit messages follow Conventional Commits, enforced by gitlint.
- make lint also enforces datetime two-zone discipline and OpenAPI drift.
2026-06-06 07:33:17 +08:00

216 lines
7.0 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any
import pytest
from everos.infra.ome._dispatch.dispatcher import EventDispatcher
from everos.infra.ome._dispatch.registry import StrategyRegistry
from everos.infra.ome._stores.counter import CounterStore
from everos.infra.ome._stores.storage import OMEStorage
from everos.infra.ome.context import StrategyContext
from everos.infra.ome.decorator import offline_strategy
from everos.infra.ome.events import BaseEvent, CronTick
from everos.infra.ome.gates import Counter
from everos.infra.ome.triggers import Cron, Immediate
class _E(BaseEvent):
user_id: str
def _make_strategy(name: str, **kw):
@offline_strategy(name=name, trigger=Immediate(on=[_E]), emits=[], **kw)
async def _f(event: Any, ctx: StrategyContext) -> None:
return None
return _f
@pytest.fixture
async def dispatcher(tmp_path: Path) -> EventDispatcher:
storage = OMEStorage(db_path=tmp_path / "ome.db")
await storage.init()
registry = StrategyRegistry()
counter = CounterStore(storage=storage)
return EventDispatcher(registry=registry, counter_store=counter)
@pytest.mark.asyncio
async def test_dispatch_passes_when_no_gate(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(_make_strategy("s_pass"))
routes = await dispatcher.dispatch(_E(user_id="u1"))
assert [m.name for m, _ in routes] == ["s_pass"]
@pytest.mark.asyncio
async def test_dispatch_skips_disabled(dispatcher: EventDispatcher) -> None:
dispatcher._registry.register(_make_strategy("s_off", enabled=False))
routes = await dispatcher.dispatch(_E(user_id="u1"))
assert routes == []
@pytest.mark.asyncio
async def test_dispatch_applies_to_string(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(
_make_strategy("s", applies_to="user_id"),
)
routes_empty = await dispatcher.dispatch(_E(user_id=""))
routes_set = await dispatcher.dispatch(_E(user_id="u1"))
assert routes_empty == []
assert len(routes_set) == 1
@pytest.mark.asyncio
async def test_dispatch_applies_to_callable(
dispatcher: EventDispatcher,
) -> None:
def is_paid(e: _E) -> bool:
return e.user_id.startswith("paid_")
dispatcher._registry.register(_make_strategy("s", applies_to=is_paid))
assert await dispatcher.dispatch(_E(user_id="free_a")) == []
assert len(await dispatcher.dispatch(_E(user_id="paid_a"))) == 1
@pytest.mark.asyncio
async def test_dispatch_counter_gate(dispatcher: EventDispatcher) -> None:
dispatcher._registry.register(
_make_strategy("s", gate=Counter(threshold=3, event_field="user_id"))
)
for _ in range(2):
routes = await dispatcher.dispatch(_E(user_id="u1"))
assert routes == []
routes = await dispatcher.dispatch(_E(user_id="u1"))
assert len(routes) == 1
@pytest.mark.asyncio
async def test_inspect_returns_route_info(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(
_make_strategy("s", gate=Counter(threshold=3, event_field="user_id"))
)
infos = await dispatcher.inspect(_E(user_id="u1"))
assert len(infos) == 1
assert infos[0].counter_progress == (1, 3)
assert infos[0].will_run is False
def _make_cron_strategy(name: str):
@offline_strategy(name=name, trigger=Cron(expr="0 * * * *"), emits=[])
async def _f(event: Any, ctx: StrategyContext) -> None:
return None
return _f
@pytest.mark.asyncio
async def test_dispatch_routes_engine_tick_to_named_strategy_only(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(_make_cron_strategy("cron_a"))
dispatcher._registry.register(_make_cron_strategy("cron_b"))
routes = await dispatcher.dispatch(CronTick(strategy_name="cron_a"))
assert [m.name for m, _ in routes] == ["cron_a"]
@pytest.mark.asyncio
async def test_inspect_engine_tick_skips_non_target_strategy(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(_make_cron_strategy("cron_a"))
dispatcher._registry.register(_make_cron_strategy("cron_b"))
infos = await dispatcher.inspect(CronTick(strategy_name="cron_b"))
assert [i.strategy_name for i in infos] == ["cron_b"]
@pytest.mark.asyncio
async def test_dispatch_force_enabled_bypasses_enabled_gate(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(_make_strategy("s_off", enabled=False))
assert await dispatcher.dispatch(_E(user_id="u1")) == []
routes = await dispatcher.dispatch(_E(user_id="u1"), force_enabled=True)
assert [m.name for m, _ in routes] == ["s_off"]
@pytest.mark.asyncio
async def test_dispatch_force_enabled_still_applies_applies_to_and_counter(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(
_make_strategy(
"s",
enabled=False,
applies_to="user_id",
gate=Counter(threshold=2, event_field="user_id"),
),
)
assert await dispatcher.dispatch(_E(user_id=""), force_enabled=True) == []
assert await dispatcher.dispatch(_E(user_id="u1"), force_enabled=True) == []
routes = await dispatcher.dispatch(_E(user_id="u1"), force_enabled=True)
assert len(routes) == 1
@pytest.mark.asyncio
async def test_dispatch_strategy_filter_scopes_to_single_strategy(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(_make_strategy("s_a"))
dispatcher._registry.register(_make_strategy("s_b"))
routes = await dispatcher.dispatch(_E(user_id="u1"), strategy_filter="s_a")
assert [m.name for m, _ in routes] == ["s_a"]
@pytest.mark.asyncio
async def test_dispatch_strategy_filter_unknown_raises(
dispatcher: EventDispatcher,
) -> None:
dispatcher._registry.register(_make_strategy("s_a"))
with pytest.raises(KeyError):
await dispatcher.dispatch(_E(user_id="u1"), strategy_filter="missing")
@pytest.mark.asyncio
async def test_dispatch_isolates_faulty_applies_to_callable(
dispatcher: EventDispatcher,
) -> None:
"""A single strategy's buggy ``applies_to`` callable must not tank
the fan-out for siblings subscribed to the same event class.
"""
def _boom(_e: _E) -> bool:
raise RuntimeError("applies_to is buggy")
dispatcher._registry.register(_make_strategy("s_buggy", applies_to=_boom))
dispatcher._registry.register(_make_strategy("s_healthy"))
routes = await dispatcher.dispatch(_E(user_id="u1"))
# s_buggy is treated as not-applies; s_healthy still routes.
assert [m.name for m, _ in routes] == ["s_healthy"]
@pytest.mark.asyncio
async def test_inspect_isolates_faulty_applies_to_callable(
dispatcher: EventDispatcher,
) -> None:
def _boom(_e: _E) -> bool:
raise RuntimeError("applies_to is buggy")
dispatcher._registry.register(_make_strategy("s_buggy", applies_to=_boom))
dispatcher._registry.register(_make_strategy("s_healthy"))
infos = await dispatcher.inspect(_E(user_id="u1"))
by_name = {i.strategy_name: i for i in infos}
assert by_name["s_buggy"].applies_to_pass is False
assert by_name["s_healthy"].applies_to_pass is True