""" ApprovalExecutionService 重試邏輯單元測試 ========================================== ADR-076 Task 3: 執行失敗重試機制 測試範圍: - _is_transient_error() 瞬態/永久性錯誤分類 - MAX_RETRY / RETRY_DELAY_SECONDS 常數 - 邊界情境: None、空字串、混合訊息 🔴🔴 遵循「禁止 Mock 測試鐵律」 - _is_transient_error 是純 Python 方法,無 DB/Redis 依賴 - 無需 Mock,直接測試真實邏輯 建立: 2026-04-14 (台北時區) Claude Haiku 4.5 """ import pytest from src.services.approval_execution import ApprovalExecutionService class TestIsTransientError: """測試瞬態/永久性錯誤判斷邏輯""" # ------- 瞬態錯誤(應返回 True)------- def test_connection_refused(self): assert ApprovalExecutionService._is_transient_error("connection refused") is True def test_connection_refused_uppercase(self): """大小寫不敏感""" assert ApprovalExecutionService._is_transient_error("Connection Refused") is True def test_timeout(self): assert ApprovalExecutionService._is_transient_error("request timeout") is True def test_timed_out(self): assert ApprovalExecutionService._is_transient_error("operation timed out") is True def test_io_error(self): assert ApprovalExecutionService._is_transient_error("i/o error reading response") is True def test_io_error_alt(self): assert ApprovalExecutionService._is_transient_error("io error") is True def test_service_unavailable(self): assert ApprovalExecutionService._is_transient_error("service unavailable") is True def test_too_many_requests(self): assert ApprovalExecutionService._is_transient_error("too many requests") is True def test_eof(self): assert ApprovalExecutionService._is_transient_error("unexpected eof") is True def test_dial_tcp(self): assert ApprovalExecutionService._is_transient_error("dial tcp 10.0.0.1:6443: connect") is True def test_connection_reset(self): assert ApprovalExecutionService._is_transient_error("connection reset by peer") is True def test_temporary_failure(self): assert ApprovalExecutionService._is_transient_error("temporary failure in name resolution") is True # ------- 永久性錯誤(應返回 False)------- def test_not_found(self): assert ApprovalExecutionService._is_transient_error("pod not found") is False def test_forbidden(self): assert ApprovalExecutionService._is_transient_error("forbidden: insufficient permissions") is False def test_permission_denied(self): assert ApprovalExecutionService._is_transient_error("permission denied") is False def test_unauthorized(self): assert ApprovalExecutionService._is_transient_error("unauthorized") is False def test_already_exists(self): assert ApprovalExecutionService._is_transient_error("resource already exists") is False def test_invalid(self): assert ApprovalExecutionService._is_transient_error("invalid field selector") is False def test_destructive_blocked(self): assert ApprovalExecutionService._is_transient_error("destructive operation blocked") is False def test_immutable(self): assert ApprovalExecutionService._is_transient_error("field is immutable") is False # ------- 邊界情境 ------- def test_none_returns_false(self): """None → 不重試(無法判斷)""" assert ApprovalExecutionService._is_transient_error(None) is False def test_empty_string_returns_false(self): """空字串 → 不重試""" assert ApprovalExecutionService._is_transient_error("") is False def test_permanent_wins_over_transient(self): """混合訊息:永久性錯誤關鍵字優先,不重試""" # "not found" (永久) + "timeout" (瞬態) → 不重試 assert ApprovalExecutionService._is_transient_error("timeout: pod not found") is False def test_unknown_error_not_retried(self): """未知錯誤不重試""" assert ApprovalExecutionService._is_transient_error("kubectl exited with code 1") is False class TestRetryConstants: """測試重試常數設定""" def test_max_retry(self): """最多重試 2 次(共 3 次嘗試)""" assert ApprovalExecutionService.MAX_RETRY == 2 def test_retry_delay(self): """重試間隔 30 秒""" assert ApprovalExecutionService.RETRY_DELAY_SECONDS == 30 def test_transient_keywords_not_empty(self): """瞬態錯誤關鍵字列表不為空""" assert len(ApprovalExecutionService._TRANSIENT_ERROR_KEYWORDS) > 0 def test_permanent_keywords_not_empty(self): """永久性錯誤關鍵字列表不為空""" assert len(ApprovalExecutionService._PERMANENT_ERROR_KEYWORDS) > 0 def test_no_overlap_in_keywords(self): """瞬態/永久性關鍵字不重疊(避免邏輯衝突)""" transient = set(ApprovalExecutionService._TRANSIENT_ERROR_KEYWORDS) permanent = set(ApprovalExecutionService._PERMANENT_ERROR_KEYWORDS) assert transient.isdisjoint(permanent)