From ebb25e5a70d6a5fdf919c72a627028530ea9a8db Mon Sep 17 00:00:00 2001 From: VAIO73 <50487331+Vaso73@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:11:29 +0200 Subject: [PATCH] fix(oci): preserve Monitor stack editor action --- .../tests/test_oci_monitor_modify_contract.py | 62 +++++++++++++++++++ AppImage/components/virtual-machines.tsx | 7 ++- oci/src/proxmenux_oci/cli.py | 4 +- oci/src/proxmenux_oci/management.py | 2 + 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/tests/test_oci_monitor_modify_contract.py diff --git a/.github/scripts/tests/test_oci_monitor_modify_contract.py b/.github/scripts/tests/test_oci_monitor_modify_contract.py new file mode 100644 index 00000000..3fa83b23 --- /dev/null +++ b/.github/scripts/tests/test_oci_monitor_modify_contract.py @@ -0,0 +1,62 @@ +"""Keep the Monitor OCI editor path distinct from coordinated stack recreation.""" +import ast +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace +from unittest import TestCase +from unittest.mock import Mock, patch + + +ROOT = Path(__file__).resolve().parents[3] +MENU = ROOT / 'oci/src/proxmenux_oci/management.py' +CLI = ROOT / 'oci/src/proxmenux_oci/cli.py' +MONITOR = ROOT / 'AppImage/components/virtual-machines.tsx' + + +def extracted_stack_manager(scope): + source = MENU.read_text(encoding='utf-8').replace( + ' from .stack_recreation import modify_stack\n', '') + node = next(item for item in ast.parse(source).body + if isinstance(item, ast.FunctionDef) and item.name == '_manage_stack') + exec(compile(ast.fix_missing_locations(ast.Module(body=[node], type_ignores=[])), + str(MENU), 'exec'), scope) + return scope['_manage_stack'] + + +class MonitorModifyContractTests(TestCase): + def test_monitor_sends_modify_for_a_stack_and_recreate_for_a_single_instance(self): + source = MONITOR.read_text(encoding='utf-8') + self.assertIn('action: "update" | "modify" | "recreate" | "recover"', source) + self.assertIn('action: ociInstance.stack ? "modify" : "recreate"', source) + + def test_cli_accepts_the_monitor_modify_action(self): + self.assertIn('choices=("update", "modify", "recreate")', CLI.read_text(encoding='utf-8')) + + def test_modify_action_opens_the_stack_editor_without_running_lifecycle(self): + primary = {'stack': {'members': [{'vmid': 101}]}} + instances = ModuleType('oci_instances') + instances.ROOT = Path('/inert') + instances.read = lambda *_: primary + modify_stack = Mock(return_value=True) + run_lifecycle = Mock() + manager = extracted_stack_manager({ + 'sys': SimpleNamespace(path=[]), + 'Path': Path, + 'modify_stack': modify_stack, + '_run_lifecycle': run_lifecycle, + }) + ui = SimpleNamespace() + with patch.dict(sys.modules, { + 'oci_instances': instances, + 'oci_stack_replay': ModuleType('oci_stack_replay'), + }): + result = manager(Path('/inert'), ui, {'vmid': 101}, action='modify') + + self.assertTrue(result) + modify_stack.assert_called_once_with(Path('/inert'), ui, primary, run_lifecycle) + run_lifecycle.assert_not_called() + + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index 4b48218f..031e88c3 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -954,7 +954,7 @@ export function VirtualMachines() { // A container restored from a backup carries the mark of its installation // and has no record on this host until it is recovered from the menu. const [ociRestored, setOciRestored] = useState(false) - const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "recreate" | "recover" } | null>(null) + const [ociAction, setOciAction] = useState<{ vmid: number; action: "update" | "modify" | "recreate" | "recover" } | null>(null) // Firewall log state — fetched only when the operator opens that tab // so a CT/VM without firewall use doesn't pay the pvesh cost on every @@ -5341,7 +5341,10 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {