Files
shard-wiki/tests/test_engine_extension.py
tegwick b48a99d3c2 feat(engine): typed-extension runtime (WP-0014 T2)
engine/extension.py: Extension contract (id/provides/declares_types/depends_on/
conflicts_with + bound hooks), ExtensionRuntime (register-with-verification,
activate = dependency closure + conflict + type-collision checks rejecting
impossible profiles), and ActiveExtensions with deterministic (topological, id-tie)
hook dispatch — transform hooks chain, collect hooks gather. 9 tests green,
coverage floor held, pyflakes clean. Marks T2 done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 00:11:18 +02:00

110 lines
3.2 KiB
Python

"""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"})