generated from coulomb/repo-seed
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>
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)
|