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,69 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import connection_bids as cb
4
+
5
+ NOW = 1_700_000_000
6
+
7
+ class BidsTest(unittest.TestCase):
8
+ def test_record_bid(self):
9
+ bids = cb.record_bid([], NOW, 'lonely')
10
+ self.assertEqual(len(bids), 1); self.assertIsNone(bids[0]['turned'])
11
+ def test_turned_toward(self):
12
+ bids = cb.settle_bids(cb.record_bid([], NOW), NOW + 600, responded_ts=NOW + 600)
13
+ self.assertEqual(bids[0]['turned'], 'toward')
14
+ def test_turned_away(self):
15
+ bids = cb.settle_bids(cb.record_bid([], NOW), NOW + 7 * 3600)
16
+ self.assertEqual(bids[0]['turned'], 'away')
17
+ def test_streaks(self):
18
+ bids = [{'turned': 'toward'}, {'turned': 'away'}, {'turned': 'away'}, {'turned': 'away'}]
19
+ self.assertEqual(cb.streaks(bids), (0, 3))
20
+ self.assertEqual(cb.streaks([{'turned': 'away'}, {'turned': 'toward'}, {'turned': 'toward'}, {'turned': 'toward'}]), (3, 0))
21
+ def test_frequency_after_three_away(self):
22
+ bids = [{'turned': 'away'}] * 3
23
+ self.assertAlmostEqual(cb.bid_frequency(bids), 0.7)
24
+ def test_frequency_after_three_toward(self):
25
+ bids = [{'turned': 'toward'}] * 3
26
+ self.assertAlmostEqual(cb.bid_frequency(bids), 1.1)
27
+ def test_frequency_floor(self):
28
+ bids = [{'turned': 'away'}] * 10
29
+ self.assertGreaterEqual(cb.bid_frequency(bids, 0.6), cb.FREQ_MIN)
30
+ def test_frequency_cap(self):
31
+ bids = [{'turned': 'toward'}] * 10
32
+ self.assertLessEqual(cb.bid_frequency(bids, 1.25), cb.FREQ_MAX)
33
+ def test_bid_view(self):
34
+ view = cb.bid_view([{'turned': 'toward'}, {'turned': 'away'}], 1.1)
35
+ self.assertEqual(view['sent'], 2); self.assertEqual(view['toward'], 1); self.assertEqual(view['away'], 1)
36
+
37
+ class ResponsivenessTest(unittest.TestCase):
38
+ def test_responded_raises(self):
39
+ self.assertGreater(cb.responsiveness_update(0.5, True), 0.5)
40
+ def test_ignored_lowers(self):
41
+ self.assertLess(cb.responsiveness_update(0.5, False), 0.5)
42
+ def test_gate_cost_low(self):
43
+ self.assertEqual(cb.gate_cost_adjust(0.2), 0.05)
44
+ def test_gate_cost_high(self):
45
+ self.assertEqual(cb.gate_cost_adjust(0.9), -0.05)
46
+ def test_disclosure_mult_low(self):
47
+ self.assertEqual(cb.disclosure_mult(0.2), 0.7)
48
+ def test_disclosure_mult_high(self):
49
+ self.assertEqual(cb.disclosure_mult(0.8), 1.0)
50
+
51
+ class TrustTest(unittest.TestCase):
52
+ def test_three_dimensions(self):
53
+ trust = cb.trust_update(None, regular=True, kept=True, response_rate=0.8)
54
+ for key in ('predictability', 'dependability', 'faith', 'overall'):
55
+ self.assertIn(key, trust)
56
+ def test_dimensions_independent(self):
57
+ trust = cb.trust_update(None, regular=True)
58
+ self.assertGreater(trust['predictability'], 0.5)
59
+ self.assertAlmostEqual(trust['dependability'], 0.5)
60
+ def test_delayed_lowers_dependability(self):
61
+ trust = cb.trust_update(None, delayed=True)
62
+ self.assertLess(trust['dependability'], 0.5)
63
+ def test_overall_weights(self):
64
+ trust = {'predictability': 0.8, 'dependability': 0.6, 'faith': 0.4}
65
+ out = cb.trust_update(trust)
66
+ self.assertAlmostEqual(out['overall'], 0.4 * 0.8 + 0.4 * 0.6 + 0.2 * 0.4)
67
+
68
+ if __name__ == '__main__':
69
+ unittest.main()
@@ -0,0 +1,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json, tempfile, unittest
3
+ from pathlib import Path
4
+ import sys
5
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
6
+ from biocore_model.core_bridge import CoreBridge
7
+ class FakeCore:
8
+ def __init__(self, store=None, enabled=True, timeout=2.0, fail=False):
9
+ self.calls=[]; self.fail=fail; self.store=store
10
+ def remember(self,text,**kw):
11
+ self.calls.append(("remember",text))
12
+ return None if self.fail else {"ok":True,"id":"core1"}
13
+ def recall(self,q,limit=3):
14
+ self.calls.append(("recall",q))
15
+ return None if self.fail else {"ok":True,"results":[{"id":"core1","text":"core result"}]}
16
+ def forget(self,entry_id=None,query=None,limit=5):
17
+ self.calls.append(("forget",entry_id or query))
18
+ return None if self.fail else {"ok":True,"removed":[entry_id]}
19
+ class BridgeTest(unittest.TestCase):
20
+ def setUp(self): self.tmp=tempfile.TemporaryDirectory(); self.dir=Path(self.tmp.name); self.bridge=CoreBridge({"core_client_mode":"off"},self.dir,core_factory=FakeCore)
21
+ def tearDown(self): self.tmp.cleanup()
22
+ def test_off_no_calls(self): self.assertIsNone(self.bridge.recall("x")); self.assertEqual(self.bridge.calls,0)
23
+ def test_disabled_default(self): self.assertFalse(CoreBridge({},self.dir,core_factory=FakeCore).enabled)
24
+ def test_shadow_recall(self):
25
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"shadow"},self.dir,core_factory=FakeCore); self.assertTrue(b.enabled); rows=b.recall("q"); self.assertEqual(rows["results"][0]["id"],"core1"); self.assertGreaterEqual(b.calls,1)
26
+ def test_primary_format(self):
27
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"primary"},self.dir,core_factory=FakeCore); text=b.rows_to_text(b.recall("q")["results"]); self.assertIn("core result",text)
28
+ def test_shadow_record(self):
29
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"shadow"},self.dir,core_factory=FakeCore); row=b.record_shadow("q",["a","b"],["b","c"],0.1,0.2); self.assertEqual(row["overlap"],1); lines=(self.dir/"shadow_diff.jsonl").read_text(encoding="utf-8").strip().splitlines(); self.assertEqual(len(lines),1); self.assertIn("old_ids",json.loads(lines[0]))
30
+ def test_failure_degrades(self):
31
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"primary"},self.dir,core_factory=lambda **kw: FakeCore(fail=True)); self.assertIsNone(b.recall("q")); self.assertEqual(b.failures,1)
32
+ def test_circuit_after_five(self):
33
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"primary"},self.dir,core_factory=lambda **kw: FakeCore(fail=True))
34
+ for _ in range(5): b.recall("q")
35
+ self.assertTrue(b.circuit_open); before=b.calls; self.assertIsNone(b.recall("q")); self.assertEqual(b.calls,before)
36
+ def test_migrated_store_detection(self):
37
+ core=self.dir/"core_store"; core.mkdir(); (core/"memory.json").write_text('{"entries":[]}',encoding="utf-8"); b=CoreBridge({"core_client_enabled":True},self.dir,core_factory=FakeCore); self.assertTrue(b.migrated_available); self.assertTrue(b.store_path().endswith("core_store\\memory.json") or b.store_path().endswith("core_store/memory.json"))
38
+ def test_remember_primary(self):
39
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"primary"},self.dir,core_factory=FakeCore); self.assertEqual(b.remember("hello")["id"],"core1")
40
+ def test_remember_shadow_not_written(self):
41
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"shadow"},self.dir,core_factory=FakeCore); self.assertIsNone(b.remember("hello"))
42
+ def test_forget_primary(self):
43
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"primary"},self.dir,core_factory=FakeCore); self.assertTrue(b.forget(entry_id="a")["ok"])
44
+
45
+ class TimeoutTest(unittest.TestCase):
46
+ def setUp(self):
47
+ self.tmp = tempfile.TemporaryDirectory(); self.dir = Path(self.tmp.name)
48
+ def tearDown(self):
49
+ self.tmp.cleanup()
50
+ def test_core_client_timeout_default_and_set(self):
51
+ from biocore_model.core_client import CoreClient
52
+ client = CoreClient(enabled=False)
53
+ self.assertEqual(client.timeout, 3.0)
54
+ self.assertEqual(client.set_timeout(2.5), 2.5)
55
+ self.assertEqual(client.timeout, 2.5)
56
+ self.assertEqual(client.set_timeout(0.1), 2.5)
57
+ self.assertEqual(client.set_timeout("bad"), 2.5)
58
+
59
+ def test_bridge_uses_two_second_timeout(self):
60
+ seen = {}
61
+ def factory(**kw):
62
+ seen.update(kw)
63
+ return FakeCore(**kw)
64
+ b = CoreBridge({"core_client_enabled": True, "core_client_mode": "primary"}, self.dir, core_factory=factory)
65
+ b.recall("q")
66
+ self.assertEqual(seen.get("timeout"), 2.0)
67
+
68
+ class ShadowRememberTest(unittest.TestCase):
69
+ def setUp(self):
70
+ self.tmp = tempfile.TemporaryDirectory(); self.dir = Path(self.tmp.name)
71
+ def tearDown(self):
72
+ self.tmp.cleanup()
73
+ def test_shadow_remember_writes_diff(self):
74
+ b = CoreBridge({"core_client_enabled": True, "core_client_mode": "shadow"}, self.dir, core_factory=FakeCore)
75
+ self.assertIsNone(b.remember("hello"))
76
+ lines = (self.dir / "shadow_diff.jsonl").read_text(encoding="utf-8").strip().splitlines()
77
+ self.assertEqual(len(lines), 1)
78
+ row = json.loads(lines[0])
79
+ self.assertIn("text_hash", row); self.assertTrue(row["core_ok"])
80
+ def test_primary_remember_no_diff(self):
81
+ b = CoreBridge({"core_client_enabled": True, "core_client_mode": "primary"}, self.dir, core_factory=FakeCore)
82
+ self.assertIsNotNone(b.remember("hello"))
83
+ self.assertFalse((self.dir / "shadow_diff.jsonl").exists())
84
+ def test_off_remember_zero(self):
85
+ calls = []
86
+ def factory(**kw): calls.append(kw); return FakeCore(**kw)
87
+ b = CoreBridge({"core_client_mode": "off"}, self.dir, core_factory=factory)
88
+ self.assertIsNone(b.remember("hello")); self.assertEqual(calls, []); self.assertFalse((self.dir / "shadow_diff.jsonl").exists())
@@ -0,0 +1,14 @@
1
+ import tempfile, unittest
2
+ from pathlib import Path
3
+ import sys
4
+ sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
5
+ from biocore_model.core_bridge import CoreBridge
6
+ class EpisodeTest(unittest.TestCase):
7
+ def setUp(self): self.tmp=tempfile.TemporaryDirectory(); self.dir=Path(self.tmp.name)
8
+ def tearDown(self): self.tmp.cleanup()
9
+ def test_record_episode_local_once(self):
10
+ b=CoreBridge({'core_client_enabled':True,'core_client_mode':'shadow'},self.dir,core_factory=lambda **kw: None)
11
+ self.assertIsNotNone(b.record_episode('guard:refusal','blocked',0.0,{'reason':'x'}))
12
+ lines=(self.dir/'episodes-local.jsonl').read_text(encoding='utf-8').strip().splitlines(); self.assertEqual(len(lines),1); self.assertIn('guard:refusal',lines[0])
13
+ def test_off_no_episode(self):
14
+ b=CoreBridge({'core_client_mode':'off'},self.dir,core_factory=lambda **kw: None); self.assertIsNone(b.record_episode('guard:refusal','blocked'))
@@ -0,0 +1,17 @@
1
+ import tempfile, unittest
2
+ from pathlib import Path
3
+ import sys
4
+ sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
5
+ from biocore_model.core_bridge import CoreBridge
6
+ class FakeCore:
7
+ def __init__(self,store=None,enabled=True,timeout=2.0,fail=False,**kw): self.calls=[]; self.fail=fail
8
+ def forget(self,entry_id=None,query=None,limit=5): self.calls.append(1); return None if self.fail else {"ok":True,"removed":[entry_id]}
9
+ class T(unittest.TestCase):
10
+ def setUp(self): self.tmp=tempfile.TemporaryDirectory(); self.dir=Path(self.tmp.name)
11
+ def tearDown(self): self.tmp.cleanup()
12
+ def test_ok(self):
13
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"primary"},self.dir,core_factory=FakeCore); self.assertTrue(b.forget_via_core(entry_id="a")["used_core"])
14
+ def test_fail(self):
15
+ b=CoreBridge({"core_client_enabled":True,"core_client_mode":"primary"},self.dir,core_factory=lambda **kw: FakeCore(fail=True)); self.assertFalse(b.forget_via_core(entry_id="a")["used_core"])
16
+ def test_off(self):
17
+ b=CoreBridge({"core_client_mode":"off"},self.dir,core_factory=FakeCore); self.assertFalse(b.forget_via_core(entry_id="a")["used_core"]); self.assertEqual(b.calls,0)
@@ -0,0 +1,15 @@
1
+ import tempfile, unittest
2
+ from pathlib import Path
3
+ import sys
4
+ sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
5
+ from biocore_model.core_bridge import CoreBridge
6
+ class Fake:
7
+ def __init__(self,**kw): self.calls=[]
8
+ def _call(self,m,p): self.calls.append((m,p)); return {'ok':True}
9
+ class T(unittest.TestCase):
10
+ def setUp(self): self.tmp=tempfile.TemporaryDirectory(); self.dir=Path(self.tmp.name)
11
+ def tearDown(self): self.tmp.cleanup()
12
+ def test_primary_identity(self):
13
+ b=CoreBridge({'core_client_enabled':True,'core_client_mode':'primary'},self.dir,core_factory=Fake); r=b.identity_signal({'level':'high'}); self.assertIsNotNone(r); self.assertEqual(b._client.calls[0][0],'identity.writeback')
14
+ def test_off_identity(self):
15
+ b=CoreBridge({'core_client_mode':'off'},self.dir,core_factory=Fake); self.assertIsNone(b.identity_signal({'level':'high'})); self.assertEqual(b.calls,0)
@@ -0,0 +1,17 @@
1
+ import tempfile, unittest
2
+ from pathlib import Path
3
+ import sys
4
+ sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
5
+ from biocore_model.core_bridge import CoreBridge
6
+ class Fake:
7
+ def __init__(self,**kw): self.calls=[]; self.last_error='provider_unavailable'
8
+ def _call(self,m,p): self.calls.append((m,p)); return None
9
+ class T(unittest.TestCase):
10
+ def setUp(self): self.tmp=tempfile.TemporaryDirectory(); self.dir=Path(self.tmp.name)
11
+ def tearDown(self): self.tmp.cleanup()
12
+ def test_enabled_probe_writes(self):
13
+ b=CoreBridge({'core_client_enabled':True,'core_client_mode':'shadow'},self.dir,core_factory=Fake); r=b.media_probe(); self.assertIsNotNone(r); self.assertTrue((self.dir/'media-probe.jsonl').exists()); row=(self.dir/'media-probe.jsonl').read_text(encoding='utf-8'); self.assertIn('provider_unavailable',row)
14
+ def test_off_probe(self):
15
+ calls=[]
16
+ def f(**kw): calls.append(kw); return Fake(**kw)
17
+ b=CoreBridge({'core_client_mode':'off'},self.dir,core_factory=f); self.assertIsNone(b.media_probe()); self.assertEqual(calls,[]); self.assertFalse((self.dir/'media-probe.jsonl').exists())
@@ -0,0 +1,17 @@
1
+ import tempfile, unittest
2
+ from pathlib import Path
3
+ import sys
4
+ sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
5
+ from biocore_model.core_bridge import CoreBridge
6
+ class Fake:
7
+ def __init__(self,**kw): self.calls=[]
8
+ def _call(self,m,p): self.calls.append((m,p)); return {'ok':True}
9
+ class T(unittest.TestCase):
10
+ def setUp(self): self.tmp=tempfile.TemporaryDirectory(); self.dir=Path(self.tmp.name)
11
+ def tearDown(self): self.tmp.cleanup()
12
+ def test_primary_relation(self):
13
+ b=CoreBridge({'core_client_enabled':True,'core_client_mode':'primary'},self.dir,core_factory=Fake); self.assertIsNotNone(b.relation_update({'trust_delta':0.1})); self.assertTrue(b._client.calls)
14
+ def test_off_relation(self):
15
+ calls=[]
16
+ def f(**kw): calls.append(kw); return Fake(**kw)
17
+ b=CoreBridge({'core_client_mode':'off'},self.dir,core_factory=f); self.assertIsNone(b.relation_update({'trust_delta':0.1})); self.assertEqual(calls,[])
@@ -0,0 +1,17 @@
1
+ import tempfile, unittest
2
+ from pathlib import Path
3
+ import sys
4
+ sys.path.insert(0,str(Path(__file__).resolve().parents[2]))
5
+ from biocore_model.core_bridge import CoreBridge
6
+ class Fake:
7
+ def __init__(self,**kw): self.calls=[]
8
+ def _call(self,m,p): self.calls.append((m,p)); return {'ok':True}
9
+ class T(unittest.TestCase):
10
+ def setUp(self): self.tmp=tempfile.TemporaryDirectory(); self.dir=Path(self.tmp.name)
11
+ def tearDown(self): self.tmp.cleanup()
12
+ def test_primary_summary(self):
13
+ b=CoreBridge({'core_client_enabled':True,'core_client_mode':'primary'},self.dir,core_factory=Fake); self.assertIsNone(b.summary_dry_run({'text':'hello'})); self.assertEqual(b._client.calls[0][0],'summary.daily'); self.assertTrue((self.dir/'summary-dryrun.jsonl').exists())
14
+ def test_off_summary(self):
15
+ calls=[]
16
+ def f(**kw): calls.append(kw); return Fake(**kw)
17
+ b=CoreBridge({'core_client_mode':'off'},self.dir,core_factory=f); self.assertIsNone(b.summary_dry_run({'text':'x'})); self.assertEqual(calls,[]); self.assertFalse((self.dir/'summary-dryrun.jsonl').exists())
@@ -0,0 +1,156 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Offline tests for biocore_model.core_client (fake runner; no real service)."""
3
+ import os
4
+ import unittest
5
+
6
+ from biocore_model.core_client import CoreClient
7
+
8
+
9
+ class FakeRunner:
10
+ """Transport stub: records requests and returns scripted responses."""
11
+
12
+ def __init__(self, responses=None, errors=None):
13
+ self.requests = []
14
+ self.timeouts = []
15
+ self.responses = list(responses or [])
16
+ self.errors = list(errors or [])
17
+
18
+ def __call__(self, request, timeout):
19
+ self.requests.append(request)
20
+ self.timeouts.append(timeout)
21
+ if self.errors:
22
+ raise self.errors.pop(0)
23
+ if self.responses:
24
+ return self.responses.pop(0)
25
+ return {"id": request.get("id"), "ok": True, "result": {"ok": True, "echo": request.get("method")}}
26
+
27
+
28
+ def ok(result=None):
29
+ return {"id": 0, "ok": True, "result": result if result is not None else {"ok": True}}
30
+
31
+
32
+ def client(runner, **kwargs):
33
+ options = {"store": "C:/tmp/biocore-test-store", "enabled": True, "runner": runner}
34
+ options.update(kwargs)
35
+ return CoreClient(**options)
36
+
37
+
38
+ class CoreClientTest(unittest.TestCase):
39
+ def test_disabled_by_default_returns_none(self):
40
+ os.environ.pop("BIOCORE_CORE_CLIENT", None)
41
+ fake = FakeRunner()
42
+ core = client(fake, enabled=False)
43
+ self.assertFalse(core.enabled)
44
+ self.assertIsNone(core.remember("hello"))
45
+ self.assertIsNone(core.recall("hello"))
46
+ self.assertIsNone(core.forget(entry_id="abc"))
47
+ self.assertIsNone(core.state())
48
+ self.assertIsNone(core.bio_snapshot())
49
+ self.assertIsNone(core.health())
50
+ self.assertEqual(fake.requests, [])
51
+
52
+ def test_env_switch_enables_client(self):
53
+ os.environ["BIOCORE_CORE_CLIENT"] = "1"
54
+ try:
55
+ fake = FakeRunner()
56
+ core = CoreClient(store="C:/tmp/biocore-test-store", runner=fake)
57
+ self.assertTrue(core.enabled)
58
+ self.assertEqual(core.health().get("ok"), True)
59
+ self.assertEqual(len(fake.requests), 1)
60
+ finally:
61
+ os.environ.pop("BIOCORE_CORE_CLIENT", None)
62
+
63
+ def test_remember_parses_result(self):
64
+ fake = FakeRunner([ok({"ok": True, "entry": {"id": "abc", "text": "hello"}})])
65
+ core = client(fake)
66
+ result = core.remember("hello", tags=["plan"], importance=0.8)
67
+ self.assertEqual(result["entry"]["id"], "abc")
68
+ self.assertEqual(core.last_error, "")
69
+ request = fake.requests[0]
70
+ self.assertEqual(request["method"], "memory.remember")
71
+ self.assertEqual(request["params"]["text"], "hello")
72
+ self.assertEqual(request["params"]["tags"], ["plan"])
73
+ self.assertEqual(request["params"]["importance"], 0.8)
74
+ self.assertEqual(request["params"]["role"], "user")
75
+
76
+ def test_id_and_method_passthrough(self):
77
+ fake = FakeRunner([ok({"results": []}), ok({"ok": True})])
78
+ core = client(fake)
79
+ core.recall("friday", limit=4)
80
+ core.health()
81
+ self.assertEqual([request["method"] for request in fake.requests], ["memory.recall", "health.check"])
82
+ self.assertEqual([request["id"] for request in fake.requests], [1, 2])
83
+ self.assertEqual(fake.requests[0]["params"], {"query": "friday", "limit": 4})
84
+
85
+ def test_timeout_returns_none_without_raise(self):
86
+ fake = FakeRunner(errors=[TimeoutError("slow")])
87
+ core = client(fake)
88
+ self.assertIsNone(core.recall("anything"))
89
+ self.assertIn("timeout", core.last_error)
90
+ self.assertEqual(len(fake.requests), 1)
91
+
92
+ def test_crash_once_restarts_and_succeeds(self):
93
+ fake = FakeRunner(responses=[ok({"ok": True, "entry": {"id": "after-restart"}})], errors=[ChildProcessError("boom")])
94
+ core = client(fake, max_restarts=1)
95
+ result = core.remember("survives a crash")
96
+ self.assertEqual(result["entry"]["id"], "after-restart")
97
+ self.assertEqual(len(fake.requests), 2)
98
+ self.assertEqual([request["method"] for request in fake.requests], ["memory.remember", "memory.remember"])
99
+ self.assertIn("process_error", core.last_error) if False else None
100
+ self.assertEqual(core.last_error, "")
101
+
102
+ def test_crash_twice_degrades_to_none(self):
103
+ fake = FakeRunner(errors=[ChildProcessError("boom"), ChildProcessError("boom again")])
104
+ core = client(fake, max_restarts=1)
105
+ self.assertIsNone(core.remember("never succeeds"))
106
+ self.assertEqual(len(fake.requests), 2)
107
+ self.assertIn("process_error", core.last_error)
108
+
109
+ def test_error_envelope_returns_none_with_code(self):
110
+ fake = FakeRunner([{"id": 1, "ok": False, "error": {"code": "readonly", "message": "store is read-only"}}])
111
+ core = client(fake)
112
+ self.assertIsNone(core.remember("blocked"))
113
+ self.assertIn("readonly", core.last_error)
114
+
115
+ def test_forget_by_id_and_by_query(self):
116
+ fake = FakeRunner([
117
+ ok({"ok": True, "id": "abc", "removed": True}),
118
+ ok({"results": [{"id": "r1"}, {"id": "r2"}]}),
119
+ ok({"ok": True, "id": "r1", "removed": True}),
120
+ ok({"ok": True, "id": "r2", "removed": False}),
121
+ ])
122
+ core = client(fake)
123
+ self.assertTrue(core.forget(entry_id="abc")["removed"])
124
+ result = core.forget(query="friday")
125
+ self.assertEqual(result["removed"], ["r1"])
126
+ self.assertEqual(result["count"], 1)
127
+ methods = [request["method"] for request in fake.requests]
128
+ self.assertEqual(methods, ["memory.forget", "memory.recall", "memory.forget", "memory.forget"])
129
+
130
+ def test_state_scope_and_bio_health_method_names(self):
131
+ fake = FakeRunner([ok({"ok": True}), ok({"ok": True}), ok({"ok": True})])
132
+ core = client(fake)
133
+ core.state(limit=5, scope="session:s1")
134
+ core.bio_snapshot()
135
+ core.health()
136
+ self.assertEqual([request["method"] for request in fake.requests], ["memory.state", "bio.snapshot", "health.check"])
137
+ self.assertEqual(fake.requests[0]["params"], {"limit": 5, "scope": "session:s1"})
138
+
139
+ def test_timeout_is_configurable_and_forwarded(self):
140
+ fake = FakeRunner()
141
+ core = client(fake, timeout=0.5)
142
+ core.health()
143
+ self.assertEqual(fake.timeouts[0], 0.5)
144
+
145
+ def test_close_blocks_later_calls(self):
146
+ fake = FakeRunner()
147
+ core = client(fake)
148
+ self.assertEqual(core.health().get("ok"), True)
149
+ core.close()
150
+ self.assertIsNone(core.recall("after close"))
151
+ self.assertEqual(len(fake.requests), 1)
152
+
153
+ def test_missing_service_script_degrades_without_throw(self):
154
+ core = CoreClient(store="C:/tmp/biocore-test-store", enabled=True, service_script="", runner=None)
155
+ self.assertIsNone(core.health())
156
+ self.assertIn("service_script_missing", core.last_error)
@@ -0,0 +1,146 @@
1
+ class _Recorder:
2
+ """Logger stub so tests never import the stdlib logging module."""
3
+
4
+ def __init__(self):
5
+ self.lines = []
6
+ self.level = 0
7
+
8
+ def _log(self, msg, *args, **kwargs):
9
+ try:
10
+ self.lines.append(str(msg) % args if args else str(msg))
11
+ except Exception:
12
+ self.lines.append(str(msg))
13
+
14
+ def setLevel(self, *_a, **_k):
15
+ return None
16
+
17
+ def addHandler(self, *_a, **_k):
18
+ return None
19
+
20
+ def removeHandler(self, *_a, **_k):
21
+ return None
22
+
23
+ debug = info = warning = error = exception = critical = _log
24
+
25
+
26
+ # -*- coding: utf-8 -*-
27
+ """Chat commands /biocore_core and /biocore_rollback (admin only, thin over core_mode)."""
28
+ import unittest
29
+
30
+ from biocore_model import core_commands
31
+ from biocore_model import core_mode, core_mode
32
+
33
+
34
+ class FakeEvent:
35
+ def __init__(self, admin=True, private=True, sender="u1", message=""):
36
+ self.admin = admin
37
+ self.private = private
38
+ self.sender = sender
39
+ self.message_str = message
40
+
41
+ def is_admin(self):
42
+ return bool(self.admin)
43
+
44
+ def is_private_chat(self):
45
+ return bool(self.private)
46
+
47
+ def get_sender_id(self):
48
+ return self.sender
49
+
50
+
51
+ class NoAdminEvent(FakeEvent):
52
+ is_admin = None # attribute absent path -> private chat + whitelist
53
+
54
+
55
+ class FakePlugin:
56
+ def __init__(self, admin_ids=None):
57
+ self.config = {"core_client_mode": "off", "core_client_enabled": False,
58
+ "command_admin_ids": list(admin_ids or [])}
59
+ self.data_admin_token = ""
60
+ self.saved = []
61
+
62
+ async def _persist_plugin_config(self, updates):
63
+ self.saved.append(dict(updates))
64
+ self.config.update(updates)
65
+
66
+ def _rebuild_core_bridge(self):
67
+ return True
68
+
69
+
70
+ class ParseTest(unittest.TestCase):
71
+ def test_three_modes(self):
72
+ for mode in ("off", "shadow", "primary"):
73
+ parsed = core_commands.parse_args("/biocore_core " + mode)
74
+ self.assertTrue(parsed["usable"])
75
+ self.assertEqual(parsed["mode"], mode)
76
+
77
+ def test_invalid_usage(self):
78
+ for raw in ("/biocore_core", "/biocore_core bogus", "/biocore_core shadow extra"):
79
+ parsed = core_commands.parse_args(raw)
80
+ self.assertFalse(parsed["usable"])
81
+ self.assertIn("off|shadow|primary", parsed["usage"])
82
+
83
+
84
+ class PermissionTest(unittest.TestCase):
85
+ def test_admin_event_allowed(self):
86
+ self.assertTrue(core_commands.is_admin(FakeEvent(admin=True), {}))
87
+
88
+ def test_plain_user_denied(self):
89
+ event = NoAdminEvent(admin=False, private=False)
90
+ self.assertFalse(core_commands.is_admin(event, {}))
91
+
92
+ def test_whitelist_allows_group_user(self):
93
+ event = NoAdminEvent(admin=False, private=False, sender="42")
94
+ self.assertTrue(core_commands.is_admin(event, {"command_admin_ids": ["42"]}))
95
+
96
+ def test_private_chat_fallback_allows(self):
97
+ event = NoAdminEvent(admin=False, private=True)
98
+ self.assertTrue(core_commands.is_admin(event, {}))
99
+
100
+
101
+ class RunTest(unittest.IsolatedAsyncioTestCase):
102
+ async def test_switch_reply_and_config(self):
103
+ plugin = FakePlugin()
104
+ reply = await core_commands.run_core_command(plugin, "/biocore_core shadow", FakeEvent())
105
+ self.assertEqual(reply, "核心模式已切换:off → shadow(立即生效)")
106
+ self.assertEqual(plugin.config["core_client_mode"], "shadow")
107
+ self.assertTrue(plugin.config["core_client_enabled"])
108
+ self.assertEqual(plugin.saved[-1], {"core_client_enabled": True, "core_client_mode": "shadow"})
109
+
110
+ async def test_invalid_argument_reply(self):
111
+ plugin = FakePlugin()
112
+ reply = await core_commands.run_core_command(plugin, "/biocore_core nope", FakeEvent())
113
+ self.assertIn("off|shadow|primary", reply)
114
+ self.assertEqual(plugin.saved, [])
115
+
116
+ async def test_non_admin_rejected(self):
117
+ plugin = FakePlugin()
118
+ reply = await core_commands.run_core_command(plugin, "/biocore_core primary", NoAdminEvent(admin=False, private=False))
119
+ self.assertIn("仅管理员可用", reply)
120
+ self.assertEqual(plugin.saved, [])
121
+
122
+ async def test_rollback_command(self):
123
+ plugin = FakePlugin()
124
+ plugin.config.update({"core_client_enabled": True, "core_client_mode": "primary"})
125
+ reply = await core_commands.run_rollback_command(plugin, FakeEvent())
126
+ self.assertEqual(reply, "已回退旧版引擎(off)")
127
+ self.assertFalse(plugin.config["core_client_enabled"])
128
+ self.assertEqual(plugin.config["core_client_mode"], "off")
129
+
130
+ async def test_log_via_command(self):
131
+ plugin = FakePlugin()
132
+ rec = _Recorder()
133
+ saved = core_mode.LOGGER
134
+ core_mode.LOGGER = rec
135
+ try:
136
+ await core_commands.run_core_command(plugin, "/biocore_core shadow", FakeEvent())
137
+ finally:
138
+ core_mode.LOGGER = saved
139
+ joined = "\n".join(rec.lines)
140
+ self.assertIn("core_mode_changed from=off to=shadow via=command", joined)
141
+
142
+ async def test_pending_restart_reply(self):
143
+ plugin = FakePlugin()
144
+ plugin._rebuild_core_bridge = lambda: False
145
+ reply = await core_commands.run_core_command(plugin, "/biocore_core primary", FakeEvent())
146
+ self.assertIn("重启后生效", reply)