Files
ewoooc/tests/test_openclaw_daily_template.py
OoO 838267c293 feat(p1+p3): logger 接 13 caller + Q&A/Nemotron/日報 feature flag 灰度
Phase 1 A4 — 13 個呼叫點接 ai_call_logger(覆蓋率 11.8% → 預估 50%+)
- TOP-1 nemoton_dispatcher: nemotron_dispatch caller (NIM 配額追蹤)
- TOP-2 openclaw_strategist: 4 reports (daily/weekly/monthly/meta) + qa caller
- TOP-3 hermes_analyst: hermes_analyst + hermes_intent (順修 commit 00591c5 殘留 bug)
- TOP-4 code_review_pipeline: code_review_hermes/openclaw/elephant 三鏈 (request_id 串)
- TOP-5 openclaw_bot_routes: openclaw_bot_main/gemini/nim 三層 fallback

Phase 3 A7 — OpenClaw Q&A → qwen3:14b(feature flag OFF)
- OPENCLAW_QA_OLLAMA_FIRST 灰度開關
- 繁中強制 system prompt + Gemini fallback chain
- _is_low_quality_response 品質守門(簡體字檢測 + 拒答訊號 + 結構分數)
- 黃金集 A/B 對照測試框架(10 樣本去 PII)

Phase 3 A8 — OpenClaw 日報 → Hermes 模板(feature flag OFF)
- OPENCLAW_DAILY_HERMES_TEMPLATE 灰度開關
- _compute_daily_kpi 純 SQL + Hermes 規則引擎
- _compute_gemini_insight 精簡 200 字洞察 prompt
- templates/daily_report_v2.j2 + _SafeUndefined 缺欄位優雅降級
- scripts/compare_daily_report_versions.py 雙版本盲測

Phase 3 A9 — Nemotron NIM → qwen3:14b(feature flag OFF)
- NEMOTRON_OLLAMA_FIRST 灰度開關(A2 紅燈:deepseek-r1 假支援,改 qwen3)
- _call_qwen3_dispatch + 既有 NIM tool_calls 解析共用
- 保留 ADR-004「🟡 [降級模式]」Hermes 規則引擎兜底

H6 PII fix — chat_id 進 ai_calls.meta 改 SHA1[:8](4 處 Bot Q&A)

Code Review pipeline — N3 動態 provider tag(gcp/secondary/111)+ A4 logger 三鏈

37 unit tests 全綠(routing 15 + golden 5 + qwen3 8 + daily template 8 + nemotron 1)

Operation Ollama-First v5.0 / Phase 1 A4 + Phase 3 A7+A8+A9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:05:38 +08:00

213 lines
8.9 KiB
Python
Raw 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.
"""
tests/test_openclaw_daily_template.py
─────────────────────────────────────────────────────────────────
Operation Ollama-First v5.0 / Phase 3 / A8 — 日報模板路由測試
驗證面:
T1. flag=false預設→ 走 _legacy_full_gemini_daily_reportregression
T2. flag=true → 走 _generate_daily_report_hermes_template
T3. _compute_daily_kpi 各 KPI 函數可獨立 mock 測DB 失敗回安全預設)
T4. _render_daily_template_v2 缺欄位優雅降級_SafeUndefined 不 raise
T5. _SafeUndefined 對 'X.Y.Z' 巢狀存取不爆
紀律:
- 不打真實 DB / Gemini API
- 不寫 ai_insights
- 不發 Telegram
"""
import os
from datetime import date, datetime
from unittest.mock import patch, MagicMock
import pytest
# ═══════════════════════════════════════════════════════════════════════════
# Fixtures
# ═══════════════════════════════════════════════════════════════════════════
@pytest.fixture(autouse=True)
def _reset_flag(monkeypatch):
"""每個 test 前清環境變數,避免互相污染"""
monkeypatch.delenv('OPENCLAW_DAILY_HERMES_TEMPLATE', raising=False)
yield
# ═══════════════════════════════════════════════════════════════════════════
# T1+T2 — Routing
# ═══════════════════════════════════════════════════════════════════════════
class TestRouting:
def test_flag_false_routes_to_legacy(self, monkeypatch):
"""flag=false → _legacy_full_gemini_daily_report 被呼叫"""
monkeypatch.setenv('OPENCLAW_DAILY_HERMES_TEMPLATE', 'false')
import importlib
import services.openclaw_strategist_service as svc
importlib.reload(svc)
legacy_called = {'v': False}
hermes_called = {'v': False}
def mock_legacy():
legacy_called['v'] = True
return {'status': 'ok', 'mode': 'legacy'}
def mock_hermes():
hermes_called['v'] = True
return {'status': 'ok', 'mode': 'hermes_template'}
monkeypatch.setattr(svc, '_legacy_full_gemini_daily_report', mock_legacy)
monkeypatch.setattr(svc, '_generate_daily_report_hermes_template', mock_hermes)
svc.generate_daily_report()
assert legacy_called['v'] is True, "flag=false 必須走 legacy 路徑"
assert hermes_called['v'] is False, "flag=false 不可走 hermes 模板"
def test_flag_true_routes_to_hermes_template(self, monkeypatch):
"""flag=true → _generate_daily_report_hermes_template 被呼叫"""
monkeypatch.setenv('OPENCLAW_DAILY_HERMES_TEMPLATE', 'true')
import importlib
import services.openclaw_strategist_service as svc
importlib.reload(svc)
legacy_called = {'v': False}
hermes_called = {'v': False}
monkeypatch.setattr(svc, '_legacy_full_gemini_daily_report',
lambda: legacy_called.update(v=True) or {'status': 'ok'})
monkeypatch.setattr(svc, '_generate_daily_report_hermes_template',
lambda: hermes_called.update(v=True) or {'status': 'ok'})
svc.generate_daily_report()
assert hermes_called['v'] is True, "flag=true 必須走 hermes 模板路徑"
assert legacy_called['v'] is False, "flag=true 不可走 legacy"
def test_flag_default_is_false(self, monkeypatch):
"""無 env 設定時 → 預設 false戰前行為"""
# 不 set env
import importlib
import services.openclaw_strategist_service as svc
importlib.reload(svc)
assert svc._daily_hermes_template_enabled() is False
# ═══════════════════════════════════════════════════════════════════════════
# T3 — KPI 計算DB 失敗安全降級)
# ═══════════════════════════════════════════════════════════════════════════
class TestKPIComputation:
def test_compute_daily_kpi_invalid_date_raises(self):
import services.openclaw_strategist_service as svc
with pytest.raises(TypeError):
svc._compute_daily_kpi("not-a-date")
def test_revenue_kpi_returns_safe_default_on_db_error(self, monkeypatch):
"""DB 異常時 _query_revenue_kpi 回零(不拋 exception"""
import services.openclaw_strategist_service as svc
class _BrokenSession:
def execute(self, *a, **kw):
raise RuntimeError('DB connection lost')
def close(self):
pass
monkeypatch.setattr(svc, 'get_session', lambda: _BrokenSession())
result = svc._query_revenue_kpi(date(2026, 5, 3))
assert result['today'] == 0.0
assert result['dod_pct'] == 0.0
assert result['wow_pct'] == 0.0
# ═══════════════════════════════════════════════════════════════════════════
# T4+T5 — Template 渲染與缺欄位優雅降級
# ═══════════════════════════════════════════════════════════════════════════
class TestTemplateRendering:
def test_render_with_full_context_succeeds(self):
import services.openclaw_strategist_service as svc
context = {
'today': '2026年05月02日',
'weekday': '週五',
'revenue': {
'today': 1234567.0,
'yesterday': 1100000.0,
'avg_7d': 1050000.0,
'dod_pct': 12.2,
'wow_pct': 17.6,
},
'orders': {
'today_rows': 234,
'today_sku': 187,
'avg_value_today': 5276.0,
},
'top_skus': [
{'name': 'SKU-A', 'qty': 50, 'revenue': 100000},
{'name': 'SKU-B', 'qty': 32, 'revenue': 80000},
],
'price_gaps': [
{'sku_name': '商品X', 'momo_price': 1200, 'comp_price': 980,
'gap_pct': 22.4, 'competitor': 'PChome'},
],
'inventory_alerts': [],
'priority_actions': ['對 SKU-A 啟動 EA 流程', '觀察 PChome 補貼'],
'gemini_insight': '今日營收強勁成長,建議加碼家電促銷檔期。',
}
rendered = svc._render_daily_template_v2(context)
assert '2026年05月02日' in rendered
assert '週五' in rendered
assert 'NT$1,234,567' in rendered
assert 'SKU-A' in rendered
assert '商品X' in rendered
assert 'PChome' in rendered
assert '今日營收強勁成長' in rendered
def test_render_with_missing_fields_does_not_raise(self):
"""_SafeUndefined 對缺欄位回 — 不拋 UndefinedError"""
import services.openclaw_strategist_service as svc
context = {
'today': '2026年05月02日',
'weekday': '週五',
'revenue': {'today': 0.0, 'dod_pct': 0.0, 'wow_pct': 0.0},
'orders': {}, # 空 dict
'top_skus': [],
'price_gaps': [],
'inventory_alerts': [],
'priority_actions': [],
'gemini_insight': '',
}
# 不 raise 即過
rendered = svc._render_daily_template_v2(context)
assert isinstance(rendered, str)
assert len(rendered) > 0
# 缺欄位該降級為 — 或預設值
assert '今日無熱銷資料' in rendered or '' in rendered
def test_safe_undefined_nested_access(self):
"""_SafeUndefined 對 'X.Y.Z' 巢狀存取不爆"""
import services.openclaw_strategist_service as svc
# 完全無 'revenue' 也不該 raise
context = {
'today': '2026年05月02日',
'weekday': '週五',
# 故意省略 revenue / orders / top_skus 等
}
rendered = svc._render_daily_template_v2(context)
assert isinstance(rendered, str)
assert '2026年05月02日' in rendered