"""Tests for cleanup_legacy_builtin_containers in legacy_cleanup.py.""" import sys import unittest from pathlib import Path from unittest.mock import MagicMock, patch sys.path.insert(0, str(Path(__file__).parent.parent / 'api')) from legacy_cleanup import cleanup_legacy_builtin_containers def _make_cm(already_cleaned=False): cm = MagicMock() cm.configs = {'_meta': {'legacy_builtins_cleaned': already_cleaned}} if already_cleaned else {} return cm class TestCleanupLegacyBuiltinContainers(unittest.TestCase): def test_sentinel_true_skips_all_docker_calls(self): cm = _make_cm(already_cleaned=True) with patch('legacy_cleanup.subprocess.run') as mock_run: cleanup_legacy_builtin_containers(cm) mock_run.assert_not_called() def test_container_not_found_skipped(self): """docker inspect returns non-zero -> container absent -> no stop/rm.""" cm = _make_cm() inspect_result = MagicMock(returncode=1, stdout='', stderr='') with patch('legacy_cleanup.subprocess.run', return_value=inspect_result) as mock_run: cleanup_legacy_builtin_containers(cm) # Only inspect calls, no stop/rm for c in mock_run.call_args_list: self.assertNotIn('stop', c.args[0]) self.assertNotIn('rm', c.args[0]) def test_container_from_per_service_project_not_removed(self): """Project label 'pic-email' -> skip (per-service install).""" cm = _make_cm() inspect_result = MagicMock(returncode=0, stdout='pic-email\n') with patch('legacy_cleanup.subprocess.run', return_value=inspect_result) as mock_run: cleanup_legacy_builtin_containers(cm) for c in mock_run.call_args_list: self.assertNotIn('stop', c.args[0]) def test_container_from_main_stack_removed(self): """Project label 'pic' -> stop + rm called.""" cm = _make_cm() def side_effect(cmd, **kwargs): r = MagicMock(returncode=0) if 'inspect' in cmd: r.stdout = 'pic\n' else: r.stdout = '' return r with patch('legacy_cleanup.subprocess.run', side_effect=side_effect) as mock_run: cleanup_legacy_builtin_containers(cm) cmds = [c.args[0] for c in mock_run.call_args_list] stop_cmds = [c for c in cmds if 'stop' in c] self.assertGreater(len(stop_cmds), 0) def test_sentinel_set_after_run(self): """_meta.legacy_builtins_cleaned is set to True after cleanup.""" cm = _make_cm() inspect_result = MagicMock(returncode=1, stdout='') with patch('legacy_cleanup.subprocess.run', return_value=inspect_result): cleanup_legacy_builtin_containers(cm) self.assertTrue(cm.configs.get('_meta', {}).get('legacy_builtins_cleaned', False)) cm._save_all_configs.assert_called() def test_exception_in_inspect_does_not_crash(self): """If docker inspect throws, the function continues and sets sentinel.""" cm = _make_cm() with patch('legacy_cleanup.subprocess.run', side_effect=OSError('docker not found')): cleanup_legacy_builtin_containers(cm) # must not raise self.assertTrue(cm.configs.get('_meta', {}).get('legacy_builtins_cleaned', False)) def test_mixed_containers_only_pic_project_removed(self): """Some containers are per-service installs, only 'pic'-labelled ones removed.""" cm = _make_cm() call_count = [0] def side_effect(cmd, **kwargs): r = MagicMock(returncode=0) if 'inspect' in cmd: call_count[0] += 1 # First container is per-service, rest are main stack r.stdout = 'pic-email\n' if call_count[0] == 1 else 'pic\n' else: r.stdout = '' return r with patch('legacy_cleanup.subprocess.run', side_effect=side_effect) as mock_run: cleanup_legacy_builtin_containers(cm) stop_cmds = [c.args[0] for c in mock_run.call_args_list if 'stop' in c.args[0]] # 4 of 5 containers should be stopped (first was per-service) self.assertEqual(len(stop_cmds), 4) if __name__ == '__main__': unittest.main()