Files
awoooi/apps/api/tests/test_kb_rot_cleaner_schedule.py
Your Name 025a493f06
Some checks failed
run-migration / migrate (push) Failing after 12s
CD Pipeline / build-and-deploy (push) Has been cancelled
feat(p3.2+adr-100): Model Version Tracker + SLO 自治 + KB rot cleaner
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>
2026-04-27 14:54:19 +08:00

121 lines
4.8 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.
"""
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"])