First implementation

This commit is contained in:
2026-06-15 00:42:14 +02:00
parent 542d7c22be
commit fa39883aec
18 changed files with 1352 additions and 66 deletions

View File

@@ -0,0 +1,116 @@
"""
Tests for T02 registry + T03 resolver (WP-0003).
Run: PYTHONPATH=src python -m pytest tests/test_registry_resolver.py -q --tb=line
"""
from feature_control_sdk import (
FeatureDefinition,
FeatureRegistry,
FeatureDecision,
Resolver,
LocalProvider,
FeatureControlClient,
)
def test_registry_validation_and_git_baseline(tmp_path):
reg = FeatureRegistry(str(tmp_path / "features.json"))
# Valid
fd = FeatureDefinition(
feature_key="test.compute",
name="Test Compute",
description="...",
owner="team-foo", # ITC-ORG
category="compute_control",
default_value=False,
safe_fallback=False,
lifecycle_state="active",
expected_lifetime="long_lived",
)
reg.register(fd)
reg.save()
assert "test.compute" in reg.keys()
loaded = FeatureRegistry(str(tmp_path / "features.json"))
assert loaded.get("test.compute").owner == "team-foo"
# Invalid: no owner
try:
reg.register(FeatureDefinition("bad", "b", "d", owner=""))
assert False
except ValueError:
pass
# Temp without review
try:
reg.register(
FeatureDefinition(
"temp.f",
"t",
"d",
owner="o",
expected_lifetime="short",
)
)
assert False
except ValueError:
pass
def test_resolver_precedence_and_decision():
reg = FeatureRegistry()
reg.register(
FeatureDefinition(
"kill.test",
"Kill Test",
"...",
owner="o",
default_value=True,
safe_fallback=False,
)
)
reg.register(
FeatureDefinition(
"tenant.test",
"Tenant Test",
"...",
owner="o",
default_value=False,
)
)
provider = LocalProvider(
{
"kill:kill.test": True, # kill signal
"tenant:acme:tenant.test": True,
}
)
resolver = Resolver(reg, provider.values)
# Kill wins
d = resolver.evaluate("kill.test", True, {"tenant_id": "acme"})
assert d.value is False
assert d.reason == "kill_switch"
assert d.source == "kill_switch"
# Tenant policy
d2 = resolver.evaluate("tenant.test", False, {"tenant_id": "acme"})
assert d2.value is True
assert d2.reason == "tenant_policy"
assert d2.scope == "tenant:acme"
# Default
d3 = resolver.evaluate("tenant.test", False, {"tenant_id": "other"})
assert d3.reason == "default"
def test_wrapper_with_resolver_context():
# End to end: client + local + registry-like
client = FeatureControlClient()
client.set_provider(LocalProvider({"feat.agent": True}))
ctx = {"actor_type": "agent", "agent_id": "foo", "tenant_id": "acme"}
val = client.get_boolean_value("feat.agent", False, ctx)
assert val is True

64
tests/test_sdk_wrapper.py Normal file
View File

@@ -0,0 +1,64 @@
"""
Basic tests for feature-control-sdk wrapper + local provider.
Run with: pytest tests/test_sdk_wrapper.py
"""
import pytest
from feature_control_sdk import FeatureControlClient, LocalProvider
def test_local_provider_boolean():
provider = LocalProvider({"test.feature": True})
client = FeatureControlClient()
client.set_provider(provider)
assert client.get_boolean_value("test.feature", False) is True
assert client.get_boolean_value("missing.feature", False) is False
def test_local_provider_with_context():
provider = LocalProvider({
"tenant.feature": False,
"agent.feature": True,
})
client = FeatureControlClient()
# Simulate context projection (as in T01)
context = {
"tenant_id": "acme",
"actor_type": "agent",
"agent_id": "inv-classifier",
}
client.set_provider(provider)
# In real, context would influence resolution; here local just returns stored
# This tests the API shape
val = client.get_boolean_value("agent.feature", False, context)
assert val is True
def test_all_types():
provider = LocalProvider({
"bool.f": True,
"str.f": "hello",
"num.f": 42,
"obj.f": {"a": 1},
})
client = FeatureControlClient()
client.set_provider(provider)
assert client.get_boolean_value("bool.f", False) is True
assert client.get_string_value("str.f", "x") == "hello"
assert client.get_number_value("num.f", 0) == 42
assert client.get_object_value("obj.f", {}) == {"a": 1}
def test_safe_default_on_missing():
client = FeatureControlClient()
client.set_provider(LocalProvider({}))
assert client.get_boolean_value("no.such", False) is False
assert client.get_string_value("no.such", "def") == "def"