feat(ttp): E.3.5 FilesystemRuleStore — inotify hot-reload + per-rule events
Implements the filesystem-backed rule store body left empty at contract phase: YAML parse + Pydantic validation, asyncinotify watch over ./rules/ttp/, in-process state cache with auto-revert on expires_at, and a subscribe_changes() async iterator yielding one RuleChange per per-rule edit. Bus topic builders ttp_rule_reloaded / ttp_rule_state ship alongside. Why: the rule plane needed a store before the engine (E.3.7) could consume RuleChange events and atomically swap compiled rules into its dispatch index. Notes: - Linux-only by construction (asyncinotify wheel gated by sys_platform marker; FilesystemRuleStore.__init__ raises on non-Linux). - Filename allowlist is the FIRST check on every inotify event. - Content-hash dedup so a single write firing IN_CREATE + IN_CLOSE_WRITE produces exactly one RuleChange. - All compile work serializes on a single asyncio.Lock. - Subscribers register their queue eagerly so events fired between subscribe_changes() and the first __anext__() are buffered. xfails flipped: per-save-style + filter-ordering + atomic-swap in test_filesystem.py; load_compiled / set_state isolation / round-trip / per-rule fan-out / expired-state revert / set_state failure semantics in test_conformance.py (FS side; DB side stays xfail until E.3.6); malformed-YAML compile-time check in test_rule_engine.py. Tests: 197 passed, 35 xfailed (gated on E.3.6 / E.3.7 / lifters). mypy + bandit: clean on all touched files. Wiki update for the per-rule reload + state-change topics lands in a matching wiki-checkout/Service-Bus.md edit (separate repo).
This commit is contained in:
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@@ -172,19 +173,23 @@ def test_e25_malformed_yaml_fails_at_schema_validation() -> None:
|
||||
RuleSchema.model_validate(bad)
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=True,
|
||||
reason="impl phase E.3.5: RuleStore.load_compiled raises on malformed YAML",
|
||||
)
|
||||
def test_e25_malformed_yaml_fails_at_compile_not_evaluate() -> None:
|
||||
"""Once the store contract lands (E.1.11) and impl ships (E.3.5),
|
||||
feeding the store a malformed YAML document must raise during
|
||||
:meth:`RuleStore.load_compiled` (the deploy-time hook) — never at
|
||||
:meth:`RuleEngine.evaluate` time. The trip-wire fires when impl
|
||||
surfaces ``RuleStore`` and stores accept malformed input.
|
||||
"""
|
||||
from decnet.ttp.store.base import RuleStore # noqa: F401
|
||||
raise AssertionError("E.3.5 will pin this once RuleStore lands")
|
||||
def test_e25_malformed_yaml_fails_at_compile_not_evaluate(tmp_path: Any) -> None:
|
||||
"""Feeding the store a malformed YAML document raises during
|
||||
:meth:`RuleStore.load_compiled` — the deploy-time hook — never at
|
||||
:meth:`RuleEngine.evaluate` time. Pinned at E.3.5 once the
|
||||
filesystem store implementation lands."""
|
||||
if sys.platform != "linux": # pragma: no cover
|
||||
pytest.skip("FilesystemRuleStore is Linux-only (inotify dep)")
|
||||
from decnet.ttp.store.impl.filesystem import FilesystemRuleStore
|
||||
|
||||
bad = tmp_path / "R0001.yaml"
|
||||
bad.write_text(
|
||||
"rule_id: R0001\nrule_version: 1\nname: broken\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
store = FilesystemRuleStore(rules_dir=tmp_path)
|
||||
with pytest.raises((ValidationError, ValueError)):
|
||||
asyncio.run(store.load_compiled())
|
||||
|
||||
|
||||
def test_e25_evaluate_unknown_source_kind_returns_empty() -> None:
|
||||
|
||||
Reference in New Issue
Block a user