Files
EverOS/tests/unit/test_infra/test_ome/test_config_reloader.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

408 lines
12 KiB
Python

"""Tests for ConfigReloader."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
import pytest
from everos.infra.ome._background.config_reloader import (
ConfigReloader,
apply_overrides,
)
from everos.infra.ome._dispatch.registry import StrategyRegistry
from everos.infra.ome.config import CounterOverride, StrategyOverride, TomlRoot
from everos.infra.ome.context import StrategyContext
from everos.infra.ome.decorator import offline_strategy
from everos.infra.ome.engine import OfflineEngine
from everos.infra.ome.events import BaseEvent
from everos.infra.ome.gates import Counter
from everos.infra.ome.triggers import Cron, Idle, Immediate
class _E(BaseEvent):
pass
class _EventUid(BaseEvent):
user_id: str
def _make(name: str, **kw: Any) -> Any:
@offline_strategy(name=name, trigger=Immediate(on=[_E]), emits=[], **kw)
async def f(event: Any, ctx: StrategyContext) -> None:
return None
return f
def _make_cron(name: str, expr: str = "0 3 * * *", **kw: Any) -> Any:
@offline_strategy(name=name, trigger=Cron(expr=expr), emits=[], **kw)
async def f(event: Any, ctx: StrategyContext) -> None:
return None
return f
def _make_idle(name: str, **kw: Any) -> Any:
@offline_strategy(
name=name,
trigger=Idle(
on=[_EventUid],
event_field="user_id",
idle_seconds=30,
scan_interval_seconds=10,
),
emits=[],
**kw,
)
async def f(event: Any, ctx: StrategyContext) -> None:
return None
return f
@pytest.fixture
def fake_engine() -> MagicMock:
"""Mock OfflineEngine; spec catches typos in mocked method names."""
return MagicMock(spec=OfflineEngine)
def test_apply_overrides_replaces_enabled(fake_engine: MagicMock) -> None:
reg = StrategyRegistry()
reg.register(_make("s", enabled=True))
root = TomlRoot(strategies={"s": StrategyOverride(enabled=False)})
apply_overrides(reg, root, fake_engine)
assert reg.get("s").enabled is False
def test_apply_overrides_max_retries(fake_engine: MagicMock) -> None:
reg = StrategyRegistry()
reg.register(_make("s", max_retries=1))
root = TomlRoot(strategies={"s": StrategyOverride(max_retries=5)})
apply_overrides(reg, root, fake_engine)
assert reg.get("s").max_retries == 5
def test_apply_overrides_counter_partial(fake_engine: MagicMock) -> None:
reg = StrategyRegistry()
reg.register(_make("s", gate=Counter(threshold=3, event_field="user_id")))
root = TomlRoot(
strategies={"s": StrategyOverride(gate=CounterOverride(threshold=10))}
)
apply_overrides(reg, root, fake_engine)
g = reg.get("s").gate
assert g.threshold == 10
assert g.event_field == "user_id" # untouched
def test_apply_overrides_unknown_strategy_ignored(fake_engine: MagicMock) -> None:
reg = StrategyRegistry()
reg.register(_make("s"))
root = TomlRoot(strategies={"unknown": StrategyOverride(enabled=False)})
apply_overrides(reg, root, fake_engine) # must not raise
def test_apply_overrides_updates_cron_expr(fake_engine: MagicMock) -> None:
reg = StrategyRegistry()
reg.register(_make_cron("s", "0 3 * * *"))
root = TomlRoot(strategies={"s": StrategyOverride(cron="*/5 * * * *")})
apply_overrides(reg, root, fake_engine)
assert isinstance(reg.get("s").trigger, Cron)
assert reg.get("s").trigger.expr == "*/5 * * * *"
fake_engine.reschedule_cron_job.assert_called_once_with("s", "*/5 * * * *")
def test_apply_overrides_skips_atomic_group_on_reschedule_failure(
fake_engine: MagicMock,
) -> None:
"""Even though StrategyOverride.cron is now syntactically validated at
parse time, reschedule_cron_job can still fail at runtime (APS internal
error, scheduler stopped, etc.). The atomic-group rollback must hold
against those failures too.
"""
reg = StrategyRegistry()
reg.register(_make_cron("s", "0 3 * * *", enabled=True, max_retries=1))
fake_engine.reschedule_cron_job.side_effect = RuntimeError("APS error")
root = TomlRoot(
strategies={
"s": StrategyOverride(enabled=False, cron="*/5 * * * *", max_retries=99)
}
)
apply_overrides(reg, root, fake_engine)
# enabled applied independently
assert reg.get("s").enabled is False
# atomic group rolled back: cron unchanged, max_retries unchanged
assert reg.get("s").trigger.expr == "0 3 * * *"
assert reg.get("s").max_retries == 1
fake_engine.reschedule_cron_job.assert_called_once_with("s", "*/5 * * * *")
def test_apply_overrides_skips_atomic_group_on_cron_type_mismatch(
fake_engine: MagicMock,
) -> None:
reg = StrategyRegistry()
reg.register(_make("s", enabled=True)) # Immediate strategy
root = TomlRoot(strategies={"s": StrategyOverride(enabled=False, cron="0 3 * * *")})
apply_overrides(reg, root, fake_engine)
assert reg.get("s").enabled is False
assert isinstance(reg.get("s").trigger, Immediate)
fake_engine.reschedule_cron_job.assert_not_called()
def test_apply_overrides_updates_idle_seconds_and_scan_interval(
fake_engine: MagicMock,
) -> None:
reg = StrategyRegistry()
reg.register(_make_idle("s"))
root = TomlRoot(
strategies={"s": StrategyOverride(idle_seconds=120, scan_interval_seconds=15)}
)
apply_overrides(reg, root, fake_engine)
t = reg.get("s").trigger
assert t.idle_seconds == 120
assert t.scan_interval_seconds == 15
fake_engine.reschedule_idle_job.assert_called_once_with(
"s", scan_interval_seconds=15
)
def test_apply_overrides_updates_only_idle_seconds_does_not_reschedule_aps(
fake_engine: MagicMock,
) -> None:
"""idle_seconds is consumed by dispatcher / engine on each scan,
not by APS IntervalTrigger, so changing only it must NOT trigger
an APS reschedule (which would reset the pending tick).
"""
reg = StrategyRegistry()
reg.register(_make_idle("s"))
root = TomlRoot(strategies={"s": StrategyOverride(idle_seconds=120)})
apply_overrides(reg, root, fake_engine)
assert reg.get("s").trigger.idle_seconds == 120
fake_engine.reschedule_idle_job.assert_not_called()
def test_apply_overrides_skips_atomic_group_on_idle_type_mismatch(
fake_engine: MagicMock,
) -> None:
reg = StrategyRegistry()
reg.register(_make_cron("s")) # Cron strategy
root = TomlRoot(strategies={"s": StrategyOverride(idle_seconds=60)})
apply_overrides(reg, root, fake_engine)
assert isinstance(reg.get("s").trigger, Cron)
fake_engine.reschedule_cron_job.assert_not_called()
fake_engine.reschedule_idle_job.assert_not_called()
def test_apply_overrides_rollback_on_aps_reschedule_failure(
fake_engine: MagicMock,
) -> None:
fake_engine.reschedule_cron_job.side_effect = RuntimeError("APS exploded")
reg = StrategyRegistry()
reg.register(_make_cron("s", "0 3 * * *", enabled=True, max_retries=1))
root = TomlRoot(
strategies={
"s": StrategyOverride(enabled=False, cron="*/5 * * * *", max_retries=99)
}
)
apply_overrides(reg, root, fake_engine)
# enabled applied (Step 1, before atomic group)
assert reg.get("s").enabled is False
# atomic group rolled back: cron + max_retries unchanged
assert reg.get("s").trigger.expr == "0 3 * * *"
assert reg.get("s").max_retries == 1
def test_apply_overrides_enabled_survives_reschedule_failure(
fake_engine: MagicMock,
) -> None:
"""enabled=false is emergency-stop semantics; must apply even when the
paired cron update fails at reschedule time.
"""
reg = StrategyRegistry()
reg.register(_make_cron("s", "0 3 * * *", enabled=True))
fake_engine.reschedule_cron_job.side_effect = RuntimeError("APS error")
root = TomlRoot(
strategies={"s": StrategyOverride(enabled=False, cron="*/5 * * * *")}
)
apply_overrides(reg, root, fake_engine)
assert reg.get("s").enabled is False
assert reg.get("s").trigger.expr == "0 3 * * *"
def test_apply_overrides_strategy_isolation(fake_engine: MagicMock) -> None:
"""One strategy's atomic-group failure must not affect another."""
reg = StrategyRegistry()
reg.register(_make_cron("a", "0 3 * * *"))
reg.register(_make_cron("b", "0 4 * * *"))
def _reschedule(name: str, expr: str) -> None:
if name == "b":
raise RuntimeError("simulated APS failure for b")
fake_engine.reschedule_cron_job.side_effect = _reschedule
root = TomlRoot(
strategies={
"a": StrategyOverride(cron="*/5 * * * *"),
"b": StrategyOverride(cron="*/7 * * * *"),
}
)
apply_overrides(reg, root, fake_engine)
assert reg.get("a").trigger.expr == "*/5 * * * *"
assert reg.get("b").trigger.expr == "0 4 * * *"
def test_apply_overrides_atomic_group_no_partial_application(
fake_engine: MagicMock,
) -> None:
"""A failure in the atomic group must roll back max_retries / gate too."""
reg = StrategyRegistry()
reg.register(
_make_cron(
"s",
"0 3 * * *",
max_retries=1,
gate=Counter(threshold=3, event_field="user_id"),
)
)
fake_engine.reschedule_cron_job.side_effect = RuntimeError("APS error")
root = TomlRoot(
strategies={
"s": StrategyOverride(
cron="*/5 * * * *",
max_retries=99,
gate=CounterOverride(threshold=100),
)
}
)
apply_overrides(reg, root, fake_engine)
assert reg.get("s").trigger.expr == "0 3 * * *"
assert reg.get("s").max_retries == 1
assert reg.get("s").gate.threshold == 3
def test_apply_overrides_succeeds_on_combined_enabled_and_trigger(
fake_engine: MagicMock,
) -> None:
reg = StrategyRegistry()
reg.register(_make_cron("s", "0 3 * * *", enabled=True))
root = TomlRoot(
strategies={"s": StrategyOverride(enabled=False, cron="*/5 * * * *")}
)
apply_overrides(reg, root, fake_engine)
assert reg.get("s").enabled is False
assert reg.get("s").trigger.expr == "*/5 * * * *"
fake_engine.reschedule_cron_job.assert_called_once_with("s", "*/5 * * * *")
def test_atomic_group_skipped_when_introducing_gate_without_threshold(
fake_engine: MagicMock,
) -> None:
"""N5: TOML that introduces a gate via cooldown alone (no threshold)
must be rejected, not silently defaulted to threshold=1 ('fire every event').
"""
reg = StrategyRegistry()
reg.register(_make("s")) # no gate
assert reg.get("s").gate is None
root = TomlRoot(
strategies={
"s": StrategyOverride(gate=CounterOverride(cooldown_seconds=60)),
}
)
apply_overrides(reg, root, fake_engine)
# Atomic group rolled back: still no gate.
assert reg.get("s").gate is None
def test_atomic_group_accepts_introducing_gate_with_explicit_threshold(
fake_engine: MagicMock,
) -> None:
"""N5 happy path: explicit threshold on a previously-gateless strategy
is the user opt-in we require.
"""
reg = StrategyRegistry()
reg.register(_make("s"))
assert reg.get("s").gate is None
root = TomlRoot(
strategies={
"s": StrategyOverride(
gate=CounterOverride(threshold=5, cooldown_seconds=60)
),
}
)
apply_overrides(reg, root, fake_engine)
g = reg.get("s").gate
assert g is not None
assert g.threshold == 5
assert g.cooldown_seconds == 60
@pytest.mark.asyncio
async def test_start_twice_raises(tmp_path: Path) -> None:
"""N7: calling start() twice surfaces the caller bug instead of
silently dropping the original task reference and racing two watchers.
"""
config_path = tmp_path / "ome.toml"
config_path.write_text("")
reloader = ConfigReloader(
config_path=config_path,
registry=StrategyRegistry(),
engine=MagicMock(spec=OfflineEngine),
)
reloader.start()
try:
with pytest.raises(RuntimeError, match=r"already started"):
reloader.start()
finally:
await reloader.stop()
@pytest.mark.asyncio
async def test_start_after_stop_is_allowed(tmp_path: Path) -> None:
"""N7: idempotency check only fires while a task is live; once stopped,
start() must work again so callers can restart the reloader.
"""
config_path = tmp_path / "ome.toml"
config_path.write_text("")
reloader = ConfigReloader(
config_path=config_path,
registry=StrategyRegistry(),
engine=MagicMock(spec=OfflineEngine),
)
reloader.start()
await reloader.stop()
# Must not raise.
reloader.start()
await reloader.stop()