refactor(ai): declare the litellm consumer contract once

Fourteen roles repeated the same two lines to say they use the gateway: a
`shared` flag gated on group_names, and an `enabled` flag conjoining that gate
with LITELLM_CHAT_MODEL_SERVED. Both are now single-sourced.

`shared` becomes literal true. The flag means the provider is a shared instance
rather than one deployed per consumer, and a central gateway is always that;
the dynamic form only said "shared if deployed", which `enabled` already
decides. The 50 shared pins under `litellm:` leave meta/variants.yml with it.
The tree had already noticed: 20 of those 50 lines carried
`# nocheck: variants-static-override`, two of them with copy-pasted text about
container_backup. This narrows e5de4e085a for one service key rather than
reversing it; every other dynamic flag keeps the group_names form.

`enabled` becomes `{{ LITELLM_USABLE }}`, defined in group_vars/all/16_ai.yml.

That shorthand is only possible because is_explicit_truth learns about it.
The resolver reads a dependency out of the flag TEXT — "in group_names" in
value — so a constant hides the role id and the provider silently leaves the
static closure of every role referencing it. Measured: the old expression
resolves True, `{{ LITELLM_USABLE }}` resolves False. GROUP_GATED_CONSTANTS
names the constants whose own definition carries the gate, and the docstring
states the obligation: a SPOT of this kind that is not listed there drops its
provider without a word.

The wider fix, accepting any Jinja as dynamic, was measured and rejected: 35
other flags carry a Jinja value without the marker (MCP sidecars, javascript
and SSO gates, recaptcha), and they would all enter closures they are not in
today.

A literal shared flag is no longer dynamic, so test_dynamic_flags wants the
`# nocheck: dynamic-flag` its own message documents for legitimate exceptions;
where a marker already existed the rule list is extended rather than a second
comment added.

Behaviour is unchanged where it counts. The closure keeps litellm with the same
app counts (wordpress 26, openwebui 23, hermes 23, mediawiki 22, flowise 23),
unrelated Jinja still resolves False, and the truth table of the new enabled
form matches the old one in all four served/deployed combinations. Three roles
resolve without litellm, and `git show HEAD:` proves that predates this change:
zammad pins enabled false in variant 0 at line 39, nextcloud at line 97.

Six unit tests cover the resolver, including one asserting that every
registered constant is actually defined in group_vars, so an entry cannot
become a dead promise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-14 19:23:59 +02:00
1 parent dac61edbc6
commit 328113e7c3
31 files changed
+102 -89

No files matched your search

+2
View File
@@ -35,6 +35,8 @@ LITELLM_CHAT_MODEL_SERVED: >-
| map(attribute='alias') | reject('search', 'embed') | list) | length > 0))
or ((OPENROUTER_API_KEY | string) | length > 0) }}
LITELLM_USABLE: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
OPENROUTER_API_KEY: ""
MCP_DISCOVERY: "{{ lookup('mcp_servers') }}"
+3 -3
View File
@@ -1,10 +1,10 @@
---
# nocheck: seaweedfs-required — Discourse's aws-sdk-s3 defaults a custom endpoint to virtual-host addressing and exposes no force_path_style option, incompatible with SeaweedFS path-style without a global SeaweedFS virtual-host/wildcard-DNS change affecting all consumers; uploads stay on the local volume
# nocheck: playwright-service-flag - shared gateway consumer; inference wiring is covered by the gateway role
# nocheck: playwright-service-flag, dynamic-flag - shared gateway consumer; inference wiring is covered by the gateway role, and the gateway is a single shared instance
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: playwright-service-flag — static-asset provider, no Playwright surface
asset:
bond: 1
@@ -22,7 +22,6 @@
shared: true
litellm:
enabled: true
shared: true
sso:
enabled: true
shared: true
@@ -59,7 +58,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
sso:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -3
View File
@@ -28,11 +28,11 @@ sso:
ldap:
enabled: false
shared: false
# nocheck: playwright-service-flag - shared gateway consumer; inference wiring is covered by the gateway role
# nocheck: playwright-service-flag, dynamic-flag - shared gateway consumer; inference wiring is covered by the gateway role, and the gateway is a single shared instance
litellm:
bond: 1
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: playwright-service-flag - DB engine, covered by role-local integration tests
qdrant:
bond: 1
-3
View File
@@ -28,7 +28,6 @@
shared: true
litellm:
enabled: true
shared: true
qdrant:
enabled: true
shared: true
@@ -68,7 +67,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
qdrant:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -108,7 +106,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
qdrant:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -3
View File
@@ -60,11 +60,11 @@ logout:
enabled: false
shared: false
# nocheck: playwright-service-flag
# nocheck: playwright-service-flag, dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 1
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
dashboard:
bond: 1
-3
View File
@@ -10,7 +10,6 @@
shared: true
litellm:
enabled: true
shared: true
dashboard:
enabled: true
shared: true
@@ -34,7 +33,6 @@
shared: true
litellm:
enabled: false
shared: false # nocheck: variants-static-override variant pins a literal off-switch
dashboard:
enabled: false
shared: false # nocheck: variants-static-override variant pins a literal off-switch
@@ -58,7 +56,6 @@
shared: false # nocheck: variants-static-override variant pins a literal off-switch
litellm:
enabled: false
shared: false # nocheck: variants-static-override variant pins a literal off-switch
dashboard:
enabled: false
shared: false # nocheck: variants-static-override variant pins a literal off-switch
+3 -2
View File
@@ -1,8 +1,9 @@
---
# nocheck: dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: dockerfile-custom
sso:
bond: 1
-3
View File
@@ -29,7 +29,6 @@
shared: true
litellm:
enabled: false
shared: false
sso:
enabled: true
shared: true
@@ -73,7 +72,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
sso:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -120,7 +118,6 @@
shared: true
litellm:
enabled: true
shared: true
sso:
enabled: true
shared: true
+3 -2
View File
@@ -113,10 +113,11 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant
bond: 1
enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}"
shared: true
# nocheck: dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: playwright-service-flag internal adapter sidecar, covered by the role's MCP spec
# nocheck: dockerfile-custom the Dockerfile is owned by svc-ai-mcp-adapter and staged per instance, so every adapter-backed provider shares one audited build
# nocheck: dynamic-flag the sidecar tracks this role's own mcp flag, not another role's presence; a group_names expression would decide its exposure from an unrelated deployment
@@ -37,7 +37,6 @@
shared: true
litellm:
enabled: false
shared: false
tor:
enabled: true
openwebui:
@@ -83,7 +82,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
tor:
enabled: true
openwebui:
@@ -130,7 +128,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: true
shared: true
tor:
enabled: true
openwebui:
+3 -3
View File
@@ -1,9 +1,9 @@
---
# nocheck: playwright-service-flag - shared gateway consumer; inference wiring is covered by the gateway role
# nocheck: playwright-service-flag, dynamic-flag - shared gateway consumer; inference wiring is covered by the gateway role, and the gateway is a single shared instance
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: seaweedfs-required — object storage not implemented for this role
sso:
bond: 1
@@ -26,7 +26,6 @@
shared: true
litellm:
enabled: true
shared: true
sso:
enabled: true
shared: true
@@ -58,7 +57,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
sso:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -91,7 +89,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: true
shared: true
sso:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -2
View File
@@ -1,8 +1,9 @@
---
# nocheck: dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: seaweedfs-required — object storage not implemented for this role
sso:
bond: 1
-3
View File
@@ -38,7 +38,6 @@
shared: true
litellm:
enabled: false
shared: false
tor:
enabled: true
@@ -83,7 +82,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
tor:
enabled: true
openwebui:
@@ -128,7 +126,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: true
shared: true
tor:
enabled: true
openwebui:
+3 -2
View File
@@ -93,10 +93,11 @@ container_backup: # nocheck: bond-baseline backup host joins outside the variant
bond: 1
enabled: "{{ 'svc-bkp-volume-2-local' in group_names }}"
shared: true
# nocheck: dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: playwright-service-flag MCP pairing is proven by the role's own MCP spec, not by a per-consumer gate
openwebui:
bond: 0.5
-2
View File
@@ -27,7 +27,6 @@
shared: true
litellm:
enabled: true
shared: true
tor:
enabled: true
@@ -69,7 +68,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
tor:
enabled: true
openwebui:
+3 -3
View File
@@ -335,11 +335,11 @@ openwebui:
bond: 0.5
enabled: "{{ 'web-app-openwebui' in group_names }}"
shared: "{{ 'web-app-openwebui' in group_names }}"
# nocheck: playwright-service-flag: bridged litellm gateway partner, covered by partner role spec
# nocheck: playwright-service-flag, dynamic-flag - bridged litellm gateway partner, covered by partner role spec, and a single shared instance
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: playwright-service-flag: bridged mastodon partner, covered by partner role spec
mastodon:
bond: 0.5
-14
View File
@@ -96,7 +96,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -193,7 +192,6 @@
shared: true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -332,7 +330,6 @@
shared: true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -427,7 +424,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -522,7 +518,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: true
shared: true
@@ -617,7 +612,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -712,7 +706,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -807,7 +800,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: true
shared: true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -902,7 +894,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -997,7 +988,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -1092,7 +1082,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -1187,7 +1176,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -1282,7 +1270,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -1377,7 +1364,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: true
shared: true
mastodon:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -3
View File
@@ -50,11 +50,11 @@ sso:
- "{{ lookup('rbac_group_path', application_id='web-app-openclaw', role='administrator') }}"
- "{{ lookup('rbac_group_path', application_id='web-app-openclaw', role='mcp') }}"
# nocheck: playwright-service-flag
# nocheck: playwright-service-flag, dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 1
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
logout:
bond: 1
-3
View File
@@ -10,7 +10,6 @@
shared: true
litellm:
enabled: true
shared: true
logout:
enabled: true
shared: true
@@ -37,7 +36,6 @@
shared: true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
logout:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -64,7 +62,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
logout:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -3
View File
@@ -81,11 +81,11 @@ css:
javascript:
enabled: false # nocheck: dynamic-flag
shared: true
# nocheck: playwright-service-flag — AI backend, infrastructural
# nocheck: playwright-service-flag, dynamic-flag — AI backend, infrastructural, and a single shared instance
litellm:
bond: 1
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
email:
bond: 1
@@ -27,7 +27,6 @@
shared: true
litellm:
enabled: true
shared: true
prometheus:
enabled: true
shared: true
@@ -63,7 +62,6 @@
shared: true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -102,7 +100,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
prometheus:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -3
View File
@@ -1,9 +1,9 @@
---
# nocheck: playwright-service-flag - shared gateway consumer; inference wiring is covered by the gateway role
# nocheck: playwright-service-flag, dynamic-flag - shared gateway consumer; inference wiring is covered by the gateway role, and the gateway is a single shared instance
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: seaweedfs-required, object storage not implemented for this role
sso:
bond: 1
@@ -35,7 +35,6 @@
shared: true
litellm:
enabled: true
shared: true
sso:
enabled: true
shared: true
@@ -79,7 +78,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
sso:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -2
View File
@@ -1,8 +1,9 @@
---
# nocheck: dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: seaweedfs-required — object storage not implemented for this role
sso:
bond: 1
-3
View File
@@ -7,7 +7,6 @@
- services:
litellm:
enabled: false
shared: false
container_backup:
enabled: true
dashboard:
@@ -40,7 +39,6 @@
- services:
litellm:
enabled: false
shared: false
container_backup:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -74,7 +72,6 @@
- services: # nocheck: variants-auth-isolation
litellm:
enabled: true
shared: true
container_backup:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
+3 -2
View File
@@ -1,8 +1,9 @@
---
# nocheck: dynamic-flag - the gateway is a single shared instance, never deployed per consumer
litellm:
bond: 0.5
enabled: "{{ ('svc-ai-litellm' in group_names) and (LITELLM_CHAT_MODEL_SERVED | bool) }}"
shared: "{{ 'svc-ai-litellm' in group_names }}"
enabled: "{{ LITELLM_USABLE }}"
shared: true
# nocheck: seaweedfs-required - object storage not implemented for this role
sso:
bond: 1
-3
View File
@@ -38,7 +38,6 @@
shared: true
litellm:
enabled: false
shared: false
tor:
enabled: true
openwebui:
@@ -86,7 +85,6 @@
shared: false # nocheck: variants-static-override backup shared stays literally true
litellm:
enabled: false
shared: false
tor:
enabled: true
@@ -117,7 +115,6 @@
shared: true
litellm:
enabled: true
shared: true
logout:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -4,12 +4,15 @@ import tempfile
import unittest
from pathlib import Path
from utils.cache.files import PROJECT_ROOT
from utils.cache.yaml import dump_yaml_str
from utils.roles.applications.services.registry import (
GROUP_GATED_CONSTANTS,
ServiceRegistryError,
build_service_registry_from_applications,
detect_service_bucket,
detect_service_channel,
is_explicit_truth,
ordered_primary_service_entries,
resolve_service_dependency_roles_from_config,
)
@@ -335,5 +338,42 @@ class TestServiceRegistryOrdering(unittest.TestCase):
ordered_primary_service_entries(registry, roles_dir)
class TestExplicitTruth(unittest.TestCase):
"""A flag may resolve a dependency through a group-gated constant.
The substring rule reads the role id out of the flag itself, so a SPOT
constant hides it. Roles referencing one keep their provider only while
the constant is registered.
"""
def test_a_literal_true_resolves(self):
self.assertTrue(is_explicit_truth(True))
def test_the_in_group_names_form_resolves(self):
self.assertTrue(is_explicit_truth("{{ 'web-app-keycloak' in group_names }}"))
def test_every_registered_constant_resolves(self):
for constant in GROUP_GATED_CONSTANTS:
with self.subTest(constant=constant):
self.assertTrue(is_explicit_truth("{{ " + constant + " }}"))
def test_an_unregistered_jinja_expression_does_not_resolve(self):
self.assertFalse(
is_explicit_truth("{{ lookup('config', 'web-app-gitea', 'mcp.enabled') }}")
)
def test_a_literal_false_does_not_resolve(self):
self.assertFalse(is_explicit_truth(False))
def test_every_registered_constant_is_defined_in_group_vars(self):
group_vars = PROJECT_ROOT / "group_vars" / "all"
defined = "".join(
path.read_text(encoding="utf-8") for path in group_vars.glob("*.yml")
)
for constant in GROUP_GATED_CONSTANTS:
with self.subTest(constant=constant):
self.assertIn(f"{constant}:", defined)
if __name__ == "__main__":
unittest.main()
+18 -3
View File
@@ -28,10 +28,21 @@ def _normalized_name(value: Any) -> str:
return value.strip()
GROUP_GATED_CONSTANTS: tuple[str, ...] = ("LITELLM_USABLE",)
"""Group_vars constants whose own definition carries an ``in group_names``
gate, so a flag referencing one is dynamic even though its own text is not.
A constant hides the role id the substring rule looks for, so every SPOT of
this kind MUST be listed here or the roles referencing it silently drop their
provider from the static closure.
"""
def is_explicit_truth(value: Any) -> bool:
"""A services.<key> flag (``enabled`` / ``shared``) MAY be true if it
is the literal Python ``True`` OR any string containing the substring
``in group_names``. Negated and compound predicates
is the literal Python ``True``, any string containing the substring
``in group_names``, or a string referencing a `GROUP_GATED_CONSTANTS`
entry. Negated and compound predicates
(``"{{ 'X' not in group_names }}"``) match on purpose: they are true
on some node shape, and the static closure is the co-deploy superset.
Mirrors the substring test at
@@ -47,7 +58,11 @@ def is_explicit_truth(value: Any) -> bool:
"""
if value is True:
return True
return bool(isinstance(value, str) and "in group_names" in value)
if not isinstance(value, str):
return False
return "in group_names" in value or any(
constant in value for constant in GROUP_GATED_CONSTANTS
)
def detect_service_channel(role_name: str) -> str: