"""Tests for flow.commands.bootstrap helpers and schema behavior.""" import os import pytest from flow.commands.bootstrap import ( _ensure_required_variables, _get_profiles, _normalize_profile_package_entry, _resolve_package_manager, _resolve_package_spec, _resolve_pkg_source_name, ) from flow.core.config import AppConfig, FlowContext from flow.core.console import ConsoleLogger from flow.core.platform import PlatformInfo @pytest.fixture def ctx(): return FlowContext( config=AppConfig(), manifest={ "packages": [ { "name": "fd", "type": "pkg", "sources": {"apt": "fd-find", "dnf": "fd-find", "brew": "fd"}, }, { "name": "neovim", "type": "binary", "source": "github:neovim/neovim", "version": "0.10.4", "asset-pattern": "nvim-{{os}}-{{arch}}.tar.gz", "platform-map": {"linux-x64": {"os": "linux", "arch": "x64"}}, "install": {"bin": ["bin/nvim"]}, }, ] }, platform=PlatformInfo(os="linux", arch="x64", platform="linux-x64"), console=ConsoleLogger(), ) def test_get_profiles_from_manifest(ctx): ctx.manifest = {"profiles": {"linux": {"os": "linux"}}} assert "linux" in _get_profiles(ctx) def test_get_profiles_rejects_environments(ctx): ctx.manifest = {"environments": {"legacy": {"os": "linux"}}} with pytest.raises(RuntimeError, match="no longer supported"): _get_profiles(ctx) def test_resolve_package_manager_explicit_value(ctx): assert _resolve_package_manager(ctx, {"os": "linux", "package-manager": "dnf"}) == "dnf" def test_resolve_package_manager_linux_auto_apt(monkeypatch, ctx): monkeypatch.setattr("flow.commands.bootstrap.shutil.which", lambda name: "/usr/bin/apt" if name == "apt" else None) assert _resolve_package_manager(ctx, {"os": "linux"}) == "apt" def test_resolve_package_manager_linux_auto_dnf(monkeypatch, ctx): monkeypatch.setattr("flow.commands.bootstrap.shutil.which", lambda name: "/usr/bin/dnf" if name == "dnf" else None) assert _resolve_package_manager(ctx, {"os": "linux"}) == "dnf" def test_resolve_package_manager_requires_os(ctx): with pytest.raises(RuntimeError, match="must be set"): _resolve_package_manager(ctx, {}) def test_normalize_package_entry_string(): assert _normalize_profile_package_entry("git") == {"name": "git"} def test_normalize_package_entry_type_prefix(): assert _normalize_profile_package_entry("cask/wezterm") == {"name": "wezterm", "type": "cask"} def test_normalize_package_entry_object(): out = _normalize_profile_package_entry({"name": "docker", "allow_sudo": True}) assert out["name"] == "docker" assert out["allow_sudo"] is True def test_resolve_package_spec_uses_catalog_type(ctx): catalog = { "fd": { "name": "fd", "type": "pkg", "sources": {"apt": "fd-find"}, } } resolved = _resolve_package_spec(catalog, {"name": "fd"}) assert resolved["type"] == "pkg" assert resolved["sources"]["apt"] == "fd-find" def test_resolve_package_spec_defaults_to_pkg(ctx): resolved = _resolve_package_spec({}, {"name": "git"}) assert resolved["type"] == "pkg" def test_resolve_package_spec_profile_override(ctx): catalog = { "neovim": { "name": "neovim", "type": "binary", "version": "0.10.4", } } resolved = _resolve_package_spec(catalog, {"name": "neovim", "post-install": "echo ok"}) assert resolved["type"] == "binary" assert resolved["post-install"] == "echo ok" def test_resolve_pkg_source_name_with_mapping(ctx): spec = {"name": "fd", "sources": {"apt": "fd-find", "dnf": "fd-find", "brew": "fd"}} assert _resolve_pkg_source_name(spec, "apt") == "fd-find" assert _resolve_pkg_source_name(spec, "dnf") == "fd-find" assert _resolve_pkg_source_name(spec, "brew") == "fd" def test_resolve_pkg_source_name_fallback_to_name(ctx): spec = {"name": "ripgrep", "sources": {"apt": "ripgrep"}} assert _resolve_pkg_source_name(spec, "dnf") == "ripgrep" def test_ensure_required_variables_missing_raises(): with pytest.raises(RuntimeError, match="Missing required environment variables"): _ensure_required_variables({"requires": ["USER_EMAIL", "TARGET_HOSTNAME"]}, {"USER_EMAIL": "a@b"}) def test_ensure_required_variables_accepts_vars(monkeypatch): env = dict(os.environ) env["USER_EMAIL"] = "a@b" env["TARGET_HOSTNAME"] = "devbox" _ensure_required_variables({"requires": ["USER_EMAIL", "TARGET_HOSTNAME"]}, env)