test_action_dispatch.py 3.4 KB

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