generated from coulomb/repo-seed
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>
This commit is contained in:
109
tests/test_engine_extension.py
Normal file
109
tests/test_engine_extension.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""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"})
|
||||
Reference in New Issue
Block a user