test_action_dispatch.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. from types import SimpleNamespace
  2. import pytest
  3. from hermes_mcp.decision_engine import DecisionSnapshot
  4. from hermes_mcp.server import _build_trader_control_payload, _maybe_dispatch_trader_action
  5. def test_build_trader_control_payload_maps_replace_to_switch():
  6. concern = {"id": "c1", "account_id": "acct-1", "market_symbol": "xrpusd"}
  7. decision = DecisionSnapshot(
  8. action="replace_with_exposure_protector",
  9. target_strategy="protect-1",
  10. reason_summary="inventory repair should start",
  11. confidence=0.81,
  12. requires_action=True,
  13. payload={"current_primary_strategy": "grid-1"},
  14. )
  15. payload = _build_trader_control_payload(decision_id="d1", concern=concern, decision=decision)
  16. assert payload == {
  17. "decision_id": "d1",
  18. "concern_id": "c1",
  19. "account_id": "acct-1",
  20. "market_symbol": "xrpusd",
  21. "action": "switch",
  22. "target_strategy_id": "protect-1",
  23. "expected_active_strategy_id": "grid-1",
  24. "reason": "inventory repair should start",
  25. "confidence": 0.81,
  26. "dry_run": False,
  27. "override": False,
  28. "source": "hermes-mcp",
  29. "source_action": "replace_with_exposure_protector",
  30. }
  31. @pytest.mark.anyio
  32. async def test_dispatch_is_blocked_when_hermes_allow_actions_is_false():
  33. concern = {"id": "c1", "account_id": "acct-1", "market_symbol": "xrpusd"}
  34. decision = DecisionSnapshot(
  35. action="replace_with_grid",
  36. target_strategy="grid-1",
  37. reason_summary="range conditions support grid again",
  38. confidence=0.77,
  39. requires_action=True,
  40. payload={"current_primary_strategy": "protect-1"},
  41. )
  42. cfg = SimpleNamespace(hermes_allow_actions=False, trader_url="http://trader.test/mcp/sse")
  43. result = await _maybe_dispatch_trader_action(cfg=cfg, decision_id="d2", concern=concern, decision=decision)
  44. assert result["dispatch"] == "blocked"
  45. assert result["reason"] == "HERMES_ALLOW_ACTIONS is false"
  46. assert result["payload"]["action"] == "switch"
  47. assert result["payload"]["target_strategy_id"] == "grid-1"
  48. @pytest.mark.anyio
  49. async def test_dispatch_calls_trader_when_gate_is_open(monkeypatch):
  50. concern = {"id": "c1", "account_id": "acct-1", "market_symbol": "xrpusd"}
  51. decision = DecisionSnapshot(
  52. action="replace_with_trend_follower",
  53. target_strategy="trend-1",
  54. reason_summary="persistent breakout pressure favors trend capture",
  55. confidence=0.84,
  56. requires_action=True,
  57. payload={"current_primary_strategy": "grid-1"},
  58. )
  59. cfg = SimpleNamespace(hermes_allow_actions=True, trader_url="http://trader.test/mcp/sse")
  60. seen = {}
  61. async def fake_apply(base_url: str, payload: dict):
  62. seen["base_url"] = base_url
  63. seen["payload"] = payload
  64. return {"ok": True, "status": "applied", "decision_id": payload["decision_id"]}
  65. monkeypatch.setattr("hermes_mcp.server.trader_apply_control_decision", fake_apply)
  66. result = await _maybe_dispatch_trader_action(cfg=cfg, decision_id="d3", concern=concern, decision=decision)
  67. assert result["dispatch"] == "sent"
  68. assert result["result"]["ok"] is True
  69. assert seen["base_url"] == "http://trader.test/mcp/sse"
  70. assert seen["payload"]["action"] == "switch"
  71. assert seen["payload"]["expected_active_strategy_id"] == "grid-1"