Files
computer-playbook/tests/unit/python/utils/roles/validation/test_invokable.py
T
kevinveenbirkenbachandClaude Opus 5 53c120aaac refactor(dsk): rename the desktop tier and give its agents a category
Every desk- role becomes dsk-, and the three coding agents move to dsk-gnt- under a category of their own. The category carries a title, hanzi and icon but no invokable flag: an invokable child under an invokable parent is rejected outright, which is also why web itself is not invokable.

Three things a search and replace would have broken without a sound. get_entity_name now strips dsk-gnt-, so the primary entity keys agnt-claude, agnt-cursor and agnt-pi no longer resolved and all three roles dropped out of the CI plan. Eighteen run_once guards carried the old name against a convention its own test enforces. DESKTOP_ROLE_PREFIX in the addon spec lint is a literal, so the desktop exemption it grants would have matched nothing and the lint would have moved to the wrong rule in silence.

docs/requirements/035 is not a historical document but a register a lint holds to every role with an application_id, so its list carries the new ids. In 026 only the links move; the prose stays as a statement about the tree at the time it was written.

Measured against the plan with the rename normalised, the deployed role#variant set is unchanged. A stale id in an inventory is caught before any deploy starts: the pre-flight validator reports Invalid group 'desk-firefox' (not in application_ids).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 22:15:41 +02:00

160 lines
5.5 KiB
Python

# tests/unit/python/utils/test_invokable.py
from __future__ import annotations
import re
import shutil
import tempfile
from pathlib import Path
from unittest import TestCase, main
from unittest.mock import patch
import utils.roles.validation.invokable as inv
from utils.cache.yaml import dump_yaml
from utils.roles.categories import categories_file
from utils.roles.mapping import ROLE_FILE_VARS_MAIN
class TestInvokable(TestCase):
def setUp(self) -> None:
self.tmp = Path(tempfile.mkdtemp(prefix="invokable_modutils_test_"))
self.roles_dir = self.tmp / "roles"
self.roles_dir.mkdir(parents=True, exist_ok=True)
self.categories_file = categories_file(self.tmp)
dump_yaml(
self.categories_file,
{"categories": [{"invokable_paths": ["web-app", "update", "util-dsk"]}]},
)
def mk_role(name: str, application_id: str | None = None) -> None:
rd = self.roles_dir / name
(rd / "vars").mkdir(parents=True, exist_ok=True)
if application_id is not None:
dump_yaml(rd / ROLE_FILE_VARS_MAIN, {"application_id": application_id})
else:
dump_yaml(rd / ROLE_FILE_VARS_MAIN, {})
mk_role("web-app-nextcloud", "web-app-nextcloud")
mk_role("web-app-matomo", "matomo-app")
mk_role("web-svc-nginx", None) # not invokable by our patched paths
mk_role("update", None) # exact match
mk_role("util-dsk-custom", None) # prefix match
def tearDown(self) -> None:
shutil.rmtree(self.tmp)
def test_list_invokable_app_ids(self) -> None:
with (
patch.object(inv, "PROJECT_ROOT", self.tmp),
patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-dsk"],
),
):
got = inv.list_invokable_app_ids()
self.assertEqual(
got,
sorted(["web-app-nextcloud", "matomo-app", "update", "util-dsk-custom"]),
)
def test_list_invokables_by_type(self) -> None:
with (
patch.object(inv, "PROJECT_ROOT", self.tmp),
patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-dsk"],
),
):
grouped = inv.list_invokables_by_type()
self.assertIn("server", grouped)
self.assertIn("workstation", grouped)
self.assertIn("universal", grouped)
self.assertEqual(grouped["server"], sorted(["web-app-nextcloud", "matomo-app"]))
self.assertEqual(grouped["workstation"], sorted(["util-dsk-custom"]))
self.assertEqual(grouped["universal"], ["update"])
def test_list_invokables_by_type_exclude(self) -> None:
"""A synthetic rule exercises the exclude_re path of
DeploymentTypeRule. The historical DEFAULT_RULES exclude (the now
dissolved oauth2-proxy helper role) was removed when its sidecar
logic was absorbed into web-app-keycloak."""
rd = self.roles_dir / "web-app-excluded"
(rd / "vars").mkdir(parents=True, exist_ok=True)
dump_yaml(rd / ROLE_FILE_VARS_MAIN, {})
rules = (
inv.DeploymentTypeRule(
name="server",
include_re=re.compile(r"^(web-app-|web-svc-)"),
exclude_re=re.compile(r"^web-app-excluded$"),
),
inv.DeploymentTypeRule(
name="universal", include_re=re.compile(r".*"), exclude_re=None
),
)
with (
patch.object(inv, "PROJECT_ROOT", self.tmp),
patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-dsk"],
),
):
grouped = inv.list_invokables_by_type(rules=rules)
self.assertNotIn("web-app-excluded", grouped["server"])
def test_list_invokables_by_type_skip_mode(self) -> None:
def fake_skip(role_dir):
return ["compose"] if role_dir.name == "web-app-matomo" else []
with (
patch.object(inv, "PROJECT_ROOT", self.tmp),
patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-dsk"],
),
patch.object(inv, "_role_skip_modes", side_effect=fake_skip),
):
excluded = inv.list_invokables_by_type(skip_mode="compose")
kept = inv.list_invokables_by_type(skip_mode="swarm")
self.assertNotIn("matomo-app", excluded["server"])
self.assertIn("matomo-app", kept["server"])
self.assertIn("web-app-nextcloud", excluded["server"])
def test_types_from_group_names(self) -> None:
"""``all`` and ``foo`` are not invokable and drop out; the three
invokable names map to server, workstation and the universal
leftover respectively."""
with (
patch.object(inv, "PROJECT_ROOT", self.tmp),
patch.object(
inv,
"_get_invokable_paths",
return_value=["web-app", "update", "util-dsk"],
),
):
got = inv.types_from_group_names(
[
"all",
"web-app-nextcloud",
"util-dsk-custom",
"update",
"foo",
]
)
self.assertEqual(got, ["server", "universal", "workstation"])
if __name__ == "__main__":
main()