- docs/superpowers/specs/2026-04-15-MASTER-ai-autonomous-flywheel-v2.md (1456 行,§0-§8 全填完:42-cell 戰術矩陣、7 Phase 計畫、7 ADR 摘要、 15 KPI、21 Feature Flags、10 風險場景) - docs/adr/ADR-080-ai-autonomy-flywheel-overview.md (7 Phase 結構 + 4 北極星 + 7 架構師 Review Gates + Phase 退出條件) - apps/api/src/core/feature_flags.py (AIOpsFeatureFlags: P1~P6 總開關全 False + 15 細粒度子開關 is_phase_enabled() / is_sub_flag_enabled() + bool cast 安全) - apps/api/src/jobs/__init__.py + baseline_snapshot.py (Phase 0 基線快照 Job:MCP calls / Playbook confidence / general 比例 / learning loop rate / auto_repair — 寫入 aiops:baseline:latest) - apps/api/tests/test_feature_flags.py (21 tests — 全綠) - docs/HARD_RULES.md → v1.9 (新增 Phase 退出條件鐵律:禁止未過 exit conditions 宣告 Phase 完成) - CLAUDE.md 防失憶閘門 1:強制讀 MASTER §0 Session Resume Protocol Gate 0 Pass — 21/21 tests green Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
"""
|
||
AIOps Feature Flags 測試
|
||
========================
|
||
ADR-080: AI 自主化飛輪 Phase 0 退出條件
|
||
|
||
測試項目:
|
||
- 所有 Phase 總開關預設 False
|
||
- is_sub_flag_enabled() 強制父 Phase 開關
|
||
- is_phase_enabled() 邊界條件
|
||
- bool cast 正確性(非 bool 型態子開關不洩漏 int)
|
||
|
||
2026-04-15 Claude Sonnet 4.6 + ogt: Phase 0 初始建立
|
||
"""
|
||
|
||
import pytest
|
||
|
||
from src.core.feature_flags import AIOpsFeatureFlags
|
||
|
||
|
||
class TestPhaseDefaultsAllFalse:
|
||
"""Phase 0 退出條件:所有 Phase 預設 False"""
|
||
|
||
def test_p1_disabled_by_default(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.AIOPS_P1_ENABLED is False
|
||
|
||
def test_p2_disabled_by_default(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.AIOPS_P2_ENABLED is False
|
||
|
||
def test_p3_disabled_by_default(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.AIOPS_P3_ENABLED is False
|
||
|
||
def test_p4_disabled_by_default(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.AIOPS_P4_ENABLED is False
|
||
|
||
def test_p5_disabled_by_default(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.AIOPS_P5_ENABLED is False
|
||
|
||
def test_p6_disabled_by_default(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.AIOPS_P6_ENABLED is False
|
||
|
||
|
||
class TestSubFlagEnforcement:
|
||
"""is_sub_flag_enabled() 必須強制父 Phase 開關"""
|
||
|
||
def test_sub_flag_blocked_when_parent_disabled(self):
|
||
"""父 Phase 關閉時,子開關必須回 False(即使子開關本身 True)"""
|
||
flags = AIOpsFeatureFlags(
|
||
AIOPS_P1_ENABLED=False,
|
||
AIOPS_P1_PRE_DECISION_INVESTIGATOR=True,
|
||
)
|
||
assert flags.is_sub_flag_enabled("AIOPS_P1_PRE_DECISION_INVESTIGATOR") is False
|
||
|
||
def test_sub_flag_allowed_when_parent_enabled(self):
|
||
"""父 Phase 開啟且子開關為 True → 回 True"""
|
||
flags = AIOpsFeatureFlags(
|
||
AIOPS_P1_ENABLED=True,
|
||
AIOPS_P1_PRE_DECISION_INVESTIGATOR=True,
|
||
)
|
||
assert flags.is_sub_flag_enabled("AIOPS_P1_PRE_DECISION_INVESTIGATOR") is True
|
||
|
||
def test_sub_flag_false_when_sub_disabled(self):
|
||
"""父 Phase 開啟但子開關為 False → 回 False"""
|
||
flags = AIOpsFeatureFlags(
|
||
AIOPS_P1_ENABLED=True,
|
||
AIOPS_P1_PRE_DECISION_INVESTIGATOR=False,
|
||
)
|
||
assert flags.is_sub_flag_enabled("AIOPS_P1_PRE_DECISION_INVESTIGATOR") is False
|
||
|
||
def test_sub_flag_returns_bool_not_int(self):
|
||
"""AIOPS_P2_AGENT_TIMEOUT_SEC 是 int — 不得從 is_sub_flag_enabled 洩漏為 truthy int"""
|
||
flags = AIOpsFeatureFlags(AIOPS_P2_ENABLED=True)
|
||
result = flags.is_sub_flag_enabled("AIOPS_P2_AGENT_TIMEOUT_SEC")
|
||
assert isinstance(result, bool), f"Expected bool, got {type(result)}"
|
||
|
||
def test_sub_flag_invalid_name_returns_false(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.is_sub_flag_enabled("INVALID_FLAG_NAME") is False
|
||
|
||
def test_sub_flag_nonexistent_field_returns_false(self):
|
||
flags = AIOpsFeatureFlags(AIOPS_P1_ENABLED=True)
|
||
assert flags.is_sub_flag_enabled("AIOPS_P1_NONEXISTENT") is False
|
||
|
||
|
||
class TestIsPhaseEnabled:
|
||
"""is_phase_enabled() 邊界條件"""
|
||
|
||
def test_valid_phase_enabled(self):
|
||
flags = AIOpsFeatureFlags(AIOPS_P3_ENABLED=True)
|
||
assert flags.is_phase_enabled(3) is True
|
||
|
||
def test_valid_phase_disabled(self):
|
||
flags = AIOpsFeatureFlags(AIOPS_P3_ENABLED=False)
|
||
assert flags.is_phase_enabled(3) is False
|
||
|
||
def test_invalid_phase_returns_false(self):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.is_phase_enabled(0) is False
|
||
assert flags.is_phase_enabled(7) is False
|
||
assert flags.is_phase_enabled(99) is False
|
||
|
||
@pytest.mark.parametrize("phase", [1, 2, 3, 4, 5, 6])
|
||
def test_all_phases_default_false(self, phase):
|
||
flags = AIOpsFeatureFlags()
|
||
assert flags.is_phase_enabled(phase) is False
|