alphax/tests/test_runtime_config.py
2026-05-16 23:54:43 +08:00

366 lines
12 KiB
Python

import json
from fastapi.testclient import TestClient
from app.config import config_loader
from app.config.system_config import bootstrap_admin_config, email_config, notification_config, scheduler_config
from app.db import auth_db
from app.db.paper_trading import get_paper_trading_summary
from app.db.runtime_config_db import delete_config, get_event_sources, set_config, set_event_driven_config, set_event_sources, set_strategy_meta
from app.integrations import feishu_push
from app.services.llm_insights import get_llm_module_enabled, get_llm_params
from app.services.onchain_monitor import get_onchain_params
from app.web import web_server
import docker.scheduler as scheduler
def _reset_config_cache():
config_loader._cache = None
config_loader._cache_mtime = None
config_loader._yaml_cache = None
config_loader._yaml_cache_mtime = None
def test_runtime_meta_and_event_sources_override_rules_without_writing_yaml(tmp_path, monkeypatch):
rules_path = tmp_path / "rules.yaml"
rules_path.write_text(
"""
strategy:
mode: long_only
direction: 多头启动
allow_short: false
screener: {}
confirm: {}
tracker: {}
signal_weights: {}
review: {}
reverse_analysis: {}
event_driven:
enabled: true
sources:
google_news_rss:
enabled: false
meta:
strategy_version: v-yaml
learned_rules: []
""".strip(),
encoding="utf-8",
)
monkeypatch.setattr(config_loader, "RULES_PATH", str(rules_path))
_reset_config_cache()
before = rules_path.read_text(encoding="utf-8")
set_strategy_meta({"strategy_version": "v-db", "iteration_count": 7}, source="test")
set_event_sources({"panewslab": {"enabled": True, "type": "rss", "url": "https://example.com/rss"}}, source="test")
rules = config_loader.load_rules(force_reload=True)
assert rules["meta"]["strategy_version"] == "v-db"
assert rules["event_driven"]["sources"]["panewslab"]["enabled"] is True
assert rules_path.read_text(encoding="utf-8") == before
def test_event_sentiment_monitoring_seed_from_rules_into_system_config(tmp_path, monkeypatch):
rules_path = tmp_path / "rules.yaml"
rules_path.write_text(
"""
strategy: {}
screener: {}
confirm: {}
tracker: {}
signal_weights: {}
review: {}
reverse_analysis: {}
event_driven:
enabled: true
news_time_window_hours: 2
sources:
panewslab:
enabled: true
url: https://example.com/rss
sentiment:
enabled: true
max_bonus: 3
monitoring:
untouched_rate:
threshold_pct: 40
meta: {}
learned_rules: []
""".strip(),
encoding="utf-8",
)
monkeypatch.setattr(config_loader, "RULES_PATH", str(rules_path))
_reset_config_cache()
rules = config_loader.load_rules(force_reload=True)
assert rules["event_driven"]["news_time_window_hours"] == 2
assert rules["event_driven"]["sources"]["panewslab"]["enabled"] is True
assert rules["sentiment"]["max_bonus"] == 3
assert rules["monitoring"]["untouched_rate"]["threshold_pct"] == 40
def test_event_driven_runtime_config_overrides_full_section(tmp_path, monkeypatch):
rules_path = tmp_path / "rules.yaml"
rules_path.write_text(
"""
strategy: {}
screener: {}
confirm: {}
tracker: {}
signal_weights: {}
review: {}
reverse_analysis: {}
event_driven:
enabled: true
news_time_window_hours: 2
sources:
old:
enabled: true
sentiment: {}
meta: {}
learned_rules: []
""".strip(),
encoding="utf-8",
)
monkeypatch.setattr(config_loader, "RULES_PATH", str(rules_path))
_reset_config_cache()
set_event_driven_config({
"enabled": False,
"news_time_window_hours": 8,
"sources": {"runtime": {"enabled": True}},
}, source="test")
rules = config_loader.load_rules(force_reload=True)
assert rules["event_driven"]["enabled"] is False
assert rules["event_driven"]["news_time_window_hours"] == 8
assert rules["event_driven"]["sources"]["runtime"]["enabled"] is True
def test_update_meta_persists_to_db_not_rules_yaml(tmp_path, monkeypatch):
rules_path = tmp_path / "rules.yaml"
rules_path.write_text(
"""
strategy: {}
screener: {}
confirm: {}
tracker: {}
signal_weights: {}
review: {}
reverse_analysis: {}
meta:
strategy_version: v-yaml
learned_rules: []
""".strip(),
encoding="utf-8",
)
monkeypatch.setattr(config_loader, "RULES_PATH", str(rules_path))
_reset_config_cache()
before = rules_path.read_text(encoding="utf-8")
config_loader.update_meta("strategy_version", "v-runtime")
assert config_loader.get_meta()["strategy_version"] == "v-runtime"
assert rules_path.read_text(encoding="utf-8") == before
def test_runtime_config_api_can_manage_system_config():
client = TestClient(web_server.app)
payload = {"config": {"enabled": True, "url": "https://example.com/feed"}, "description": "测试源"}
resp = client.put("/api/runtime-config/system/event_driven.sources", json=payload)
assert resp.status_code == 200
assert resp.json()["config"]["url"] == "https://example.com/feed"
listed = client.get("/api/runtime-config?kind=system")
assert listed.status_code == 200
keys = [x["config_key"] for x in listed.json()["items"]]
assert "event_driven.sources" in keys
def test_runtime_config_api_seeds_all_system_defaults_when_listing():
for key in ["llm", "onchain", "paper_trading", "price_streamer", "notification", "email", "bootstrap_admin", "scheduler"]:
delete_config("system", key)
client = TestClient(web_server.app)
resp = client.get("/api/runtime-config?kind=system")
assert resp.status_code == 200
keys = {x["config_key"] for x in resp.json()["items"]}
for key in ["llm", "onchain", "paper_trading", "price_streamer", "notification", "email", "bootstrap_admin", "scheduler"]:
assert key in keys
def test_llm_system_config_overrides_env_defaults(monkeypatch):
monkeypatch.setenv("ALPHAX_LLM_ENABLED", "0")
set_config("system", "llm", {
"enabled": True,
"base_url": "https://llm.example/v1",
"api_key_env": "TEST_LLM_KEY",
"model": "test-model",
"timeout": 33,
"max_tokens": 444,
"modules": {"sentiment": False, "review": True},
})
params = get_llm_params()
assert params["enabled"] is True
assert params["base_url"] == "https://llm.example/v1"
assert params["model"] == "test-model"
assert params["timeout"] == 33
assert params["max_tokens"] == 444
assert get_llm_module_enabled("sentiment") is False
assert get_llm_module_enabled("review") is True
def test_onchain_system_config_overrides_env(monkeypatch):
monkeypatch.setenv("ALPHAX_ONCHAIN_ENABLED", "0")
monkeypatch.setenv("TEST_ETHERSCAN_KEY", "etherscan-secret")
set_config("system", "onchain", {
"enabled": True,
"chains": ["base", "solana"],
"timeout": 9,
"candidate_min_score": 88,
"dex_min_liquidity_usd": 123456,
"etherscan_api_key_env": "TEST_ETHERSCAN_KEY",
"helius_api_key_env": "TEST_HELIUS_KEY",
})
params = get_onchain_params()
assert params["enabled"] is True
assert params["chains"] == ["base", "solana"]
assert params["timeout"] == 9
assert params["candidate_min_score"] == 88
assert params["dex_min_liquidity_usd"] == 123456
assert params["etherscan_api_key"] == "etherscan-secret"
def test_paper_trading_system_config_controls_account_model(monkeypatch):
monkeypatch.setenv("ALPHAX_PAPER_TRADE_NOTIONAL_USDT", "999")
set_config("system", "paper_trading", {
"enabled": True,
"account_equity_usdt": 30000,
"trade_notional_usdt": 6000,
"trade_leverage": 3,
"fee_rate": 0,
"slippage_pct": 0,
})
summary = get_paper_trading_summary(days=30)
assert summary["account_equity_usdt"] == 30000
assert summary["notional_usdt"] == 6000
assert summary["leverage"] == 3
assert summary["margin_usdt"] == 2000
def test_notification_system_config_controls_feishu_webhook(monkeypatch):
calls = []
monkeypatch.setenv("TEST_FEISHU_WEBHOOK", "https://open.feishu.test/hook")
set_config("system", "notification", {
"enabled": True,
"feishu": {"enabled": True, "webhook_env": "TEST_FEISHU_WEBHOOK", "timeout": 3},
})
class FakeResponse:
status_code = 200
def json(self):
return {"StatusCode": 0}
def fake_post(url, json=None, timeout=None):
calls.append({"url": url, "json": json, "timeout": timeout})
return FakeResponse()
monkeypatch.setattr(feishu_push.requests, "post", fake_post)
ok, result = feishu_push.push_card({"elements": []})
assert ok is True
assert result["StatusCode"] == 0
assert calls[0]["url"] == "https://open.feishu.test/hook"
assert calls[0]["timeout"] == 3
assert notification_config()["feishu"]["webhook_env"] == "TEST_FEISHU_WEBHOOK"
set_config("system", "notification", {"enabled": False, "feishu": {"enabled": True, "webhook_env": "TEST_FEISHU_WEBHOOK"}})
ok, reason = feishu_push.push_card({"elements": []})
assert ok is False
assert "disabled" in reason
def test_email_system_config_uses_env_pointers(monkeypatch):
sent = []
monkeypatch.setenv("SMTP_USER_ENV", "noreply@example.com")
monkeypatch.setenv("SMTP_PASS_ENV", "secret")
monkeypatch.setenv("SMTP_SENDER_ENV", "sender@example.com")
set_config("system", "email", {
"enabled": True,
"smtp": {
"host": "smtp.example.com",
"port": 465,
"username_env": "SMTP_USER_ENV",
"password_env": "SMTP_PASS_ENV",
"sender_env": "SMTP_SENDER_ENV",
"timeout": 7,
},
})
class FakeSMTP:
def __init__(self, host, port, timeout=None):
sent.append({"host": host, "port": port, "timeout": timeout})
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def login(self, username, password):
sent[-1]["login"] = (username, password)
def send_message(self, msg):
sent[-1]["from"] = msg["From"]
sent[-1]["to"] = msg["To"]
monkeypatch.setattr(auth_db.smtplib, "SMTP_SSL", FakeSMTP)
assert auth_db.is_smtp_configured() is True
assert auth_db.send_verification_email("user@example.com", "123456") is True
assert sent[0]["host"] == "smtp.example.com"
assert sent[0]["timeout"] == 7
assert sent[0]["login"] == ("noreply@example.com", "secret")
assert sent[0]["from"] == "sender@example.com"
assert email_config()["smtp"]["username_env"] == "SMTP_USER_ENV"
def test_bootstrap_admin_system_config_uses_env_pointers(monkeypatch):
monkeypatch.setenv("BOOT_EMAIL", "admin-runtime@alphax.local")
monkeypatch.setenv("BOOT_PASSWORD", "AlphaXAdmin123")
set_config("system", "bootstrap_admin", {
"enabled": True,
"email_env": "BOOT_EMAIL",
"password_env": "BOOT_PASSWORD",
})
created = auth_db.ensure_default_admin()
assert created["created"] is True
assert created["email"] == "admin-runtime@alphax.local"
assert auth_db.login_user("admin-runtime@alphax.local", "AlphaXAdmin123")["user"]["is_admin"] is True
assert bootstrap_admin_config()["email_env"] == "BOOT_EMAIL"
def test_scheduler_system_config_controls_dry_run(monkeypatch):
monkeypatch.setattr(scheduler, "DRY_RUN", None)
set_config("system", "scheduler", {
"dry_run": False,
"poll_seconds": 2,
"config_reload_seconds": 6,
"pending_warn_seconds": 9,
})
assert scheduler.scheduler_dry_run() is False
assert scheduler._scheduler_settings()["poll_seconds"] == 2
assert scheduler_config()["dry_run"] is False