import datetime import importlib.util import sys import types from pathlib import Path def load_auto_cleanup(monkeypatch): grpc_requests = types.ModuleType("grpc_requests") grpc_requests.Client = object grpc = types.ModuleType("grpc") grpc.RpcError = Exception requests = types.ModuleType("requests") requests.post = lambda *args, **kwargs: None requests.exceptions = types.SimpleNamespace(RequestException=Exception) monkeypatch.setitem(sys.modules, "grpc_requests", grpc_requests) monkeypatch.setitem(sys.modules, "grpc", grpc) monkeypatch.setitem(sys.modules, "requests", requests) path = ( Path(__file__).resolve().parents[4] / "actor" / "recycler" / "cluster_recycler" / "auto_cleanup.py" ) spec = importlib.util.spec_from_file_location("auto_cleanup_test_module", path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def old_cluster(cluster_id, name, deploy_type): create_time = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta( hours=24 ) return { "cluster_id": cluster_id, "cluster_name": name, "deploy_type": deploy_type, "create_time": create_time.isoformat(), } def test_parse_deploy_types_accepts_names_numbers_and_proto_names(monkeypatch): auto_cleanup = load_auto_cleanup(monkeypatch) assert auto_cleanup._parse_deploy_types("dedicated,2,DEPLOY_TYPE_NEXTGEN") == [ auto_cleanup.DEPLOY_TYPE_DEDICATED, auto_cleanup.DEPLOY_TYPE_SERVERLESS, auto_cleanup.DEPLOY_TYPE_NEXTGEN, ] def test_process_cluster_cleanup_uses_only_selected_deploy_type(monkeypatch): auto_cleanup = load_auto_cleanup(monkeypatch) listed_types = [] deleted_requests = [] class FakeClient: def close(self): pass clusters_by_type = { auto_cleanup.DEPLOY_TYPE_DEDICATED: [ old_cluster("dedicated-1", "dedicated", "DEPLOY_TYPE_DEDICATED") ], auto_cleanup.DEPLOY_TYPE_SERVERLESS: [ old_cluster("serverless-1", "serverless", "DEPLOY_TYPE_SERVERLESS") ], auto_cleanup.DEPLOY_TYPE_NEXTGEN: [ old_cluster("nextgen-1", "nextgen", "DEPLOY_TYPE_NEXTGEN") ], } def list_all_clusters(client, cluster_type): listed_types.append(cluster_type) return clusters_by_type[cluster_type] def grpc_request(client, service_name, method_name, request_data, metadata): assert method_name == auto_cleanup.MN_DELETE_CLUSTER deleted_requests.append(request_data) return {} monkeypatch.setattr(auto_cleanup, "_get_client", lambda addr: FakeClient()) monkeypatch.setattr(auto_cleanup, "_list_all_clusters", list_all_clusters) monkeypatch.setattr(auto_cleanup, "_grpc_request", grpc_request) auto_cleanup.process_cluster_cleanup( dry_run=False, deploy_types=[auto_cleanup.DEPLOY_TYPE_SERVERLESS] ) assert listed_types == [auto_cleanup.DEPLOY_TYPE_SERVERLESS] assert deleted_requests == [ {"cluster_id": "serverless-1", "deploy_type": "DEPLOY_TYPE_SERVERLESS"} ] def test_build_failure_notification_message_contains_cluster_details(monkeypatch): auto_cleanup = load_auto_cleanup(monkeypatch) message = auto_cleanup._build_failure_notification_message( [ "Deletion failed for cluster 123: rpc timeout", "Deletion failed for cluster 456: Forbidden.", ] ) assert "cluster clean failed: 2 deletion(s) failed." in message assert "Failed clusters and reasons:" in message assert "- Deletion failed for cluster 123: rpc timeout" in message assert "- Deletion failed for cluster 456: Forbidden." in message