Files
ewoooc/tests/test_hermes_ollama_cascade.py
OoO b73dc6df3f
All checks were successful
CD Pipeline / deploy (push) Successful in 1m5s
V10.415 protect Hermes fallback routing
2026-05-24 14:25:22 +08:00

167 lines
5.5 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Hermes 分析師必須透過 OllamaService 三主機級聯。"""
import time
import inspect
from types import SimpleNamespace
import pytest
import services.ai_call_logger as logger_mod
import services.hermes_analyst_service as hermes_mod
from services.ai_call_logger import _reset_kill_switch
@pytest.fixture(autouse=True)
def reset_ai_logger(monkeypatch):
_reset_kill_switch()
captured = []
def fake_write(state):
captured.append({
'caller': state.caller,
'provider': state.provider,
'model': state.model,
'status': state.status,
'fallback_to': state.fallback_to,
'error': state.error,
'meta': dict(state.meta),
})
monkeypatch.setattr(logger_mod, '_write_to_db', fake_write)
monkeypatch.setenv('AI_CALL_LOGGING_ENABLED', 'true')
yield captured
def _wait_for(captured, n=1, timeout=2.0):
deadline = time.time() + timeout
while time.time() < deadline:
if len(captured) >= n:
return True
time.sleep(0.01)
return False
def _stub_ollama(monkeypatch, *, content: str, host: str):
fake_resp = SimpleNamespace(
success=True,
content=content,
model=hermes_mod.HERMES_MODEL,
error=None,
total_duration=1.2,
host=host,
input_tokens=33,
output_tokens=22,
)
class FakeOllamaService:
instances = []
def __init__(self, *args, **kwargs):
self.init_args = args
self.init_kwargs = kwargs
self.generate_calls = []
FakeOllamaService.instances.append(self)
def generate(self, **kwargs):
self.generate_calls.append(kwargs)
return fake_resp
monkeypatch.setattr(hermes_mod, 'OllamaService', FakeOllamaService)
return FakeOllamaService
def test_hermes_intent_uses_ollama_service_and_logs_actual_host(monkeypatch, reset_ai_logger):
fake_service = _stub_ollama(
monkeypatch,
content='{"intent":"query_sales","confidence":0.9,"complexity_score":0.8,'
'"requires_data_fetch":true,"preliminary_answer":""}',
host='http://34.21.145.224:11434',
)
svc = hermes_mod.HermesAnalystService()
result = svc._call_hermes_intent("本週業績如何?")
assert result['intent'] == 'query_sales'
assert result['metadata']['source'] == 'hermes_llm'
call_kwargs = fake_service.instances[0].generate_calls[0]
assert call_kwargs['model'] == hermes_mod.HERMES_MODEL
assert call_kwargs['keep_alive'] == hermes_mod.HERMES_KEEP_ALIVE
assert call_kwargs['allow_111_fallback'] is False
assert _wait_for(reset_ai_logger, 1)
rec = reset_ai_logger[0]
assert rec['caller'] == 'hermes_intent'
assert rec['provider'] == 'ollama_secondary'
assert rec['meta']['host_label'] == 'GCP-SSD-2'
def test_hermes_batch_analyze_uses_ollama_service_and_logs_secondary(monkeypatch, reset_ai_logger):
fake_service = _stub_ollama(
monkeypatch,
content='[{"sku":"A1","name":"測試商品","category":"家電","momo_price":120,'
'"pchome_price":100,"gap_pct":20,"sales_7d_delta_pct":-30,'
'"risk":"HIGH","recommended_action":"建議人工評估","confidence":0.8}]',
host='http://34.21.145.224:11434',
)
monkeypatch.setattr(hermes_mod, 'build_mcp_context', lambda *args, **kwargs: 'MCP context')
candidates = [{
'sku': 'A1',
'name': '測試商品',
'category': '家電',
'momo_price': 120,
'pchome_price': 100,
'sales_7d_prev': 1000,
'sales_7d_curr': 700,
'competitor_tags': [
'identity_v2',
'match_type_same_product_different_pack',
'price_basis_unit_price',
'alert_tier_unit_price_review',
],
'competitor_match_score': 0.74,
'competitor_product_id': 'PCH-UNIT',
'competitor_product_name': '測試商品 2入組',
}]
svc = hermes_mod.HermesAnalystService()
raw_threats, items = svc._batch_analyze(candidates)
assert raw_threats[0]['sku'] == 'A1'
assert items[0]['gap_pct'] == 20.0
assert items[0]['match_type'] == 'same_product_different_pack'
assert items[0]['price_basis'] == 'unit_price'
assert items[0]['alert_tier'] == 'unit_price_review'
call_kwargs = fake_service.instances[0].generate_calls[0]
assert call_kwargs['system_prompt'] == svc.SYSTEM_PROMPT
assert call_kwargs['keep_alive'] == hermes_mod.HERMES_KEEP_ALIVE
assert call_kwargs['allow_111_fallback'] is False
assert _wait_for(reset_ai_logger, 1)
rec = reset_ai_logger[0]
assert rec['caller'] == 'hermes_analyst'
assert rec['provider'] == 'ollama_secondary'
assert rec['meta']['host_label'] == 'GCP-SSD-2'
def test_hermes_candidate_sql_only_joins_direct_price_alert_matches():
sql_text = inspect.getsource(hermes_mod.HermesAnalystService.fetch_candidates)
compact_sql = "".join(sql_text.split())
assert '"match_type":"exact"' in compact_sql
assert '"price_basis":"total_price"' in compact_sql
assert '"alert_tier":"price_alert_exact"' in compact_sql
assert "match_type_exact" in sql_text
assert "price_basis_total_price" in sql_text
assert "alert_tier_price_alert_exact" in sql_text
def test_hermes_keep_alive_defaults_to_short_runner_residency():
assert hermes_mod.HERMES_KEEP_ALIVE == "5m"
def test_hermes_disables_111_fallback_by_default():
assert hermes_mod.HERMES_ALLOW_111_FALLBACK is False