mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 21:26:44 +00:00
Four independent bugs were each blocking the autonomous
fresh-purged-apps iteration of web-app-{openwebui,wordpress,nextcloud,
phpmyadmin,yourls}; bundling them so the pipeline keeps running.
- cli/core/discovery.py: stop walking argv prefixes at the first flag
or path-shaped arg and ignore OSError so the matrix-init's
`infinito create inventory <abs-path> --vars '<huge JSON>'` no longer
hits ENAMETOOLONG inside `is_dir()`. New regression tests pin both.
- utils/cache/data.py: when this module moved to utils/cache/data.py,
PROJECT_ROOT silently slid one level deep, pointing ROLES_DIR at a
non-existent `<repo>/utils/roles`. Implicit lookups returned empty
defaults so `lookup('users','contact')` failed at deploy time as if
the user weren't defined. Use parents[2] and pin the invariant in a
new regression test.
- roles/web-app-keycloak/vars/main.yml: KEYCLOAK_PER_APP_LDAP_ROLES_MAPPER
inherited `subComponents`/`id` from the realm-import dictionary's base
`ldap-roles` mapper. kcadm rejects those keys on `create components`
("Unrecognized field 'subComponents'"), with stderr swallowed by
`2>/dev/null` in upsert_app_mapper.sh. Strip both keys exactly like
update/05_ldap.yml already does for the shared mapper.
- scripts/tests/deploy/local/deploy/fresh-purged-app.sh: pass
`RUNTIME=dev` through `--vars`, otherwise the host process running
init falls back to `detect_runtime() == "host"` and the Playwright
E2E gate (`RUNTIME in [dev, act, github]`) never fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
5.1 KiB
Python
119 lines
5.1 KiB
Python
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from cli.core.discovery import discover_commands, resolve_command_module
|
|
|
|
|
|
class TestDiscovery(unittest.TestCase):
|
|
def _touch(self, path: Path, content: str = "") -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
def test_discover_commands_finds_packages_with_main(self):
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cli_dir = Path(td) / "cli"
|
|
cli_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Dispatcher exists but must NOT be treated as a command
|
|
self._touch(cli_dir / "__main__.py", "# dispatcher\n")
|
|
|
|
# commands
|
|
self._touch(cli_dir / "deploy" / "container" / "__main__.py", "# cmd\n")
|
|
self._touch(cli_dir / "build" / "tree" / "__main__.py", "# cmd\n")
|
|
self._touch(cli_dir / "make" / "__main__.py", "# cmd\n")
|
|
|
|
cmds = discover_commands(cli_dir)
|
|
modules = {c.module for c in cmds}
|
|
|
|
self.assertIn("cli.deploy.container", modules)
|
|
self.assertIn("cli.build.tree", modules)
|
|
self.assertIn("cli.make", modules)
|
|
self.assertNotIn("cli", modules) # root must not be a command
|
|
|
|
def test_discover_commands_ignores_pycache(self):
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cli_dir = Path(td) / "cli"
|
|
cli_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self._touch(cli_dir / "__main__.py", "# dispatcher\n")
|
|
pycache = cli_dir / "x" / "__pycache__"
|
|
self._touch(pycache / "__main__.py", "# should be ignored\n")
|
|
|
|
cmds = discover_commands(cli_dir)
|
|
modules = {c.module for c in cmds}
|
|
self.assertNotIn("cli.x.__pycache__", modules)
|
|
|
|
def test_resolve_command_module_longest_prefix_wins(self):
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cli_dir = Path(td) / "cli"
|
|
cli_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Both deploy and deploy/container exist -> prefer deploy/container
|
|
self._touch(cli_dir / "__main__.py", "# dispatcher\n")
|
|
self._touch(cli_dir / "deploy" / "__main__.py", "# cmd\n")
|
|
self._touch(cli_dir / "deploy" / "container" / "__main__.py", "# cmd\n")
|
|
|
|
module, remaining = resolve_command_module(
|
|
cli_dir, ["deploy", "container", "--foo", "bar"]
|
|
)
|
|
self.assertEqual(module, "cli.deploy.container")
|
|
self.assertEqual(remaining, ["--foo", "bar"])
|
|
|
|
def test_resolve_command_module_returns_none_when_not_found(self):
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cli_dir = Path(td) / "cli"
|
|
cli_dir.mkdir(parents=True, exist_ok=True)
|
|
self._touch(cli_dir / "__main__.py", "# dispatcher\n")
|
|
|
|
module, remaining = resolve_command_module(cli_dir, ["nope", "--x"])
|
|
self.assertIsNone(module)
|
|
self.assertEqual(remaining, ["nope", "--x"])
|
|
|
|
def test_resolve_command_module_stops_at_first_flag(self):
|
|
# `infinito create inventory <abs-path> --host localhost ...`: the
|
|
# subcommand is `create inventory`; the absolute positional arg
|
|
# MUST NOT be tried as a sub-package, otherwise pathlib resets
|
|
# the search root and downstream args (huge `--vars` JSON) end
|
|
# up concatenated as a single path that hits ENAMETOOLONG.
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cli_dir = Path(td) / "cli"
|
|
cli_dir.mkdir(parents=True, exist_ok=True)
|
|
self._touch(cli_dir / "__main__.py", "# dispatcher\n")
|
|
self._touch(cli_dir / "create" / "inventory" / "__main__.py", "# cmd\n")
|
|
|
|
argv = [
|
|
"create",
|
|
"inventory",
|
|
"/home/user/inventories/localhost-0",
|
|
"--host",
|
|
"localhost",
|
|
"--vars",
|
|
'{"a": 1, "b": 2}',
|
|
]
|
|
module, remaining = resolve_command_module(cli_dir, argv)
|
|
self.assertEqual(module, "cli.create.inventory")
|
|
self.assertEqual(remaining, argv[2:])
|
|
|
|
def test_resolve_command_module_survives_oserror_on_long_path(self):
|
|
# Defence in depth: even if a freshly-introduced positional arg
|
|
# somehow slips past the flag-stop heuristic and produces a path
|
|
# that stat() refuses (ENAMETOOLONG, ENOENT, EACCES), the
|
|
# dispatcher MUST keep trying shorter prefixes instead of crashing.
|
|
with tempfile.TemporaryDirectory() as td:
|
|
cli_dir = Path(td) / "cli"
|
|
cli_dir.mkdir(parents=True, exist_ok=True)
|
|
self._touch(cli_dir / "__main__.py", "# dispatcher\n")
|
|
self._touch(cli_dir / "build" / "tree" / "__main__.py", "# cmd\n")
|
|
|
|
# 5000-char arg blows past most filesystem name-length limits.
|
|
blob = "x" * 5000
|
|
argv = ["build", "tree", blob]
|
|
module, remaining = resolve_command_module(cli_dir, argv)
|
|
self.assertEqual(module, "cli.build.tree")
|
|
self.assertEqual(remaining, [blob])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|