Files
shard-wiki/tests/test_engine_profile.py
tegwick b9bb1f7d10 feat(engine): capability profile derived from active extensions (WP-0014 T4, E-5)
engine/profile.py: engine_base_profile() (kernel-only c2-minimum profile),
ProfileContribution (an extension's ON_PROFILE contribution: axis raises + verbs),
derive_profile() folds active extensions' contributions onto the base in deterministic
order then validate() — so configuration->capability is one chain and composition can
never yield an impossible profile (encrypted+native-query rejected). 5 tests green,
96 total, pyflakes clean. Marks T4 done.

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

99 lines
2.8 KiB
Python

"""Tests for capability-profile-derived-from-extensions (SHARD-WP-0014 T4, E-5)."""
import pytest
from shard_wiki.engine import (
Extension,
ExtensionRuntime,
Hook,
ProfileContribution,
derive_profile,
engine_base_profile,
)
from shard_wiki.model import (
Addressing,
ContentOpacity,
NativeQuery,
ProfileError,
Verb,
)
class StructExt(Extension):
id = "ext.struct"
def hooks(self):
return {
Hook.ON_PROFILE: lambda payload, ctx: ProfileContribution(
verbs_add=frozenset({Verb.STRUCTURED_PAYLOAD})
)
}
class AddrExt(Extension):
id = "ext.addr"
def hooks(self):
return {
Hook.ON_PROFILE: lambda payload, ctx: ProfileContribution(
addressing=Addressing.SPAN, verbs_add=frozenset({Verb.TRANSCLUDE_SOURCE})
)
}
class EncryptExt(Extension):
id = "ext.encrypt"
def hooks(self):
return {Hook.ON_PROFILE: lambda p, c: ProfileContribution(content_opacity=ContentOpacity.ENCRYPTED)}
class QueryExt(Extension):
id = "ext.query"
def hooks(self):
return {Hook.ON_PROFILE: lambda p, c: ProfileContribution(native_query=NativeQuery.DB_QUERY)}
def _active(*exts, ids=None):
rt = ExtensionRuntime()
for e in exts:
rt.register(e)
return rt.activate(ids if ids is not None else {e.id for e in exts})
def test_base_profile_is_valid_kernel_minimum():
p = engine_base_profile()
assert p.supports(Verb.READ) and p.supports(Verb.WRITE)
assert not p.supports(Verb.STRUCTURED_PAYLOAD)
assert p.addressing is Addressing.PATH
def test_no_extensions_yields_base():
rt = ExtensionRuntime()
active = rt.activate(set())
assert derive_profile(active) == engine_base_profile()
def test_activating_extension_raises_the_profile():
active = _active(StructExt(), AddrExt())
p = derive_profile(active)
assert p.supports(Verb.STRUCTURED_PAYLOAD) # from ext.struct
assert p.supports(Verb.TRANSCLUDE_SOURCE) # from ext.addr
assert p.addressing is Addressing.SPAN # raised by ext.addr
assert p.supports(Verb.READ) # base preserved
def test_profile_changes_with_active_set():
only_struct = derive_profile(_active(StructExt(), AddrExt(), ids={"ext.struct"}))
both = derive_profile(_active(StructExt(), AddrExt()))
assert not only_struct.supports(Verb.TRANSCLUDE_SOURCE)
assert both.supports(Verb.TRANSCLUDE_SOURCE) # E-5: profile reflects what's active
def test_composition_cannot_yield_an_impossible_profile():
# encrypted opacity + native query violates §6.5 implication rules -> derive must reject.
active = _active(EncryptExt(), QueryExt())
with pytest.raises(ProfileError):
derive_profile(active)