"""Plugin/stage contract test (Phase 6). A drop-in stage and plugin, defined ONLY in this test module via the public ``@register_stage`` / ``@register_plugin`` decorators, are picked up by the engine with zero edits to core files. This is the pluggability guarantee: a new filter behavior is a new module + a config entry, never an edit to ``engine.py``. """ from filter_pipeline.engine import FilterEngine from filter_pipeline.models import FilterResult from filter_pipeline.plugins.base import BaseFilterPlugin from filter_pipeline.registry import ( get_plugin_class, get_stage_class, register_plugin, register_stage, ) from filter_pipeline.stages.base_stage import BaseStage @register_stage("sentinel_dropin_stage") class SentinelStage(BaseStage): """Drop-in stage that tags any result it sees.""" def get_name(self): return "Sentinel" def process(self, post, result): result.tags.append("sentinel_ran") return result @register_plugin("sentinel_dropin_plugin") class SentinelPlugin(BaseFilterPlugin): """Drop-in plugin: never rejects, returns a fixed score.""" def get_name(self): return "SentinelPlugin" def should_filter(self, post, context=None): return False def score(self, post, context=None): return 0.9 def test_dropin_stage_is_registered(): assert get_stage_class("sentinel_dropin_stage") is SentinelStage def test_dropin_plugin_is_registered(): assert get_plugin_class("sentinel_dropin_plugin") is SentinelPlugin def test_engine_instantiates_dropin_stage(): eng = FilterEngine("filter_config.json", "filtersets.json") eng._init_stages() assert "sentinel_dropin_stage" in eng._stages stage = eng._stages["sentinel_dropin_stage"] # Running the stage through the contract it claims to implement works. result = FilterResult(post_uuid="x", passed=True, score=0.5) out = stage.process({"uuid": "x"}, result) assert "sentinel_ran" in out.tags def test_dropin_stage_can_be_selected_in_a_filterset(tmp_path): """A filterset that lists the drop-in stage actually runs it. Builds a throwaway config + filterset on disk so no core file is edited. """ import json cfg = tmp_path / "cfg.json" cfg.write_text(json.dumps({ "ai": {"enabled": False}, "cache": {"enabled": False}, "pipeline": {"default_stages": ["sentinel_dropin_stage"], "enable_parallel": False}, "plugins": {"enabled": [], "configs": {}}, })) fs = tmp_path / "fs.json" fs.write_text(json.dumps({"custom": {"post_rules": {}, "comment_rules": {}}})) eng = FilterEngine(str(cfg), str(fs)) eng._init_stages() results = eng.process_batch( [{"uuid": "p1", "title": "t", "content": "", "score": 0, "timestamp": 0}], "custom", ) assert "sentinel_ran" in results[0].tags