Wave 8 P3.2 模型版本追蹤 + ADR-100 SLO 自我治理 + 配套: P3.2 — Model Version Tracking: - model_version_probe.py (268 行) — 探測 Ollama / OpenRouter 等 provider 的 model version - model_version_tracker.py (101 行) — 對齊 PG provider_version_history 表 - migrations/p3_2_provider_version_history.sql + rollback — 25 行 schema - db/models.py +32 行 — ProviderVersionHistory ORM ADR-100 — AI 自主化 SLO: - docs/adr/ADR-100-ai-autonomous-slo.md (167 行) — 飛輪 SLO 設計與閾值 - ops/monitoring/slo-rules.yml (254 行) — Prometheus SLO recording rules + alerts - ops/monitoring/tests/test_slo_rules.yaml (242 行) — promtool unit tests 整合修改: - main.py +72 行 — Lifespan 啟動 model_version_probe + KB rot cleaner schedule - gitea_webhook.py +45 行 — webhook 接收 model 版本變化通知 - ci_auto_repair.py / evidence_snapshot.py / pre_decision_investigator.py — 配合接線 新測試: - test_kb_rot_cleaner_schedule.py (120 行) — 9 tests pass - test_slo_rules.yaml — promtool 驗收 Tests: 9 passed (test_kb_rot_cleaner_schedule) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Multiple Engineers (P3.2 + ADR-100) <noreply@anthropic.com>
121 lines
4.8 KiB
Python
121 lines
4.8 KiB
Python
"""
|
||
P3.1-T3: kb_rot_cleaner 月度排程時機計算測試
|
||
=============================================
|
||
驗證 _run_kb_rot_cleaner_loop 的 next_run 計算邏輯正確:
|
||
- 月初 3 點前 → 當月 1 號 03:00
|
||
- 其他時間 → 下月 1 號 03:00
|
||
- 12 月 → 翌年 1 月 1 號 03:00
|
||
|
||
2026-04-27 P3.1-T3 by Claude
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
import pytest
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# 複製 main.py 中的 next_run 計算邏輯(純函式萃取,方便測試)
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
def _calc_next_run(now: datetime) -> datetime:
|
||
"""
|
||
計算下次 kb_rot_cleaner 執行時間(月初 03:00 台北時間)
|
||
|
||
同 main.py lifespan 中 _run_kb_rot_cleaner_loop 的邏輯。
|
||
"""
|
||
if now.day == 1 and now.hour < 3:
|
||
return now.replace(hour=3, minute=0, second=0, microsecond=0)
|
||
elif now.month == 12:
|
||
return now.replace(
|
||
year=now.year + 1, month=1, day=1,
|
||
hour=3, minute=0, second=0, microsecond=0,
|
||
)
|
||
else:
|
||
return now.replace(
|
||
month=now.month + 1, day=1,
|
||
hour=3, minute=0, second=0, microsecond=0,
|
||
)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Tests
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
class TestKbRotCleanerSchedule:
|
||
"""月度排程時機計算測試"""
|
||
|
||
def test_day1_before_3am_returns_same_day_3am(self):
|
||
"""月初 02:59 → 同日 03:00"""
|
||
now = datetime(2026, 5, 1, 2, 59, 0)
|
||
result = _calc_next_run(now)
|
||
assert result == datetime(2026, 5, 1, 3, 0, 0)
|
||
|
||
def test_day1_exactly_midnight_returns_same_day_3am(self):
|
||
"""月初 00:00 → 同日 03:00"""
|
||
now = datetime(2026, 5, 1, 0, 0, 0)
|
||
result = _calc_next_run(now)
|
||
assert result == datetime(2026, 5, 1, 3, 0, 0)
|
||
|
||
def test_day1_after_3am_returns_next_month(self):
|
||
"""月初 03:01 → 下月 1 號 03:00"""
|
||
now = datetime(2026, 5, 1, 3, 1, 0)
|
||
result = _calc_next_run(now)
|
||
assert result == datetime(2026, 6, 1, 3, 0, 0)
|
||
|
||
def test_mid_month_returns_next_month(self):
|
||
"""月中 → 下月 1 號 03:00"""
|
||
now = datetime(2026, 5, 15, 12, 0, 0)
|
||
result = _calc_next_run(now)
|
||
assert result == datetime(2026, 6, 1, 3, 0, 0)
|
||
|
||
def test_december_rolls_over_to_january(self):
|
||
"""12 月 → 翌年 1 月 1 號 03:00"""
|
||
now = datetime(2026, 12, 15, 12, 0, 0)
|
||
result = _calc_next_run(now)
|
||
assert result == datetime(2027, 1, 1, 3, 0, 0)
|
||
|
||
def test_december_day1_before_3am_stays_december(self):
|
||
"""12 月 1 日 02:00 → 同日 03:00(不跨年)"""
|
||
now = datetime(2026, 12, 1, 2, 0, 0)
|
||
result = _calc_next_run(now)
|
||
assert result == datetime(2026, 12, 1, 3, 0, 0)
|
||
|
||
def test_sleep_seconds_positive(self):
|
||
"""sleep_sec 必須為正數"""
|
||
now = datetime(2026, 5, 15, 12, 0, 0)
|
||
next_run = _calc_next_run(now)
|
||
sleep_sec = (next_run - now).total_seconds()
|
||
assert sleep_sec > 0
|
||
|
||
def test_next_run_always_at_hour_3(self):
|
||
"""所有情況下 next_run 的 hour 必須是 3"""
|
||
test_cases = [
|
||
datetime(2026, 1, 1, 2, 59, 59),
|
||
datetime(2026, 3, 15, 8, 0, 0),
|
||
datetime(2026, 12, 31, 23, 59, 59),
|
||
datetime(2026, 6, 1, 3, 30, 0),
|
||
]
|
||
for now in test_cases:
|
||
result = _calc_next_run(now)
|
||
assert result.hour == 3, f"Expected hour=3 for now={now}, got {result}"
|
||
assert result.minute == 0
|
||
assert result.second == 0
|
||
assert result.microsecond == 0
|
||
|
||
def test_next_run_always_day1(self):
|
||
"""所有情況下 next_run 的 day 必須是 1"""
|
||
test_cases = [
|
||
datetime(2026, 1, 15, 12, 0, 0),
|
||
datetime(2026, 5, 2, 0, 0, 0),
|
||
datetime(2026, 12, 20, 6, 0, 0),
|
||
]
|
||
for now in test_cases:
|
||
result = _calc_next_run(now)
|
||
assert result.day == 1, f"Expected day=1 for now={now}, got {result}"
|
||
|
||
|
||
if __name__ == "__main__":
|
||
pytest.main([__file__, "-v"])
|