feat(flags): the registry and a fail-closed Unleash client
Unleash holds flag state; it does not hold the list of flags. REGISTRY is that list, because the SDK evaluates an unknown flag to False and without a registry that is an undeclared False — indistinguishable from a typo in a flag name. Fail-closed throughout, and never raises: an unset UNLEASH_URL, an unreachable server, a client that throws, an undeclared name — all False. A flag layer that can 500 a request path or stop the API booting is worse than one that is switched off. Every flag defaults to False, with no per-flag override, because a flag that defaults on is a kill switch and this is deliberately not one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
This commit is contained in:
1 parent
01ccbb8e82
commit
7424cef7c6
4 files changed
+196
-1
No files matched your search
@@ -0,0 +1,74 @@
|
||||
"""Tests for the feature flag layer (spec 2026-08-23).
|
||||
|
||||
None of these need a running Unleash. That is the point: an unset UNLEASH_URL
|
||||
means every flag is False, which is what local development and CI get.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
from backend import flags
|
||||
|
||||
|
||||
def test_every_declared_flag_is_keyed_by_its_own_name():
|
||||
# One string is the registry key, the Unleash flag name and the JSON key.
|
||||
# A mismatch here would mean the UI toggles a flag the code never reads.
|
||||
for key, flag in flags.REGISTRY.items():
|
||||
assert key == flag.name
|
||||
|
||||
|
||||
def test_flag_names_are_snake_case():
|
||||
# Matches the API's existing convention (admission_distance,
|
||||
# rwm_expected_pct) so no case transformation exists to get wrong.
|
||||
for name in flags.REGISTRY:
|
||||
assert name == name.lower()
|
||||
assert "-" not in name and " " not in name
|
||||
|
||||
|
||||
def test_an_unconfigured_client_evaluates_every_flag_false(monkeypatch):
|
||||
monkeypatch.setattr(flags, "_client", None)
|
||||
for name in flags.REGISTRY:
|
||||
assert flags.is_enabled(name) is False
|
||||
|
||||
|
||||
def test_an_undeclared_flag_is_false_rather_than_an_error(monkeypatch):
|
||||
# A typo'd flag name must not raise in a request path. It is logged as an
|
||||
# error, because an undeclared flag is always a bug.
|
||||
monkeypatch.setattr(flags, "_client", None)
|
||||
assert flags.is_enabled("no_such_flag") is False
|
||||
|
||||
|
||||
def test_an_exploding_client_is_false_rather_than_a_500(monkeypatch):
|
||||
class Boom:
|
||||
def is_enabled(self, *a, **kw):
|
||||
raise RuntimeError("unleash is on fire")
|
||||
|
||||
monkeypatch.setattr(flags, "_client", Boom())
|
||||
name = next(iter(flags.REGISTRY))
|
||||
assert flags.is_enabled(name) is False
|
||||
|
||||
|
||||
def test_all_flags_reports_every_declared_flag(monkeypatch):
|
||||
monkeypatch.setattr(flags, "_client", None)
|
||||
assert set(flags.all_flags()) == set(flags.REGISTRY)
|
||||
assert all(v is False for v in flags.all_flags().values())
|
||||
|
||||
|
||||
def test_a_flag_older_than_the_limit_fails_this_test():
|
||||
"""A tripwire, not an assertion about correctness.
|
||||
|
||||
Flags are temporary scaffolding and the failure mode of every flag system
|
||||
is accumulation. This fails on the day a flag turns 90, on whatever PR
|
||||
happens to be open — which is the point: someone has to decide.
|
||||
|
||||
To fix: delete the flag and the branches that read it, or, if it genuinely
|
||||
still needs to exist, move its `added` date and say why in the commit.
|
||||
"""
|
||||
stale = [
|
||||
f.name for f in flags.REGISTRY.values()
|
||||
if date.today() - f.added > timedelta(days=flags.MAX_FLAG_AGE_DAYS)
|
||||
]
|
||||
assert not stale, (
|
||||
f"Flags older than {flags.MAX_FLAG_AGE_DAYS} days: {stale}. "
|
||||
"Remove the flag and the code branches it guards, or move its `added` "
|
||||
"date deliberately."
|
||||
)
|
||||
Reference in new issue
Block a user