mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
Merge branch 'feature/svc-ai-litellm/remote-models-config' into feature/store-i18n
# Conflicts: # cli/build/docs/readme/generate.py # docs/requirements/035-mcp-proxy-expansion.md # roles/svc-ai-lmstudio/meta/services.yml
This commit is contained in:
commit
f2cee9bb34
532 files changed
+12314
-546
No files matched your search
@@ -61,12 +61,12 @@ class TestOwningRole(unittest.TestCase):
|
||||
|
||||
def test_foreign_role_id_fails(self):
|
||||
with self.assertRaises(AnsibleActionFail) as caught:
|
||||
self._spec_for("nfs-ganesha", "desk-micro")
|
||||
self._spec_for("nfs-ganesha", "dsk-micro")
|
||||
self.assertIn("svc-storage-nfs-server", str(caught.exception))
|
||||
|
||||
def test_unknown_id_fails(self):
|
||||
with self.assertRaises(AnsibleActionFail):
|
||||
self._spec_for("nope", "desk-micro")
|
||||
self._spec_for("nope", "dsk-micro")
|
||||
|
||||
def test_own_id_resolves(self):
|
||||
with mock.patch(
|
||||
@@ -78,7 +78,7 @@ class TestOwningRole(unittest.TestCase):
|
||||
|
||||
def test_shared_id_resolves_for_any_role(self):
|
||||
with mock.patch("plugins.action.package_install.resolve", return_value="spec"):
|
||||
spec = self._spec_for("git", "desk-micro")
|
||||
spec = self._spec_for("git", "dsk-micro")
|
||||
self.assertEqual(spec, "spec")
|
||||
|
||||
def test_no_role_context_skips_the_check(self):
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestGetAllInvokableApps(unittest.TestCase):
|
||||
"util": {
|
||||
"title": "utils",
|
||||
"invokable": False,
|
||||
"desk": {"title": "Desktop utils", "invokable": True},
|
||||
"dsk": {"title": "Desktop utils", "invokable": True},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class TestGetDeploymentTypesFromGroups(unittest.TestCase):
|
||||
return [
|
||||
"web-app",
|
||||
"web-svc",
|
||||
"desk",
|
||||
"dsk",
|
||||
"update",
|
||||
]
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestGetDeploymentTypesFromGroups(unittest.TestCase):
|
||||
get_deployment_types_from_groups(
|
||||
[
|
||||
"web-app-nextcloud",
|
||||
"desk-firefox",
|
||||
"dsk-firefox",
|
||||
]
|
||||
),
|
||||
["server", "workstation"],
|
||||
@@ -48,7 +48,7 @@ class TestGetDeploymentTypesFromGroups(unittest.TestCase):
|
||||
get_deployment_types_from_groups(
|
||||
[
|
||||
"web-svc-logout",
|
||||
"desk-nextcloud",
|
||||
"dsk-nextcloud",
|
||||
]
|
||||
),
|
||||
["server", "workstation"],
|
||||
|
||||
@@ -22,9 +22,7 @@ class TestGetEntityNameFilter(unittest.TestCase):
|
||||
"app": {"title": "Applications", "invokable": True},
|
||||
"svc": {"title": "Services", "invokable": True},
|
||||
},
|
||||
"util": {
|
||||
"desk": {"dev": {"title": "Dev Utilities", "invokable": True}}
|
||||
},
|
||||
"util": {"dsk": {"dev": {"title": "Dev Utilities", "invokable": True}}},
|
||||
"sys": {
|
||||
"ctl": {
|
||||
"bkp": {"title": "Backup", "invokable": True},
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from plugins.filter.litellm.model_routes import litellm_model_routes
|
||||
|
||||
OLLAMA_URL = "http://ollama:11434"
|
||||
LMSTUDIO_URL = "http://lmstudio:1234"
|
||||
|
||||
SHARED = {"alias": "qwen2.5:0.5b", "name": "qwen2.5-0.5b-instruct", "context": 32768}
|
||||
LMSTUDIO_ONLY = {"alias": "mistral:latest", "name": "mistral-7b-instruct-v0.3"}
|
||||
REMOTE = {
|
||||
"alias": "openrouter/auto",
|
||||
"model": "openrouter/openrouter/auto",
|
||||
"provider": "openrouter",
|
||||
}
|
||||
MOCK = {
|
||||
"alias": "mock/deterministic",
|
||||
"provider": "mock",
|
||||
"response": "pong",
|
||||
"context": 131072,
|
||||
}
|
||||
|
||||
|
||||
def routes(ollama=(), lmstudio=(), declared=()):
|
||||
return litellm_model_routes(
|
||||
list(ollama),
|
||||
list(lmstudio),
|
||||
list(declared),
|
||||
ollama_url=OLLAMA_URL,
|
||||
lmstudio_url=LMSTUDIO_URL,
|
||||
max_tokens=512,
|
||||
timeout=60,
|
||||
mock_provider=MOCK["provider"],
|
||||
)
|
||||
|
||||
|
||||
def by_alias(built):
|
||||
return {route["alias"]: route for route in built}
|
||||
|
||||
|
||||
class TestPublicationOrder(unittest.TestCase):
|
||||
"""The first route is what LITELLM_CHAT_MODEL names, so order is a contract."""
|
||||
|
||||
def test_a_local_model_outranks_a_declared_one(self) -> None:
|
||||
built = routes(ollama=[SHARED], declared=[MOCK, REMOTE])
|
||||
self.assertEqual(next(route["alias"] for route in built), SHARED["alias"])
|
||||
|
||||
def test_declared_models_keep_their_declaration_order(self) -> None:
|
||||
built = routes(declared=[MOCK, REMOTE])
|
||||
self.assertEqual(
|
||||
[route["alias"] for route in built], [MOCK["alias"], REMOTE["alias"]]
|
||||
)
|
||||
|
||||
def test_nothing_declared_publishes_nothing(self) -> None:
|
||||
self.assertEqual(routes(), [])
|
||||
|
||||
|
||||
class TestLocalBackends(unittest.TestCase):
|
||||
def test_ollama_is_called_through_its_own_scheme(self) -> None:
|
||||
params = by_alias(routes(ollama=[SHARED]))[SHARED["alias"]]["params"]
|
||||
self.assertEqual(params["model"], f"ollama/{SHARED['alias']}")
|
||||
self.assertEqual(params["api_base"], OLLAMA_URL)
|
||||
|
||||
def test_a_declared_window_reaches_the_backend_and_the_model_info(self) -> None:
|
||||
route = by_alias(routes(ollama=[SHARED]))[SHARED["alias"]]
|
||||
self.assertEqual(route["params"]["num_ctx"], SHARED["context"])
|
||||
self.assertEqual(route["context"], SHARED["context"])
|
||||
|
||||
def test_a_model_without_a_window_sets_no_num_ctx(self) -> None:
|
||||
route = by_alias(routes(lmstudio=[LMSTUDIO_ONLY]))[LMSTUDIO_ONLY["alias"]]
|
||||
self.assertNotIn("num_ctx", route["params"])
|
||||
self.assertIsNone(route["context"])
|
||||
|
||||
def test_lmstudio_publishes_the_window_it_declares(self) -> None:
|
||||
route = by_alias(routes(lmstudio=[SHARED]))[SHARED["alias"]]
|
||||
self.assertEqual(
|
||||
route["context"],
|
||||
SHARED["context"],
|
||||
"the LM Studio branch used to drop the window its README promised",
|
||||
)
|
||||
|
||||
def test_an_alias_both_backends_serve_is_published_once_by_ollama(self) -> None:
|
||||
built = routes(ollama=[SHARED], lmstudio=[SHARED, LMSTUDIO_ONLY])
|
||||
self.assertEqual(
|
||||
[route["alias"] for route in built],
|
||||
[SHARED["alias"], LMSTUDIO_ONLY["alias"]],
|
||||
)
|
||||
self.assertEqual(
|
||||
by_alias(built)[SHARED["alias"]]["params"]["api_base"], OLLAMA_URL
|
||||
)
|
||||
|
||||
|
||||
class TestDeclaredModels(unittest.TestCase):
|
||||
def test_a_provider_route_reads_its_key_from_the_environment(self) -> None:
|
||||
params = by_alias(routes(declared=[REMOTE]))[REMOTE["alias"]]["params"]
|
||||
self.assertEqual(params["api_key"], "os.environ/OPENROUTER_API_KEY")
|
||||
self.assertEqual(params["model"], REMOTE["model"])
|
||||
|
||||
def test_a_mock_answers_from_its_own_response(self) -> None:
|
||||
params = by_alias(routes(declared=[MOCK]))[MOCK["alias"]]["params"]
|
||||
self.assertEqual(params["mock_response"], MOCK["response"])
|
||||
|
||||
def test_a_mock_names_no_upstream_to_call(self) -> None:
|
||||
params = by_alias(routes(declared=[MOCK]))[MOCK["alias"]]["params"]
|
||||
self.assertNotIn("api_base", params)
|
||||
self.assertNotIn(
|
||||
"timeout", params, "a mock cannot time out against a backend it never calls"
|
||||
)
|
||||
|
||||
def test_a_mock_publishes_its_window(self) -> None:
|
||||
self.assertEqual(
|
||||
by_alias(routes(declared=[MOCK]))[MOCK["alias"]]["context"], MOCK["context"]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -94,14 +94,14 @@ class TestDotenvQuoteModeAware(unittest.TestCase):
|
||||
def test_swarm_non_string_is_stringified(self):
|
||||
self.assertEqual(self._render(42, mode="swarm"), "42")
|
||||
|
||||
def test_swarm_quotes_value_with_interior_whitespace(self):
|
||||
def test_swarm_passes_a_value_with_spaces_through_unchanged(self):
|
||||
self.assertEqual(
|
||||
self._render("My Org Helpdesk", mode="swarm"), '"My Org Helpdesk"'
|
||||
self._render('{"hermes": {"group": "/x"}}', mode="swarm"),
|
||||
'{"hermes": {"group": "/x"}}',
|
||||
"swarm delivers the value verbatim, so quoting it makes the "
|
||||
"container read the quotes as part of the JSON and parse a string",
|
||||
)
|
||||
|
||||
def test_swarm_whitespace_value_escapes_quotes_not_dollars(self):
|
||||
self.assertEqual(self._render('a "b" $c', mode="swarm"), '"a \\"b\\" $c"')
|
||||
|
||||
def test_compose_mode_quotes_like_default(self):
|
||||
self.assertEqual(self._render("abc", mode="compose"), '"abc"')
|
||||
self.assertEqual(self._render("$ecret", mode="compose"), '"$$ecret"')
|
||||
|
||||
@@ -47,12 +47,23 @@ class TestCaInjectedLookup(unittest.TestCase):
|
||||
def setUpClass(cls):
|
||||
cls.mod = _load_lookup()
|
||||
|
||||
def _resolve(self, *, domains, enabled, mode, app="web-app-moodle"):
|
||||
def _resolve(
|
||||
self,
|
||||
*,
|
||||
domains,
|
||||
enabled,
|
||||
mode,
|
||||
app="web-app-moodle",
|
||||
services=None,
|
||||
tls_mode="self_signed",
|
||||
):
|
||||
calls: list[tuple[str, list]] = []
|
||||
|
||||
def fake_get(name, **_kwargs):
|
||||
def run(args, variables=None):
|
||||
calls.append((name, list(args)))
|
||||
if name == "config":
|
||||
return [services or {}]
|
||||
if name == "domains":
|
||||
return [domains]
|
||||
return [enabled if args[1] == "enabled" else mode]
|
||||
@@ -63,7 +74,8 @@ class TestCaInjectedLookup(unittest.TestCase):
|
||||
plugin._loader = None
|
||||
plugin._templar = _DummyTemplar()
|
||||
with mock.patch.object(self.mod.lookup_loader, "get", side_effect=fake_get):
|
||||
result = plugin.run([app], variables={})
|
||||
variables = {} if tls_mode is None else {"TLS_MODE": tls_mode}
|
||||
result = plugin.run([app], variables=variables)
|
||||
return result[0], calls
|
||||
|
||||
def test_a_self_signed_app_with_a_domain_gets_the_ca(self):
|
||||
@@ -93,7 +105,64 @@ class TestCaInjectedLookup(unittest.TestCase):
|
||||
def test_an_app_without_a_domain_never_asks_about_tls(self):
|
||||
verdict, calls = self._resolve(domains={}, enabled=True, mode="self_signed")
|
||||
self.assertIs(verdict, False)
|
||||
self.assertEqual([name for name, _ in calls], ["domains"])
|
||||
self.assertEqual([name for name, _ in calls], ["domains", "config"])
|
||||
|
||||
def test_a_domainless_ca_client_gets_the_ca_on_the_deployment_mode(self):
|
||||
verdict, calls = self._resolve(
|
||||
domains={},
|
||||
enabled=True,
|
||||
mode="self_signed",
|
||||
services={"agent-broker": {"ca_client": True}, "socket-proxy": {}},
|
||||
)
|
||||
self.assertIs(
|
||||
verdict,
|
||||
True,
|
||||
"a container that only calls the self-signed endpoint has no domain "
|
||||
"of its own, and without the CA every outbound call fails the "
|
||||
"handshake",
|
||||
)
|
||||
self.assertEqual([name for name, _ in calls], ["domains", "config"])
|
||||
|
||||
def test_a_ca_client_with_a_domain_follows_its_own_tls_mode(self):
|
||||
verdict, calls = self._resolve(
|
||||
domains={"web-app-moodle": "moodle.example"},
|
||||
enabled=True,
|
||||
mode="letsencrypt",
|
||||
services={"agent-broker": {"ca_client": True}},
|
||||
tls_mode="self_signed",
|
||||
)
|
||||
self.assertIs(
|
||||
verdict,
|
||||
False,
|
||||
"an app that has a domain is served over its own resolved mode, which "
|
||||
"may override the deployment default; the ca_client shortcut must not "
|
||||
"answer for it on the global TLS_MODE",
|
||||
)
|
||||
self.assertEqual([name for name, _ in calls], ["domains", "tls", "tls"])
|
||||
|
||||
def test_a_ca_client_gets_nothing_when_the_ca_is_not_self_signed(self):
|
||||
verdict, _ = self._resolve(
|
||||
domains={},
|
||||
enabled=True,
|
||||
mode="self_signed",
|
||||
services={"agent-broker": {"ca_client": True}},
|
||||
tls_mode="letsencrypt",
|
||||
)
|
||||
self.assertIs(verdict, False)
|
||||
|
||||
def test_a_ca_client_refuses_an_absent_tls_mode(self):
|
||||
with self.assertRaises(
|
||||
AnsibleError,
|
||||
msg="a missing mode read as 'not self signed' withholds the CA in "
|
||||
"silence, and every handshake then fails far from here",
|
||||
):
|
||||
self._resolve(
|
||||
domains={},
|
||||
enabled=True,
|
||||
mode="self_signed",
|
||||
services={"agent-broker": {"ca_client": True}},
|
||||
tls_mode=None,
|
||||
)
|
||||
|
||||
def test_the_term_is_required(self):
|
||||
plugin = self.mod.LookupModule()
|
||||
|
||||
@@ -30,6 +30,14 @@ class _DomainsResolveStub:
|
||||
return [(variables or {}).get("domains", {})]
|
||||
|
||||
|
||||
class _CaInjectedStub:
|
||||
def __init__(self, injected: bool):
|
||||
self._injected = injected
|
||||
|
||||
def run(self, terms, variables=None, **kwargs):
|
||||
return [self._injected]
|
||||
|
||||
|
||||
class TestComposeFArgs(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.m = _load_module(
|
||||
@@ -48,18 +56,48 @@ class TestComposeFArgs(unittest.TestCase):
|
||||
},
|
||||
}
|
||||
|
||||
def _loader_dispatch(self, tls_stub):
|
||||
# "domains" resolves through variables['domains'] to keep tests hermetic;
|
||||
# "tls" resolves through the per-test stub.
|
||||
def _loader_dispatch(self, tls_stub, ca_injected=None):
|
||||
verdict = tls_stub.run(None)[0]
|
||||
injected = (
|
||||
(verdict["enabled"] and verdict["mode"] == "self_signed")
|
||||
if ca_injected is None
|
||||
else ca_injected
|
||||
)
|
||||
|
||||
def _get(name, *args, **kwargs):
|
||||
if name == "domains":
|
||||
return _DomainsResolveStub()
|
||||
if name == "tls":
|
||||
return tls_stub
|
||||
if name == "ca_injected":
|
||||
return _CaInjectedStub(injected)
|
||||
raise AssertionError(f"unexpected lookup name: {name}")
|
||||
|
||||
return _get
|
||||
|
||||
def test_a_domainless_ca_client_still_gets_the_ca_override_file(self):
|
||||
with (
|
||||
patch.object(
|
||||
self.m.lookup_loader,
|
||||
"get",
|
||||
side_effect=self._loader_dispatch(
|
||||
_TlsResolveStub(False, "off"), ca_injected=True
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
self.m, "get_docker_paths", side_effect=self._stub_get_docker_paths
|
||||
),
|
||||
patch.object(self.m, "_role_provides_override", return_value=False),
|
||||
):
|
||||
out = self.lookup.run(["web-app-a"], variables={**self.vars, "domains": {}})
|
||||
self.assertIn(
|
||||
"/x/compose.ca.override.yml",
|
||||
out[0],
|
||||
"ca_injected grants a domainless ca_client the override and the "
|
||||
"handler writes it; a domain gate here leaves that file on disk "
|
||||
"while the container runs without a trust store",
|
||||
)
|
||||
|
||||
def _stub_get_docker_paths(self, application_id: str, base_dir: str) -> dict:
|
||||
self.assertEqual(application_id, "web-app-a")
|
||||
self.assertEqual(base_dir, "/x/")
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""The stage loop skips roles no host carries, and gives every host one list.
|
||||
|
||||
Dropping what nobody deploys is free: 132 of 139 entries were skipped one task
|
||||
at a time on a single-app deploy, 278 task deltas, 41.8 seconds measured on
|
||||
2026-09-23.
|
||||
|
||||
Dropping what *this* host does not carry is not free, and these cases forbid
|
||||
it. 8aea5a9b35 measured that a per-host condition above the loop makes ansible
|
||||
insert blocks in host order rather than loop order, after which
|
||||
svc-storage-nfs-client mounted an export svc-storage-nfs-server had not
|
||||
deployed yet (run 31645153977). The list therefore has to come out identical
|
||||
for every host, and per-host narrowing stays in the body's ``when``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from typing import ClassVar
|
||||
from unittest import mock
|
||||
|
||||
from plugins.lookup.group_roles import LookupModule
|
||||
|
||||
ENTRIES = [
|
||||
{"role": "web-app-alpha", "app": "alpha"},
|
||||
{"role": "web-app-beta", "app": "beta"},
|
||||
{"role": "web-app-gamma", "app": "gamma"},
|
||||
]
|
||||
|
||||
|
||||
def run(**kwargs):
|
||||
with mock.patch(
|
||||
"plugins.lookup.group_roles.ordered_roles", return_value=list(ENTRIES)
|
||||
):
|
||||
return LookupModule().run(["web-app"], **kwargs)[0]
|
||||
|
||||
|
||||
class TestWithoutAnInventory(unittest.TestCase):
|
||||
def test_no_groups_returns_the_whole_group(self) -> None:
|
||||
self.assertEqual(run(), ENTRIES)
|
||||
|
||||
|
||||
class TestDroppingWhatNobodyDeploys(unittest.TestCase):
|
||||
def test_a_role_no_host_carries_is_dropped(self) -> None:
|
||||
selected = run(inventory_groups={"alpha": ["h1"], "beta": []})
|
||||
self.assertEqual([entry["app"] for entry in selected], ["alpha"])
|
||||
|
||||
def test_an_empty_inventory_deploys_nothing(self) -> None:
|
||||
self.assertEqual(run(inventory_groups={}), [])
|
||||
|
||||
def test_run_after_order_survives_the_filter(self) -> None:
|
||||
selected = run(inventory_groups={"gamma": ["h1"], "alpha": ["h1"]})
|
||||
self.assertEqual(
|
||||
[entry["app"] for entry in selected],
|
||||
["alpha", "gamma"],
|
||||
"run_after order is the contract; the inventory's key order is not",
|
||||
)
|
||||
|
||||
|
||||
class TestSameListForEveryHost(unittest.TestCase):
|
||||
"""The property 8aea5a9b35 paid for; losing it inverts deploy order."""
|
||||
|
||||
GROUPS: ClassVar[dict] = {"alpha": ["h1"], "beta": ["h2"], "gamma": ["h1", "h2"]}
|
||||
|
||||
def test_a_role_only_one_host_carries_still_enters_the_loop(self) -> None:
|
||||
selected = [entry["app"] for entry in run(inventory_groups=self.GROUPS)]
|
||||
self.assertEqual(
|
||||
selected,
|
||||
["alpha", "beta", "gamma"],
|
||||
"beta belongs to h2 only; dropping it for h1 would give the two "
|
||||
"hosts different loops and let host order decide item order",
|
||||
)
|
||||
|
||||
def test_the_lookup_reads_no_per_host_variable(self) -> None:
|
||||
selected = run(inventory_groups=self.GROUPS, group_names=["alpha"])
|
||||
self.assertEqual(
|
||||
[entry["app"] for entry in selected],
|
||||
["alpha", "beta", "gamma"],
|
||||
"a host's own groups must not reach this lookup; narrowing to one "
|
||||
"host belongs in the body's when",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -245,6 +245,29 @@ class RolesWithServiceLookupTests(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(self._run(["dashboard"], applications), [[]])
|
||||
|
||||
def test_include_headless_keeps_a_role_without_canonical_domain(self):
|
||||
applications = {
|
||||
"svc-ai-foo": {
|
||||
"services": {
|
||||
"litellm": {"enabled": True, "shared": True},
|
||||
},
|
||||
"domains": {},
|
||||
},
|
||||
}
|
||||
self.assertEqual(
|
||||
self._run(["litellm"], applications, include_headless=True),
|
||||
[
|
||||
[
|
||||
{
|
||||
"id": "svc-ai-foo",
|
||||
"canonical_domain": "",
|
||||
"canonical_url": "",
|
||||
"iframe": True,
|
||||
}
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
def test_canonical_as_string_is_accepted(self):
|
||||
applications = {
|
||||
"web-app-foo": {
|
||||
|
||||
Reference in new issue
Block a user