test_concern_cleanup.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. from __future__ import annotations
  2. import sqlite3
  3. from uuid import uuid4
  4. from hermes_mcp.store import (
  5. DB_PATH,
  6. delete_concern,
  7. init_db,
  8. prune_older_than_hours,
  9. upsert_decision_profile,
  10. upsert_concern,
  11. upsert_cycle,
  12. upsert_decision,
  13. upsert_strategy_assignment,
  14. upsert_strategy_group,
  15. upsert_narrative,
  16. upsert_observation,
  17. upsert_regime_sample,
  18. upsert_state,
  19. )
  20. def _count(table: str, value: str, column: str = "concern_id") -> int:
  21. with sqlite3.connect(DB_PATH) as conn:
  22. conn.row_factory = sqlite3.Row
  23. row = conn.execute(f"select count(*) as n from {table} where {column} = ?", (value,)).fetchone()
  24. return int(row["n"] if row else 0)
  25. def _total_count(table: str) -> int:
  26. with sqlite3.connect(DB_PATH) as conn:
  27. conn.row_factory = sqlite3.Row
  28. row = conn.execute(f"select count(*) as n from {table}").fetchone()
  29. return int(row["n"] if row else 0)
  30. def test_delete_concern_purges_related_rows():
  31. init_db()
  32. concern_id = f"test:{uuid4().hex}"
  33. cycle_id = f"cycle:{uuid4().hex}"
  34. decision_id = f"decision:{uuid4().hex}"
  35. action_id = f"action:{uuid4().hex}"
  36. upsert_concern(
  37. id=concern_id,
  38. account_id="acct-1",
  39. market_symbol="xrpusd",
  40. base_currency="XRP",
  41. quote_currency="USD",
  42. strategy_id="trend-1",
  43. source="test",
  44. status="active",
  45. notes="cleanup target",
  46. )
  47. upsert_cycle(id=cycle_id, started_at="2026-04-19T00:00:00+00:00", finished_at=None, status="ok", trigger="test")
  48. upsert_observation(id=f"obs:{uuid4().hex}", cycle_id=cycle_id, concern_id=concern_id, source="test", kind="snapshot", payload_json="{}")
  49. upsert_state(
  50. id=f"state:{uuid4().hex}",
  51. cycle_id=cycle_id,
  52. concern_id=concern_id,
  53. market_regime="bull",
  54. volatility_state="normal",
  55. liquidity_state="good",
  56. sentiment_pressure="neutral",
  57. event_risk="low",
  58. execution_quality="good",
  59. confidence=0.9,
  60. payload_json="{}",
  61. )
  62. upsert_narrative(
  63. id=f"narr:{uuid4().hex}",
  64. cycle_id=cycle_id,
  65. concern_id=concern_id,
  66. summary="cleanup target",
  67. key_drivers_json="[]",
  68. risk_flags_json="[]",
  69. uncertainties_json="[]",
  70. confidence=0.8,
  71. )
  72. upsert_decision(
  73. id=decision_id,
  74. cycle_id=cycle_id,
  75. concern_id=concern_id,
  76. action="replace_with_grid",
  77. target_strategy="grid-1",
  78. target_policy_json="{}",
  79. reason_summary="cleanup target",
  80. confidence=0.7,
  81. requires_action=True,
  82. )
  83. upsert_regime_sample(id=f"regime:{uuid4().hex}", cycle_id=cycle_id, concern_id=concern_id, timeframe="1h", regime_json="{}", captured_at="2026-04-19T00:00:00+00:00")
  84. with sqlite3.connect(DB_PATH) as conn:
  85. conn.execute(
  86. "insert into actions(id, decision_id, target, command, request_json, response_json, status, executed_at) values(?, ?, ?, ?, ?, ?, ?, ?)",
  87. (action_id, decision_id, "trader", "switch", "{}", None, "pending", None),
  88. )
  89. conn.commit()
  90. deleted = delete_concern(concern_id=concern_id)
  91. assert deleted["concerns"] == 1
  92. assert deleted["decisions"] == 1
  93. assert deleted["actions"] == 1
  94. assert deleted["observations"] == 1
  95. assert deleted["states"] == 1
  96. assert deleted["narratives"] == 1
  97. assert deleted["regime_samples"] == 1
  98. assert _count("concerns", concern_id, "id") == 0
  99. for table in ("observations", "states", "narratives", "decisions", "coverage_gaps", "regime_samples"):
  100. assert _count(table, concern_id) == 0
  101. assert _count("actions", decision_id, "decision_id") == 0
  102. def test_prune_older_than_hours_keeps_concerns_and_config_tables():
  103. init_db()
  104. concern_id = f"test:{uuid4().hex}"
  105. cycle_id = f"cycle:{uuid4().hex}"
  106. decision_id = f"decision:{uuid4().hex}"
  107. profile_id = f"profile:{uuid4().hex}"
  108. group_id = f"group:{uuid4().hex}"
  109. assignment_id = f"assignment:{uuid4().hex}"
  110. old_at = "2026-04-24T00:00:00+00:00"
  111. upsert_concern(
  112. id=concern_id,
  113. account_id="acct-1",
  114. market_symbol="xrpusd",
  115. base_currency="XRP",
  116. quote_currency="USD",
  117. strategy_id="trend-1",
  118. source="test",
  119. status="active",
  120. notes="preserve me",
  121. )
  122. upsert_decision_profile(id=profile_id, name="Config profile", config={"keep": True})
  123. upsert_strategy_group(id=group_id, concern_id=concern_id, name="Config group", strategy_family="mixed", decision_profile_id=profile_id)
  124. upsert_strategy_assignment(id=assignment_id, strategy_group_id=group_id, strategy_id="trend-1", strategy_type="trend_follower", role="primary")
  125. upsert_cycle(id=cycle_id, started_at=old_at, finished_at=None, status="ok", trigger="test")
  126. upsert_observation(id=f"obs:{uuid4().hex}", cycle_id=cycle_id, concern_id=concern_id, source="test", kind="snapshot", payload_json="{}", observed_at=old_at)
  127. upsert_state(
  128. id=f"state:{uuid4().hex}",
  129. cycle_id=cycle_id,
  130. concern_id=concern_id,
  131. market_regime="bull",
  132. volatility_state="normal",
  133. liquidity_state="good",
  134. sentiment_pressure="neutral",
  135. event_risk="low",
  136. execution_quality="good",
  137. confidence=0.9,
  138. payload_json="{}",
  139. created_at=old_at,
  140. )
  141. upsert_narrative(
  142. id=f"narr:{uuid4().hex}",
  143. cycle_id=cycle_id,
  144. concern_id=concern_id,
  145. summary="old",
  146. key_drivers_json="[]",
  147. risk_flags_json="[]",
  148. uncertainties_json="[]",
  149. confidence=0.8,
  150. created_at=old_at,
  151. )
  152. upsert_decision(
  153. id=decision_id,
  154. cycle_id=cycle_id,
  155. concern_id=concern_id,
  156. action="replace_with_grid",
  157. target_strategy="grid-1",
  158. target_policy_json="{}",
  159. reason_summary="old",
  160. confidence=0.7,
  161. requires_action=True,
  162. created_at=old_at,
  163. )
  164. upsert_regime_sample(id=f"regime:{uuid4().hex}", cycle_id=cycle_id, concern_id=concern_id, timeframe="1h", regime_json="{}", captured_at=old_at)
  165. with sqlite3.connect(DB_PATH) as conn:
  166. conn.execute(
  167. "insert into actions(id, decision_id, target, command, request_json, response_json, status, executed_at) values(?, ?, ?, ?, ?, ?, ?, ?)",
  168. (f"action:{uuid4().hex}", decision_id, "trader", "switch", "{}", None, "done", old_at),
  169. )
  170. conn.commit()
  171. before_counts = {
  172. "cycles": _total_count("cycles"),
  173. "decisions": _total_count("decisions"),
  174. "actions": _total_count("actions"),
  175. "observations": _total_count("observations"),
  176. "states": _total_count("states"),
  177. "narratives": _total_count("narratives"),
  178. "coverage_gaps": _total_count("coverage_gaps"),
  179. "regime_samples": _total_count("regime_samples"),
  180. }
  181. deleted = prune_older_than_hours(24)
  182. assert deleted["cycles"] == before_counts["cycles"] - _total_count("cycles")
  183. assert deleted["decisions"] == before_counts["decisions"] - _total_count("decisions")
  184. assert deleted["actions"] == before_counts["actions"] - _total_count("actions")
  185. assert deleted["observations"] == before_counts["observations"] - _total_count("observations")
  186. assert deleted["states"] == before_counts["states"] - _total_count("states")
  187. assert deleted["narratives"] == before_counts["narratives"] - _total_count("narratives")
  188. assert deleted["coverage_gaps"] == before_counts["coverage_gaps"] - _total_count("coverage_gaps")
  189. assert deleted["regime_samples"] == before_counts["regime_samples"] - _total_count("regime_samples")
  190. assert "concerns" not in deleted
  191. assert "decision_profiles" not in deleted
  192. assert "strategy_groups" not in deleted
  193. assert "strategy_assignments" not in deleted
  194. assert _count("concerns", concern_id, "id") == 1
  195. assert _count("decision_profiles", profile_id, "id") == 1
  196. assert _count("strategy_groups", group_id, "id") == 1
  197. assert _count("strategy_assignments", assignment_id, "id") == 1
  198. assert _count("decisions", concern_id) == 0
  199. assert _count("observations", concern_id) == 0
  200. assert _count("states", concern_id) == 0
  201. assert _count("narratives", concern_id) == 0
  202. assert _count("regime_samples", concern_id) == 0