"""Tests for the typed-extension runtime (SHARD-WP-0014 T2).""" import pytest from shard_wiki.engine import Extension, ExtensionError, ExtensionRuntime, Hook class Upper(Extension): id = "ext.upper" declares_types = ("upper",) def hooks(self): return {Hook.ON_WRITE: lambda body, ctx: body.upper()} class Bang(Extension): id = "ext.bang" depends_on = ("ext.upper",) def hooks(self): return {Hook.ON_WRITE: lambda body, ctx: body + "!"} class Profiler(Extension): id = "ext.profiler" def hooks(self): return {Hook.ON_PROFILE: lambda payload, ctx: {"structure": "typed"}} def _runtime(*exts): rt = ExtensionRuntime() for e in exts: rt.register(e) return rt def test_register_rejects_bad_id_and_noncallable_hook(): rt = ExtensionRuntime() class BadId(Extension): id = "upper" # missing 'ext.' prefix with pytest.raises(ExtensionError, match="ext."): rt.register(BadId()) class Liar(Extension): id = "ext.liar" def hooks(self): return {Hook.ON_WRITE: "not-callable"} # type: ignore[dict-item] with pytest.raises(ExtensionError, match="not callable"): rt.register(Liar()) def test_transform_hook_chains_in_dependency_order(): rt = _runtime(Upper(), Bang()) active = rt.activate({"ext.bang"}) # pulls ext.upper via depends_on assert set(active.ids) == {"ext.upper", "ext.bang"} # upper (dependency) runs before bang (dependent): "hi" -> "HI" -> "HI!" assert active.handlers(Hook.ON_WRITE) == ("ext.upper", "ext.bang") assert active.dispatch_transform(Hook.ON_WRITE, "hi") == "HI!" def test_dependency_closure_is_automatic(): rt = _runtime(Upper(), Bang()) assert set(rt.activate({"ext.bang"}).ids) == {"ext.upper", "ext.bang"} def test_unknown_extension_rejected(): with pytest.raises(ExtensionError, match="unknown"): ExtensionRuntime().activate({"ext.ghost"}) def test_conflict_rejected(): class A(Extension): id = "ext.a" conflicts_with = ("ext.b",) class B(Extension): id = "ext.b" with pytest.raises(ExtensionError, match="conflicts"): _runtime(A(), B()).activate({"ext.a", "ext.b"}) def test_type_collision_rejected(): class S1(Extension): id = "ext.s1" declares_types = ("record",) class S2(Extension): id = "ext.s2" declares_types = ("record",) with pytest.raises(ExtensionError, match="type collision"): _runtime(S1(), S2()).activate({"ext.s1", "ext.s2"}) def test_collect_hook_gathers_contributions(): active = _runtime(Profiler()).activate({"ext.profiler"}) assert active.dispatch_collect(Hook.ON_PROFILE) == [{"structure": "typed"}] def test_wrong_dispatch_kind_errors(): active = _runtime(Profiler()).activate({"ext.profiler"}) with pytest.raises(ExtensionError, match="not a transform hook"): active.dispatch_transform(Hook.ON_PROFILE, "x") def test_unmet_dependency_rejected(): # Bang depends on ext.upper, but only Bang is registered. rt = ExtensionRuntime() rt.register(Bang()) with pytest.raises(ExtensionError, match="unmet dependency|unknown"): rt.activate({"ext.bang"})