Files
awoooi/apps/api/tests/test_feature_flags.py
OG T db9e304a14 feat(adr-080): Phase 0 防護欄建立 — AI 自主化飛輪啟動
- 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>
2026-04-15 12:44:53 +08:00

111 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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