dsh-biocore 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,219 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Wiring tests: validate the memory_dual snippets used/needed in main.py.
3
+
4
+ - sort: executes the ACTUAL snippet currently in main.py (search anchors, then
5
+ exec with fake scored list) and asserts ordering follows memory_dual scores.
6
+ - forget: executes the proposed replacement for _memory_save_doc_for_session
7
+ (main.py:9768-9770) with fake MEMORY_DIR/json/logger and asserts hidden marks.
8
+ - testing effect: executes the proposed on_recall write-back snippet.
9
+ - policy: reward direction, Q update direction, ui_settings persistence key.
10
+ """
11
+ import json
12
+ import tempfile
13
+ import textwrap
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+ from biocore_model import memory_dual as md
18
+ from biocore_model import policy_learning as pl
19
+ from biocore_model import ui_settings
20
+
21
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
22
+ NOW = 1_700_000_000.0
23
+
24
+
25
+ class FakePlugin:
26
+ def __init__(self, data_dir):
27
+ self.data_dir = str(data_dir)
28
+
29
+
30
+ class FakeSelf:
31
+ def __init__(self):
32
+ self._last_memory_context = None
33
+
34
+
35
+ def _extract_sort_snippet():
36
+ """The real insertion in main.py: from the memory_dual import to the fallback."""
37
+ source = MAIN.read_text(encoding="utf-8").splitlines()
38
+ import_line = None
39
+ for index, line in enumerate(source):
40
+ if "from biocore_model import memory_dual as _md" in line:
41
+ import_line = index
42
+ break
43
+ if import_line is None:
44
+ raise AssertionError("memory_dual sorting snippet not found in main.py")
45
+ start = import_line
46
+ for index in range(import_line - 1, max(-1, import_line - 8), -1):
47
+ if source[index].strip().startswith("try:"):
48
+ start = index
49
+ break
50
+ if start is None:
51
+ raise AssertionError("memory_dual sorting snippet not found in main.py")
52
+ end = None
53
+ for index in range(start + 1, len(source)):
54
+ if "scored.sort(key=lambda x: x[0], reverse=True)" in source[index]:
55
+ end = index
56
+ break
57
+ if end is None:
58
+ raise AssertionError("sort fallback not found")
59
+ body = "\n".join(source[start:end + 1])
60
+ return "def _run_snippet(self, scored, now):\n" + textwrap.indent(body, " ")
61
+
62
+
63
+ class DualSortWiringTest(unittest.TestCase):
64
+ def setUp(self):
65
+ self.fresh = {"id": "fresh", "text": "fresh", "storage_s": 0.6,
66
+ "retrieval_s": 0.55, "access_count": 0,
67
+ "last_access_ts": NOW - 2 * 3600, "open_loop": False}
68
+ self.stale = {"id": "stale", "text": "stale", "storage_s": 0.5,
69
+ "retrieval_s": 0.35, "access_count": 0,
70
+ "last_access_ts": NOW - 72 * 3600, "open_loop": False}
71
+
72
+ def test_snippet_reorders_by_dual_score(self):
73
+ snippet = _extract_sort_snippet()
74
+ scored = [(0.99, self.stale), (0.50, self.fresh)] # legacy order: stale first
75
+ namespace = {}
76
+ exec(compile(snippet, "<main.memory_dual.sort>", "exec"), namespace)
77
+ namespace["_run_snippet"](FakeSelf(), scored, NOW)
78
+ ordered = [entry["id"] for _score, entry in scored]
79
+ expected = sorted(
80
+ ["fresh", "stale"],
81
+ key=lambda eid: -md.score(self.fresh if eid == "fresh" else self.stale,
82
+ NOW, context_match=1.0))
83
+ self.assertEqual(ordered, expected)
84
+ self.assertEqual(ordered[0], "fresh")
85
+ self.assertGreater(md.score(self.fresh, NOW), md.score(self.stale, NOW))
86
+
87
+ def test_snippet_can_fall_back(self):
88
+ snippet = _extract_sort_snippet()
89
+ scored = [(0.90, {"id": "a", "retrieval_s": 0.5, "last_access_ts": NOW}),
90
+ (0.10, {"id": "b", "retrieval_s": 0.5, "last_access_ts": NOW})]
91
+ namespace = {}
92
+ exec(compile(snippet, "<main.memory_dual.sort>", "exec"), namespace)
93
+ namespace["_run_snippet"](FakeSelf(), scored, NOW)
94
+ self.assertEqual([e["id"] for _s, e in scored], ["a", "b"])
95
+
96
+
97
+ FORGET_PATCH = '''
98
+ def _memory_save_doc_for_session(self, path, doc):
99
+ try:
100
+ from biocore_model import memory_dual as _md
101
+ _rows = doc.get("entries") if isinstance(doc.get("entries"), list) else []
102
+ _fade = _md.forget(_rows, now=time.time())
103
+ doc["entries"] = _fade["entries"]
104
+ if _fade["forgotten_ids"]:
105
+ logger.info("[biocore] memory_dual hidden=%s", _fade["forgotten_ids"])
106
+ except Exception:
107
+ pass
108
+ MEMORY_DIR.mkdir(parents=True, exist_ok=True)
109
+ path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
110
+ '''
111
+
112
+
113
+ class ForgetWiringTest(unittest.TestCase):
114
+ def test_patch_marks_hidden_without_deleting(self):
115
+ with tempfile.TemporaryDirectory() as tmp:
116
+ tmp_path = Path(tmp)
117
+ class Logger:
118
+ def info(self, *a, **k): pass
119
+ def warning(self, *a, **k): pass
120
+ namespace = {"json": json, "time": __import__("time"),
121
+ "logger": Logger(), "MEMORY_DIR": tmp_path}
122
+ exec(compile(FORGET_PATCH, "<forget_patch>", "exec"), namespace)
123
+ fake = object.__new__(type("Fake", (), {}))
124
+ import time as _time
125
+ now_real = _time.time()
126
+ doc = {"entries": [
127
+ {"id": "faint", "retrieval_s": 0.10, "last_access_ts": now_real - 200 * 3600},
128
+ {"id": "solid", "retrieval_s": 0.50, "last_access_ts": now_real},
129
+ ]}
130
+ target = tmp_path / "session.json"
131
+ namespace["_memory_save_doc_for_session"](fake, target, doc)
132
+ saved = json.loads(target.read_text(encoding="utf-8"))
133
+ by_id = {row["id"]: row for row in saved["entries"]}
134
+ self.assertEqual(len(saved["entries"]), 2) # nothing deleted
135
+ self.assertTrue(by_id["faint"]["hidden"])
136
+ self.assertIn("hidden_ts", by_id["faint"])
137
+ self.assertNotIn("hidden", by_id["solid"])
138
+
139
+ def test_patch_second_call_is_idempotent(self):
140
+ with tempfile.TemporaryDirectory() as tmp:
141
+ tmp_path = Path(tmp)
142
+ class Logger:
143
+ def info(self, *a, **k): pass
144
+ def warning(self, *a, **k): pass
145
+ namespace = {"json": json, "time": __import__("time"),
146
+ "logger": Logger(), "MEMORY_DIR": tmp_path}
147
+ exec(compile(FORGET_PATCH, "<forget_patch>", "exec"), namespace)
148
+ fake = object.__new__(type("Fake", (), {}))
149
+ import time as _time
150
+ doc = {"entries": [{"id": "faint", "retrieval_s": 0.10,
151
+ "last_access_ts": _time.time() - 200 * 3600}]}
152
+ target = tmp_path / "session.json"
153
+ namespace["_memory_save_doc_for_session"](fake, target, doc)
154
+ first = json.loads(target.read_text(encoding="utf-8"))
155
+ namespace["_memory_save_doc_for_session"](fake, target, first)
156
+ second = json.loads(target.read_text(encoding="utf-8"))
157
+ self.assertTrue(second["entries"][0]["hidden"])
158
+ self.assertEqual(len(second["entries"]), 1)
159
+
160
+
161
+ TESTING_EFFECT_SNIPPET = '''
162
+ _upd = _md.on_recall(e, now)
163
+ e["storage_s"] = _upd["storage_s"]
164
+ e["retrieval_s"] = _upd["retrieval_s"]
165
+ e["access_count"] = _upd["access_count"]
166
+ e["last_access_ts"] = _upd["last_access_ts"]
167
+ '''
168
+
169
+
170
+ class TestingEffectWiringTest(unittest.TestCase):
171
+ def test_patch_strengthens_selected_entry(self):
172
+ entry = {"id": "e1", "storage_s": 0.5, "retrieval_s": 0.4,
173
+ "access_count": 0, "last_access_ts": NOW - 24 * 3600}
174
+ namespace = {"e": entry, "now": NOW, "_md": md}
175
+ exec(compile(TESTING_EFFECT_SNIPPET, "<testing_effect>", "exec"), namespace)
176
+ self.assertGreater(entry["storage_s"], 0.5)
177
+ self.assertAlmostEqual(entry["retrieval_s"], 0.75, places=4)
178
+ self.assertEqual(entry["access_count"], 1)
179
+ self.assertEqual(entry["last_access_ts"], NOW)
180
+
181
+ def test_patch_is_idempotent_same_now(self):
182
+ entry = {"id": "e1", "storage_s": 0.5, "retrieval_s": 0.4,
183
+ "access_count": 0, "last_access_ts": 0.0}
184
+ namespace = {"e": entry, "now": NOW, "_md": md}
185
+ exec(compile(TESTING_EFFECT_SNIPPET, "<testing_effect>", "exec"), namespace)
186
+ first = dict(entry)
187
+ exec(compile(TESTING_EFFECT_SNIPPET, "<testing_effect>", "exec"), namespace)
188
+ self.assertEqual(entry, first)
189
+
190
+
191
+ class PolicyWiringTest(unittest.TestCase):
192
+ def test_reward_direction(self):
193
+ responded = pl.reward(True, 5.0, -0.5)
194
+ ignored = pl.reward(False, 900.0, 0.0)
195
+ self.assertGreater(responded, ignored)
196
+ self.assertAlmostEqual(responded, 0.541667, places=6)
197
+ self.assertAlmostEqual(ignored, 0.0, places=6)
198
+
199
+ def test_update_direction(self):
200
+ table = pl.update({}, "expect|secure|day", "approach", 1.0, alpha=0.2)
201
+ self.assertGreater(table["expect|secure|day"]["approach"]["q"], 0.0)
202
+ down = pl.update(table, "expect|secure|day", "approach", 0.0, alpha=0.2)
203
+ self.assertLess(down["expect|secure|day"]["approach"]["q"],
204
+ table["expect|secure|day"]["approach"]["q"])
205
+
206
+ def test_q_table_persists_under_ui_settings(self):
207
+ with tempfile.TemporaryDirectory() as tmp:
208
+ plugin = FakePlugin(tmp)
209
+ table = pl.update({}, "expect|secure|day", "share", 0.5)
210
+ saved = ui_settings.save(plugin, {"autonomy": {"policy_q": table}})
211
+ self.assertTrue(saved.get("ok"))
212
+ data = ui_settings.load(plugin)
213
+ self.assertEqual(data["autonomy"]["policy_q"],
214
+ table)
215
+ self.assertIn("policy_q", data["autonomy"])
216
+
217
+
218
+ if __name__ == "__main__":
219
+ unittest.main()
@@ -0,0 +1,241 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-218: recall feedback into affect (回忆回灌情绪).
3
+
4
+ Run from the plugin root:
5
+
6
+ python -m unittest discover -s biocore_model/tests -p "test_memory_feedback.py"
7
+ """
8
+ import json
9
+ import os
10
+ import tempfile
11
+ import unittest
12
+ from pathlib import Path
13
+
14
+ from biocore_memory.engine import MemoryEngine
15
+ from biocore_memory.core import config as engine_config
16
+
17
+
18
+ def entry(i, text, affect=None, importance=0.5, ts=1700000000.0):
19
+ value = {"id": i, "text": text, "role": "user", "ts": ts,
20
+ "importance": importance, "vector": [], "hits": 1,
21
+ "self_relevance": 0.0, "pin": False, "memory_source": "direct",
22
+ "memory_confidence": 0.7, "previous_versions": [],
23
+ "module": "biocore_memory", "scope": "default"}
24
+ if affect is not None:
25
+ value["affect"] = affect
26
+ return value
27
+
28
+
29
+ class MemoryFeedbackTest(unittest.TestCase):
30
+ def setUp(self):
31
+ self.tmp = tempfile.TemporaryDirectory()
32
+ self.dir = Path(self.tmp.name)
33
+ self.clock = [1700000000.0]
34
+ self._saved = os.environ.get("BIOCORE_MEMORY_FEEDBACK")
35
+ os.environ.pop("BIOCORE_MEMORY_FEEDBACK", None)
36
+
37
+ def tearDown(self):
38
+ self.tmp.cleanup()
39
+ if self._saved is None:
40
+ os.environ.pop("BIOCORE_MEMORY_FEEDBACK", None)
41
+ else:
42
+ os.environ["BIOCORE_MEMORY_FEEDBACK"] = self._saved
43
+
44
+ def write(self, entries):
45
+ (self.dir / "memory.json").write_text(
46
+ json.dumps({"entries": entries, "tombstones": [],
47
+ "schema_version": 1}), encoding="utf-8")
48
+
49
+ def engine(self, affect, enabled=True):
50
+ e = MemoryEngine(data_dir=str(self.dir), affect=affect,
51
+ now=lambda: self.clock[0])
52
+ if enabled:
53
+ e.flags["memory_feedback"] = True
54
+ return e
55
+
56
+ # -- switch ---------------------------------------------------------
57
+ def test_flag_defaults_off(self):
58
+ self.assertIs(engine_config.FLAGS.get("memory_feedback"), False)
59
+
60
+ def test_off_leaves_affect_unchanged(self):
61
+ self.write([entry("warm", "beach warm", {"valence": 0.9, "arousal": 0.9})])
62
+ affect = {"v": 0.0, "a": 0.5}
63
+ e = self.engine(affect, enabled=False)
64
+ rows = e.recall("beach", budget=3)
65
+ self.assertEqual(len(rows), 1)
66
+ self.assertAlmostEqual(affect["v"], 0.0, places=6)
67
+ self.assertAlmostEqual(affect["a"], 0.5, places=6)
68
+ self.assertIsNone(e._last_memory_feedback)
69
+
70
+ # -- basic pull -----------------------------------------------------
71
+ def test_on_pulls_toward_recalled_affect(self):
72
+ self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
73
+ affect = {"v": 0.0, "a": 0.5}
74
+ e = self.engine(affect)
75
+ e.recall("beach", budget=3)
76
+ report = e._last_memory_feedback
77
+ self.assertIsNotNone(report)
78
+ self.assertEqual(report["applied"], ["warm"])
79
+ self.assertAlmostEqual(report["delta"][0], 0.02, places=6)
80
+ self.assertAlmostEqual(report["delta"][1], 0.02, places=6)
81
+ self.assertAlmostEqual(affect["v"], 0.02, places=4)
82
+ self.assertAlmostEqual(affect["a"], 0.52, places=4)
83
+
84
+ def test_per_turn_cap_both_axes(self):
85
+ self.write([entry("max", "beach max", {"valence": 1.0, "arousal": 1.0})])
86
+ affect = {"v": -1.0, "a": 0.0}
87
+ e = self.engine(affect)
88
+ e.recall("beach", budget=3)
89
+ report = e._last_memory_feedback
90
+ self.assertTrue(report["capped"])
91
+ self.assertAlmostEqual(report["delta"][0], 0.02, places=6)
92
+ self.assertAlmostEqual(report["delta"][1], 0.02, places=6)
93
+ self.assertAlmostEqual(affect["v"], -0.98, places=4)
94
+ self.assertAlmostEqual(affect["a"], 0.02, places=4)
95
+
96
+ def test_positive_cap_is_symmetric(self):
97
+ self.write([entry("max", "beach max", {"valence": 1.0, "arousal": 1.0})])
98
+ affect = {"v": -0.2, "a": 0.3}
99
+ e = self.engine(affect)
100
+ e.recall("beach", budget=3)
101
+ self.assertAlmostEqual(affect["v"], -0.18, places=4)
102
+ self.assertAlmostEqual(affect["a"], 0.32, places=4)
103
+
104
+ def test_weighted_mean_of_batch(self):
105
+ self.write([
106
+ entry("hi", "beach one", {"valence": 0.9, "arousal": 0.9},
107
+ importance=1.0),
108
+ entry("lo", "beach two", {"valence": -0.9, "arousal": 0.1},
109
+ importance=0.0),
110
+ ])
111
+ affect = {"v": 0.0, "a": 0.5}
112
+ e = self.engine(affect)
113
+ e.recall("beach", budget=3)
114
+ report = e._last_memory_feedback
115
+ # weight 1.0 vs 0.0 -> target is the weighty memory (0.9, 0.9)
116
+ self.assertAlmostEqual(report["target"][0], 0.9, places=4)
117
+ self.assertAlmostEqual(report["target"][1], 0.9, places=4)
118
+ self.assertAlmostEqual(affect["v"], 0.02, places=4)
119
+
120
+ # -- cooldown -------------------------------------------------------
121
+ def test_cooldown_blocks_same_memory(self):
122
+ self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
123
+ affect = {"v": 0.0, "a": 0.5}
124
+ e = self.engine(affect)
125
+ e.recall("beach", budget=3)
126
+ moved = affect["v"]
127
+ self.assertAlmostEqual(moved, 0.02, places=4)
128
+ e.recall("beach", budget=3)
129
+ report = e._last_memory_feedback
130
+ self.assertEqual(report["applied"], [])
131
+ self.assertEqual(report["delta"], [0.0, 0.0])
132
+ self.assertIn("cooldown", [x["reason"] for x in report["skipped"]])
133
+ self.assertAlmostEqual(affect["v"], moved, places=6)
134
+
135
+ def test_cooldown_expires_after_window(self):
136
+ self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
137
+ affect = {"v": 0.0, "a": 0.5}
138
+ e = self.engine(affect)
139
+ e.recall("beach", budget=3)
140
+ self.clock[0] += engine_config.FEEDBACK_COOLDOWN + 1.0
141
+ e.recall("beach", budget=3)
142
+ report = e._last_memory_feedback
143
+ self.assertEqual(report["applied"], ["warm"])
144
+ self.assertAlmostEqual(affect["v"], 0.04, places=4)
145
+
146
+ # -- negative floor / regulation ------------------------------------
147
+ def test_negative_floor_blocks_descent_and_regulates(self):
148
+ self.write([entry("bad", "beach bad", {"valence": -0.9, "arousal": 0.5})])
149
+ affect = {"v": -0.5, "a": 0.5}
150
+ e = self.engine(affect)
151
+ e.recall("beach", budget=3)
152
+ report = e._last_memory_feedback
153
+ self.assertTrue(report["blocked_negative"])
154
+ self.assertEqual(report["delta"][0], 0.0)
155
+ self.assertAlmostEqual(affect["v"], -0.5, places=6)
156
+ regulation = report.get("regulation")
157
+ self.assertIsInstance(regulation, dict)
158
+ self.assertIn(regulation.get("strategy"),
159
+ ("situation_selection", "situation_modification",
160
+ "attentional_deployment", "cognitive_reappraisal",
161
+ "response_modulation"))
162
+ self.assertEqual(report["applied"], [])
163
+
164
+ def test_negative_above_floor_still_pulls(self):
165
+ self.write([entry("bad", "beach bad", {"valence": -0.9, "arousal": 0.5})])
166
+ affect = {"v": -0.2, "a": 0.5}
167
+ e = self.engine(affect)
168
+ e.recall("beach", budget=3)
169
+ report = e._last_memory_feedback
170
+ self.assertFalse(report["blocked_negative"])
171
+ self.assertIsNone(report["regulation"])
172
+ self.assertAlmostEqual(affect["v"], -0.22, places=4)
173
+
174
+ # -- scope: only the injected top-k ---------------------------------
175
+ def test_only_injected_topk_matters(self):
176
+ self.write([
177
+ entry("hit", "beach picnic", {"valence": 0.9, "arousal": 0.9}),
178
+ entry("miss", "zebra zebra", {"valence": -1.0, "arousal": 0.0}),
179
+ ])
180
+ affect = {"v": 0.0, "a": 0.5}
181
+ e = self.engine(affect)
182
+ rows = e.recall("beach picnic", budget=1)
183
+ self.assertEqual(len(rows), 1)
184
+ self.assertEqual(rows[0]["entry"]["id"], "hit")
185
+ report = e._last_memory_feedback
186
+ self.assertEqual(report["applied"], ["hit"])
187
+ self.assertAlmostEqual(affect["v"], 0.02, places=4)
188
+
189
+ def test_entry_without_affect_is_skipped(self):
190
+ self.write([entry("plain", "beach plain")])
191
+ affect = {"v": 0.0, "a": 0.5}
192
+ e = self.engine(affect)
193
+ e.recall("beach", budget=3)
194
+ report = e._last_memory_feedback
195
+ self.assertEqual(report["applied"], [])
196
+ self.assertEqual(report["reason"], "no_eligible_memory")
197
+ self.assertIn("no_affect", [x["reason"] for x in report["skipped"]])
198
+ self.assertAlmostEqual(affect["v"], 0.0, places=6)
199
+
200
+ # -- state-only, env switch, robustness -----------------------------
201
+ def test_memory_entry_is_not_rewritten(self):
202
+ warm = entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})
203
+ self.write([warm])
204
+ affect = {"v": 0.0, "a": 0.5}
205
+ e = self.engine(affect)
206
+ e.recall("beach", budget=3)
207
+ stored = e.store.find("warm")
208
+ self.assertIsNotNone(stored)
209
+ self.assertEqual(stored["affect"], {"valence": 0.8, "arousal": 0.9})
210
+ self.assertEqual(len(e.store.entries), 1)
211
+
212
+ def test_env_switch_enables(self):
213
+ os.environ["BIOCORE_MEMORY_FEEDBACK"] = "1"
214
+ self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
215
+ affect = {"v": 0.0, "a": 0.5}
216
+ e = self.engine(affect, enabled=False)
217
+ e.recall("beach", budget=3)
218
+ self.assertAlmostEqual(affect["v"], 0.02, places=4)
219
+
220
+ def test_no_affect_state_is_noop(self):
221
+ self.write([entry("warm", "beach warm", {"valence": 0.8, "arousal": 0.9})])
222
+ e = self.engine(None)
223
+ rows = e.recall("beach", budget=3)
224
+ self.assertEqual(len(rows), 1)
225
+ report = e._last_memory_feedback
226
+ self.assertEqual(report["reason"], "no_affect_state")
227
+ self.assertEqual(report["applied"], [])
228
+
229
+ def test_missing_affect_dict_is_noop(self):
230
+ self.write([{"id": "bare", "text": "beach bare", "ts": 1700000000.0,
231
+ "importance": 0.5, "hits": 1, "affect": {"v": None}}])
232
+ affect = {"v": 0.0, "a": 0.5}
233
+ e = self.engine(affect)
234
+ e.recall("beach", budget=3)
235
+ report = e._last_memory_feedback
236
+ self.assertEqual(report["delta"], [0.0, 0.0])
237
+ self.assertAlmostEqual(affect["v"], 0.0, places=6)
238
+
239
+
240
+ if __name__ == "__main__":
241
+ unittest.main()
@@ -0,0 +1,84 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Memory fixes: _save_memory forget path, recall dedup, maintenance entries."""
3
+ import ast
4
+ import json
5
+ import tempfile
6
+ import types
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ import memory_meta
11
+ from biocore_model import bio_tick as bt
12
+
13
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
14
+ SOURCE = MAIN.read_text(encoding="utf-8")
15
+
16
+
17
+ def _load_methods(names, namespace=None):
18
+ tree = ast.parse(SOURCE)
19
+ cls = next(n for n in tree.body
20
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
21
+ ns = dict(namespace or {})
22
+ fns = {}
23
+ for node in cls.body:
24
+ if isinstance(node, ast.FunctionDef) and node.name in names:
25
+ clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
26
+ decorator_list=[], returns=node.returns,
27
+ type_comment=None, lineno=node.lineno,
28
+ col_offset=node.col_offset,
29
+ end_lineno=node.end_lineno,
30
+ end_col_offset=node.end_col_offset)
31
+ exec(compile(ast.Module(body=[clone], type_ignores=[]), "<mem>", "exec"), ns)
32
+ fns[node.name] = ns[node.name]
33
+ return fns
34
+
35
+
36
+ class SaveMemoryTest(unittest.TestCase):
37
+ def test_save_memory_runs_forget(self):
38
+ with tempfile.TemporaryDirectory() as tmp:
39
+ fns = _load_methods(("_save_memory", "_memory_save_doc_for_session"), {
40
+ "json": json, "time": __import__("time"), "Path": Path,
41
+ "MEMORY_SCHEMA_VERSION": 2,
42
+ "MEMORY_DIR": Path(tmp),
43
+ "logger": types.SimpleNamespace(
44
+ warning=lambda *a, **k: None, info=lambda *a, **k: None),
45
+ })
46
+ plugin = types.SimpleNamespace(
47
+ _memory_path=lambda event: Path(tmp) / "s1.json")
48
+ plugin._save_memory = types.MethodType(fns["_save_memory"], plugin)
49
+ plugin._memory_save_doc_for_session = types.MethodType(
50
+ fns["_memory_save_doc_for_session"], plugin)
51
+ doc = {"entries": [{"id": "stale", "retrieval_s": 0.001,
52
+ "last_access_ts": 1.0, "text": "old"}]}
53
+ plugin._save_memory(None, doc)
54
+ saved = json.loads((Path(tmp) / "s1.json").read_text(encoding="utf-8"))
55
+ self.assertTrue(saved["entries"][0].get("hidden"))
56
+
57
+
58
+ class DedupTest(unittest.TestCase):
59
+ def test_same_cluster_keeps_two(self):
60
+ fns = _load_methods(("_dedup_memory_top",), {"memory_meta": memory_meta})
61
+ plugin = types.SimpleNamespace()
62
+ fn = types.MethodType(fns["_dedup_memory_top"], plugin)
63
+ rows = [(0.9, {"text": "今天去海边玩"}),
64
+ (0.8, {"text": "今天去海边玩"}),
65
+ (0.7, {"text": "今天去海边玩"}),
66
+ (0.6, {"text": "完全不同的内容"})]
67
+ out = fn(rows)
68
+ self.assertLessEqual(len(out), 3)
69
+ self.assertEqual(len(out), 3)
70
+
71
+
72
+ class MaintenanceTest(unittest.TestCase):
73
+ def test_entries_are_processed_and_returned(self):
74
+ now = 2_000_000_000.0
75
+ rows = [{"id": "a", "retrieval_s": 0.9, "last_access_ts": now},
76
+ {"id": "b", "retrieval_s": 0.01, "last_access_ts": now}]
77
+ out = bt.memory_maintenance(None, now, dt_hours=1.0, entries=rows)
78
+ self.assertFalse(out.get("skipped"))
79
+ self.assertIn("entries", out)
80
+ self.assertEqual(len(out["entries"]), 2)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ unittest.main()
@@ -0,0 +1,58 @@
1
+ # -*- coding: utf-8 -*-
2
+ import random, unittest
3
+ from biocore_model import memory_forgetting as mf
4
+
5
+ NOW = 1_700_000_000
6
+
7
+ class ForgettingTest(unittest.TestCase):
8
+ def test_decay_1h(self):
9
+ self.assertLess(mf.strength(1.0, 1), 1.0)
10
+ def test_decay_monotonic(self):
11
+ self.assertGreater(mf.strength(1.0, 1), mf.strength(1.0, 24))
12
+ self.assertGreater(mf.strength(1.0, 24), mf.strength(1.0, 24 * 7))
13
+ def test_power_law_value(self):
14
+ self.assertAlmostEqual(mf.strength(1.0, 24), round(2 ** -0.5, 6), places=5)
15
+ def test_recall_boost(self):
16
+ entry = mf.recall({'strength': 0.3}, NOW)
17
+ self.assertGreater(entry['strength'], 0.3); self.assertEqual(entry['recall_count'], 1)
18
+ def test_recall_cap(self):
19
+ entry = mf.recall({'strength': 0.95}, NOW)
20
+ self.assertLessEqual(entry['strength'], 1.0)
21
+ def test_core_after_five(self):
22
+ entry = {'strength': 0.5}
23
+ for _ in range(6): entry = mf.recall(entry, NOW)
24
+ self.assertTrue(entry.get('core_memory'))
25
+ def test_core_never_decays(self):
26
+ entry = mf.decayed({'strength': 0.6, 'core_memory': True, 'last_decay_ts': NOW - 86400 * 30}, NOW)
27
+ self.assertEqual(entry['strength'], 0.6)
28
+ def test_forgotten_threshold(self):
29
+ self.assertFalse(mf.injectable({'strength': 0.1}))
30
+ self.assertTrue(mf.injectable({'strength': 0.2}))
31
+ def test_memory_view(self):
32
+ view = mf.memory_view([{'id': 'a', 'strength': 0.9}, {'id': 'b', 'strength': 0.05}])
33
+ self.assertEqual(view['top'][0]['id'], 'a'); self.assertEqual(view['forgotten_count'], 1)
34
+ def test_rumination_requires_negative(self):
35
+ items = mf.add_rumination([], 'message', 'm1', 'text', NOW, valence=0.2)
36
+ self.assertEqual(items, [])
37
+ def test_rumination_added(self):
38
+ items = mf.add_rumination([], 'message', 'm1', 'text', NOW, valence=-0.6)
39
+ self.assertEqual(len(items), 1); self.assertEqual(items[0]['count'], 1)
40
+ def test_rumination_dedupe(self):
41
+ items = mf.add_rumination([], 'message', 'm1', 't', NOW, conflict=True)
42
+ items = mf.add_rumination(items, 'message', 'm1', 't', NOW + 60, conflict=True)
43
+ self.assertEqual(len(items), 1); self.assertEqual(items[0]['count'], 2)
44
+ def test_rumination_removed_at_count5(self):
45
+ items = [{'target_type': 'message', 'target_id': 'm', 'count': 5, 'first_ts': NOW - 100, 'last_ts': NOW - 100, 'intensity': 1.0}]
46
+ self.assertEqual(mf.tick_rumination(items, NOW, random.Random(1)), [])
47
+ def test_rumination_removed_after_72h(self):
48
+ items = [{'target_type': 'message', 'target_id': 'm', 'count': 1, 'first_ts': NOW - 80 * 3600, 'last_ts': NOW - 80 * 3600}]
49
+ self.assertEqual(mf.tick_rumination(items, NOW, random.Random(1)), [])
50
+ def test_rumination_same_now_idempotent(self):
51
+ items = [{'target_type': 'message', 'target_id': 'm', 'count': 1, 'first_ts': NOW, 'last_ts': NOW, 'last_tick_ts': NOW}]
52
+ kept = mf.tick_rumination(items, NOW, random.Random(1))
53
+ self.assertEqual(len(kept), 1); self.assertEqual(kept[0]['count'], 1)
54
+ def test_rumination_text(self):
55
+ self.assertIn('你还在想', mf.rumination_text([{'text_snippet': '那句话'}]))
56
+
57
+ if __name__ == '__main__':
58
+ unittest.main()