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