import sys from contextlib import ExitStack from pathlib import Path from unittest.mock import patch, MagicMock import pytest # Mock tkinter and other GUI/macOS specific imports sys.modules["tkinter"] = MagicMock() sys.modules["tkinter.ttk"] = MagicMock() sys.modules["tkinter.scrolledtext"] = MagicMock() sys.modules["tkinter.messagebox"] = MagicMock() sys.modules["Foundation"] = MagicMock() sys.modules["AppKit"] = MagicMock() sys.modules["PIL"] = MagicMock() sys.modules["PIL.Image"] = MagicMock() sys.modules["PIL.ImageTk"] = MagicMock() from install import KnoeInstaller @pytest.fixture def mock_installer(tmp_path): test_cfg_src = Path(__file__).parent / "fixtures" / "test-knoe.cfg" cfg_path = tmp_path / "knoe.cfg" cfg_path.write_text(test_cfg_src.read_text()) with ExitStack() as stack: stack.enter_context(patch("install.tk.Tk")) stack.enter_context(patch("install.tk.Frame")) stack.enter_context(patch("install.tk.Canvas")) stack.enter_context(patch("install.tk.Label")) stack.enter_context(patch("install.tk.Button")) mock_entry = stack.enter_context(patch("install.tk.Entry")) stack.enter_context(patch("install.tk.StringVar")) stack.enter_context(patch("install.tk.BooleanVar")) stack.enter_context(patch("install.tk.IntVar")) stack.enter_context(patch("install.ttk.Style")) stack.enter_context(patch("install.Path.exists", return_value=True)) stack.enter_context(patch("install.Path.mkdir")) stack.enter_context(patch.object(KnoeInstaller, "_save_env_to_file")) stack.enter_context(patch.object(KnoeInstaller, "_save_knoe_cfg")) stack.enter_context(patch.object(KnoeInstaller, "_save_ansible_knoe_vault")) stack.enter_context(patch.object(KnoeInstaller, "_after_env_saved")) stack.enter_context(patch("threading.Thread")) mock_entry.return_value.get.return_value.strip.return_value = "mock_val" root = MagicMock() root.winfo_screenwidth.return_value = 1920 root.winfo_screenheight.return_value = 1080 app = KnoeInstaller(root, config_path=str(cfg_path)) try: app.bg_canvas.winfo_width.return_value = 1000 except Exception: try: app.bg_canvas.winfo_width = MagicMock(return_value=1000) except Exception: pass # Ensure deterministic gating for the Cluster Nodes screen (multi-node, non-k3d). try: app.cluster_env.get.return_value = "knoe-service-cluster" except Exception: pass app.ansible_topology = { "domain": "knoe.org", "hosts": {"merlin": "10.0.0.10", "myrddin": "10.0.0.11"}, "groups": {"k3s_hosts": ["merlin", "myrddin"]}, } return app def test_init_cluster_next_goes_to_cluster_nodes(mock_installer): mock_installer.show_page("init_cluster") with ( patch.object(mock_installer, "_should_show_cluster_nodes_screen", return_value=True), patch.object(mock_installer, "_cluster_ready_for_navigation", return_value=True), patch.object(mock_installer, "_validate_and_save_cluster_config", return_value=True), patch.object(mock_installer, "_run_service_layer_overlay") as mock_overlay, ): mock_installer.on_next() # Service-layer overlay gating has been removed; placement happens in Cluster Nodes. mock_overlay.assert_not_called() assert mock_installer.pages[mock_installer.page_index][0] == "cluster_nodes" def test_cluster_nodes_next_proceeds_to_common_services(mock_installer): mock_installer.show_page("cluster_nodes") mock_installer._cluster_nodes_current_policy = { "hosts": {}, "services": {}, } with patch.object( mock_installer, "_validate_and_save_cluster_nodes_policy", return_value=True, ): mock_installer.on_next() assert mock_installer.pages[mock_installer.page_index][0] == "common_services" def test_cluster_nodes_prev_goes_back_to_cluster_environment(mock_installer): mock_installer.show_page("cluster_nodes") mock_installer.on_prev() assert mock_installer.pages[mock_installer.page_index][0] == "init_cluster" def test_common_services_prev_goes_back_to_cluster_nodes(mock_installer): mock_installer.show_page("common_services") with patch.object(mock_installer, "_should_show_cluster_nodes_screen", return_value=True): mock_installer.on_prev() assert mock_installer.pages[mock_installer.page_index][0] == "cluster_nodes"