99 lines
2.8 KiB
Python
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)
|