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,258 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-193 本地 embedding 健康投影单测(/health + 面板,只读,不加载模型)。
3
+
4
+ main.py 无法在离线单测里 import(依赖 AstrBot),因此这里用 AST 从 main.py
5
+ 抽取四个纯投影函数与 _local_embedding_health 方法并执行;面板断言读取
6
+ app.js / index.src.html / 构建后的 index.html 源码。
7
+ """
8
+ import ast
9
+ import json
10
+ import sys
11
+ import tempfile
12
+ import types
13
+ import unittest
14
+ from pathlib import Path
15
+ from unittest import mock
16
+
17
+ ROOT = Path(__file__).resolve().parents[2]
18
+ MAIN_SRC = (ROOT / "main.py").read_text(encoding="utf-8")
19
+ APP_SRC = (ROOT / "pages" / "dashboard" / "app.js").read_text(encoding="utf-8")
20
+ TPL_SRC = (ROOT / "pages" / "dashboard" / "index.src.html").read_text(encoding="utf-8")
21
+ BUILT_SRC = (ROOT / "pages" / "dashboard" / "index.html").read_text(encoding="utf-8")
22
+
23
+ HELPER_NAMES = ("_local_embedding_switch", "_local_embedding_value",
24
+ "_probe_local_embedding", "_local_embedding_payload")
25
+
26
+
27
+ def _projection_namespace():
28
+ tree = ast.parse(MAIN_SRC)
29
+ functions = {node.name: node for node in tree.body
30
+ if isinstance(node, ast.FunctionDef)
31
+ and node.name in HELPER_NAMES}
32
+ missing = [name for name in HELPER_NAMES if name not in functions]
33
+ if missing:
34
+ raise AssertionError("main.py is missing helpers: %s" % missing)
35
+ plugin = next((node for node in tree.body
36
+ if isinstance(node, ast.ClassDef)
37
+ and node.name == "BioCorePlugin"), None)
38
+ if plugin is None:
39
+ raise AssertionError("BioCorePlugin not found")
40
+ method = next((node for node in plugin.body
41
+ if isinstance(node, ast.FunctionDef)
42
+ and node.name == "_local_embedding_health"), None)
43
+ if method is None:
44
+ raise AssertionError("_local_embedding_health not found")
45
+ module = ast.Module(
46
+ body=[functions[name] for name in HELPER_NAMES] + [method],
47
+ type_ignores=[])
48
+ namespace = {}
49
+ exec(compile(module, str(ROOT / "main.py"), "exec"), namespace)
50
+ return namespace
51
+
52
+
53
+ class _FakeEngine:
54
+ def __init__(self, status, detail=None):
55
+ self.status = status
56
+ self.detail = detail or {}
57
+
58
+ def health(self):
59
+ return {"local_embedding": self.status, "embedding": dict(self.detail)}
60
+
61
+
62
+ class _FakeSelf:
63
+ def __init__(self, config, engines=None, switch=None):
64
+ self.config = config
65
+ self._memory_module_engines = engines or {}
66
+ self._switch = switch
67
+
68
+ def _local_embedding_enabled(self):
69
+ if callable(self._switch):
70
+ return bool(self._switch(self.config))
71
+ return False
72
+
73
+
74
+ class ProjectionTest(unittest.TestCase):
75
+ @classmethod
76
+ def setUpClass(cls):
77
+ cls.ns = _projection_namespace()
78
+
79
+ def setUp(self):
80
+ self._tmp = tempfile.TemporaryDirectory()
81
+ self.root = Path(self._tmp.name)
82
+
83
+ def tearDown(self):
84
+ self._tmp.cleanup()
85
+
86
+ def _payload(self, config):
87
+ return self.ns["_local_embedding_payload"](config)
88
+
89
+ def test_switch_off_returns_no_field(self):
90
+ self.assertIsNone(self._payload(
91
+ {"embedding_local_fallback_enabled": False}))
92
+ self.assertIsNone(self._payload(
93
+ {"embedding_local_fallback_enabled": "off"}))
94
+
95
+ def test_default_off_returns_no_field(self):
96
+ self.assertIsNone(self._payload({}))
97
+ self.assertIsNone(self._payload(
98
+ {"embedding_local_fallback_enabled": False}))
99
+
100
+ def test_missing_when_enabled_without_model_path(self):
101
+ payload = self._payload(
102
+ {"embedding_local_fallback_enabled": True})
103
+ self.assertEqual(payload["local_embedding"], "missing")
104
+ self.assertEqual(payload["local_embedding_model_path"], "")
105
+ self.assertEqual(payload["local_embedding_threads"], 2)
106
+ self.assertEqual(payload["local_embedding_timeout_s"], 8.0)
107
+ self.assertEqual(payload["local_embedding_detail"]["status"], "missing")
108
+ self.assertEqual(payload["local_embedding_detail"]["reason"], "未配置模型路径")
109
+ for key in ("model_path", "threads", "timeout_s", "reason"):
110
+ self.assertIn(key, payload["local_embedding_detail"])
111
+
112
+ def test_failed_when_model_file_missing(self):
113
+ path = str(self.root / "nope.onnx")
114
+ payload = self._payload({"embedding_local_fallback_enabled": True,
115
+ "embedding_local_model_path": path})
116
+ self.assertEqual(payload["local_embedding"], "failed")
117
+ self.assertEqual(payload["local_embedding_model_path"], path)
118
+ self.assertIn("not found", payload["local_embedding_detail"]["reason"])
119
+
120
+ def test_failed_when_tokenizer_missing(self):
121
+ model = self.root / "model.onnx"
122
+ model.write_bytes(b"fake")
123
+ with mock.patch("importlib.util.find_spec", return_value=object()):
124
+ payload = self._payload({"embedding_local_fallback_enabled": True,
125
+ "embedding_local_model_path": str(model)})
126
+ self.assertEqual(payload["local_embedding"], "failed")
127
+ self.assertIn("tokenizer", payload["local_embedding_detail"]["reason"])
128
+
129
+ def test_ok_when_component_files_ready(self):
130
+ model = self.root / "model.onnx"
131
+ model.write_bytes(b"fake")
132
+ (self.root / "tokenizer.json").write_text(
133
+ json.dumps({"model": {"vocab": {}}}), encoding="utf-8")
134
+ with mock.patch("importlib.util.find_spec", return_value=object()):
135
+ payload = self._payload({
136
+ "embedding_local_fallback_enabled": True,
137
+ "embedding_local_model_path": str(model),
138
+ "embedding_local_threads": 3,
139
+ "embedding_timeout_s": 6,
140
+ })
141
+ self.assertEqual(payload["local_embedding"], "ok")
142
+ self.assertEqual(payload["local_embedding_threads"], 3)
143
+ self.assertEqual(payload["local_embedding_timeout_s"], 6.0)
144
+ self.assertEqual(payload["local_embedding_detail"]["status"], "ok")
145
+
146
+ def test_failed_when_onnxruntime_missing(self):
147
+ model = self.root / "model.onnx"
148
+ model.write_bytes(b"fake")
149
+ (self.root / "tokenizer.json").write_text("{}", encoding="utf-8")
150
+ with mock.patch("importlib.util.find_spec", return_value=None):
151
+ payload = self._payload({"embedding_local_fallback_enabled": True,
152
+ "embedding_local_model_path": str(model)})
153
+ self.assertEqual(payload["local_embedding"], "failed")
154
+ self.assertIn("onnxruntime",
155
+ payload["local_embedding_detail"]["reason"])
156
+
157
+ def test_engine_health_is_preferred(self):
158
+ engine = _FakeEngine("ok", {"local_model_path": "C:/m.onnx",
159
+ "local_threads": 2, "timeout_s": 8.0,
160
+ "local_embedding": "ok"})
161
+ method = self.ns["_local_embedding_health"]
162
+ out = method(_FakeSelf({"embedding_local_fallback_enabled": True},
163
+ {"s1": engine},
164
+ switch=self.ns["_local_embedding_switch"]))
165
+ self.assertEqual(out["local_embedding"], "ok")
166
+ self.assertEqual(out["local_embedding_model_path"], "C:/m.onnx")
167
+ failed = _FakeEngine("failed", {"local_model_path": "C:/bad.onnx"})
168
+ out = method(_FakeSelf({"embedding_local_fallback_enabled": True},
169
+ {"s1": failed},
170
+ switch=self.ns["_local_embedding_switch"]))
171
+ self.assertEqual(out["local_embedding"], "failed")
172
+
173
+ def test_switch_off_never_probes(self):
174
+ with mock.patch.dict(self.ns, {
175
+ "_probe_local_embedding": mock.Mock(
176
+ side_effect=AssertionError("must not probe"))}):
177
+ self.assertIsNone(self._payload(
178
+ {"embedding_local_fallback_enabled": False}))
179
+
180
+ def test_switch_off_beats_engine_health(self):
181
+ engine = _FakeEngine("ok", {"local_model_path": "C:/m.onnx"})
182
+ method = self.ns["_local_embedding_health"]
183
+ out = method(_FakeSelf({"embedding_local_fallback_enabled": False},
184
+ {"s1": engine}))
185
+ self.assertIsNone(out)
186
+
187
+ def test_projection_never_loads_the_model(self):
188
+ tree = ast.parse(MAIN_SRC)
189
+ plugin = next(node for node in tree.body
190
+ if isinstance(node, ast.ClassDef)
191
+ and node.name == "BioCorePlugin")
192
+ method_node = next(node for node in plugin.body
193
+ if isinstance(node, ast.FunctionDef)
194
+ and node.name == "_local_embedding_health")
195
+ probe_node = next(node for node in tree.body
196
+ if isinstance(node, ast.FunctionDef)
197
+ and node.name == "_probe_local_embedding")
198
+ for source in (ast.get_source_segment(MAIN_SRC, probe_node),
199
+ ast.get_source_segment(MAIN_SRC, method_node),
200
+ ast.get_source_segment(
201
+ MAIN_SRC,
202
+ next(node for node in tree.body
203
+ if isinstance(node, ast.FunctionDef)
204
+ and node.name == "_local_embedding_payload"))):
205
+ self.assertTrue(source)
206
+ self.assertNotIn("InferenceSession", source)
207
+ self.assertNotIn("warmup(", source)
208
+ self.assertNotIn(".encode(", source)
209
+ self.assertNotIn("import onnxruntime", source)
210
+
211
+
212
+ class WiringSourceTest(unittest.TestCase):
213
+ def test_health_route_and_page_status_wiring(self):
214
+ routes_src = (ROOT / "biocore_model" / "plugin_routes.py").read_text(
215
+ encoding="utf-8")
216
+ self.assertIn("_local_emb = self._local_embedding_health()", routes_src)
217
+ self.assertIn("_data.update(_local_emb)", routes_src)
218
+ self.assertIn("self._local_embedding_enabled()", routes_src)
219
+ self.assertIn("embedding_local_fallback_enabled", routes_src)
220
+ self.assertIn("/astrbot_plugin_biocore/health", MAIN_SRC)
221
+ self.assertIn("self.page_health", MAIN_SRC)
222
+ self.assertIn("async def page_health", MAIN_SRC)
223
+ self.assertIn("_local_emb = self._local_embedding_health()", MAIN_SRC)
224
+ self.assertIn("_data.update(_local_emb)", MAIN_SRC)
225
+ self.assertIn("local_embedding_detail", MAIN_SRC)
226
+
227
+ def test_schema_default_is_off(self):
228
+ schema = json.loads((ROOT / "_conf_schema.json").read_text(
229
+ encoding="utf-8-sig"))
230
+ entry = schema["embedding_local_fallback_enabled"]
231
+ self.assertIs(entry["default"], False)
232
+ self.assertIn("23MB", entry["hint"])
233
+ self.assertIn("默认关", entry["description"])
234
+
235
+ def test_panel_markup_has_switch_and_status(self):
236
+ for source in (TPL_SRC, BUILT_SRC):
237
+ self.assertIn('id="localEmbeddingRow"', source)
238
+ self.assertIn('id="swLocalEmbedding"', source)
239
+ self.assertIn('id="localEmbeddingState"', source)
240
+ self.assertIn('id="localEmbeddingDetail"', source)
241
+ self.assertIn("本地向量", source)
242
+ self.assertIn("本地向量兜底", source)
243
+
244
+ def test_panel_renderer_handles_ok_missing_failed_off(self):
245
+ self.assertIn("function renderLocalEmbedding()", APP_SRC)
246
+ self.assertIn("renderLocalEmbedding();", APP_SRC)
247
+ self.assertIn("local_embedding_detail", APP_SRC)
248
+ self.assertIn("embedding_local_fallback_enabled", APP_SRC)
249
+ self.assertIn("swLocalEmbedding", APP_SRC)
250
+ self.assertIn("本地向量:关(纯远程)", APP_SRC)
251
+ self.assertIn("本地向量:失败", APP_SRC)
252
+ self.assertIn("未配置模型路径", APP_SRC)
253
+ self.assertIn("meta.model_path", APP_SRC)
254
+ self.assertIn("renderLocalEmbedding", APP_SRC)
255
+
256
+
257
+ if __name__ == "__main__":
258
+ unittest.main()
@@ -0,0 +1,79 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os, tempfile, unittest
3
+ from pathlib import Path
4
+ from biocore_memory.engine import MemoryEngine
5
+ from biocore_memory.core import config as engine_config
6
+
7
+ class AffectEncodingTest(unittest.TestCase):
8
+ def setUp(self):
9
+ self.tmp = tempfile.TemporaryDirectory()
10
+ self.dir = Path(self.tmp.name)
11
+ self._saved = {k: os.environ.get(k) for k in
12
+ ("BIOCORE_AFFECT_ENCODING", "BIOCORE_AFFECT_ENCODE_A", "BIOCORE_AFFECT_ENCODE_B")}
13
+ for k in self._saved:
14
+ os.environ.pop(k, None)
15
+
16
+ def tearDown(self):
17
+ self.tmp.cleanup()
18
+ for k, v in self._saved.items():
19
+ if v is None: os.environ.pop(k, None)
20
+ else: os.environ[k] = v
21
+
22
+ def remember(self, affect, **event):
23
+ e = MemoryEngine(data_dir=str(self.dir), affect=affect)
24
+ payload = {"text": event.pop("text", "affect encoding probe"), "role": "user"}
25
+ payload.update(event)
26
+ return e.remember(payload)
27
+
28
+ def test_flag_defaults_off(self):
29
+ self.assertIs(engine_config.FLAGS.get("affect_encoding"), False)
30
+
31
+ def test_off_does_not_change(self):
32
+ result = self.remember({"v": 0.8, "a": 0.8})
33
+ self.assertAlmostEqual(result["importance"], 0.2, places=4)
34
+
35
+ def test_high_arousal_boosts(self):
36
+ os.environ["BIOCORE_AFFECT_ENCODING"] = "1"
37
+ result = self.remember({"v": 0.8, "a": 0.8})
38
+ self.assertAlmostEqual(result["importance"], 0.40, places=3)
39
+
40
+ def test_calm_no_change(self):
41
+ os.environ["BIOCORE_AFFECT_ENCODING"] = "1"
42
+ result = self.remember({"v": 0.0, "a": 0.0})
43
+ self.assertAlmostEqual(result["importance"], 0.2, places=3)
44
+
45
+ def test_negative_valence_uses_abs(self):
46
+ os.environ["BIOCORE_AFFECT_ENCODING"] = "1"
47
+ result = self.remember({"v": -0.8, "a": 0.8})
48
+ self.assertAlmostEqual(result["importance"], 0.40, places=3)
49
+
50
+ def test_explicit_importance_wins(self):
51
+ os.environ["BIOCORE_AFFECT_ENCODING"] = "1"
52
+ result = self.remember({"v": 0.9, "a": 0.9}, importance=0.9)
53
+ self.assertAlmostEqual(result["importance"], 0.9, places=4)
54
+
55
+ def test_clamped_to_one(self):
56
+ os.environ["BIOCORE_AFFECT_ENCODING"] = "1"
57
+ os.environ["BIOCORE_AFFECT_ENCODE_A"] = "0.9"
58
+ os.environ["BIOCORE_AFFECT_ENCODE_B"] = "0.9"
59
+ result = self.remember({"v": 1.0, "a": 1.0})
60
+ self.assertLessEqual(result["importance"], 1.0)
61
+ self.assertGreater(result["importance"], 0.9)
62
+
63
+ def test_no_affect_state_falls_back(self):
64
+ os.environ["BIOCORE_AFFECT_ENCODING"] = "1"
65
+ result = self.remember(None)
66
+ self.assertAlmostEqual(result["importance"], 0.2, places=4)
67
+
68
+ def test_custom_weights(self):
69
+ os.environ["BIOCORE_AFFECT_ENCODING"] = "1"
70
+ os.environ["BIOCORE_AFFECT_ENCODE_A"] = "0.3"
71
+ os.environ["BIOCORE_AFFECT_ENCODE_B"] = "0.2"
72
+ result = self.remember({"v": -0.8, "a": 0.8})
73
+ self.assertAlmostEqual(result["importance"], 0.60, places=3)
74
+
75
+ def test_flag_enabled_via_engine_flags(self):
76
+ e = MemoryEngine(data_dir=str(self.dir), affect={"v": 0.8, "a": 0.8})
77
+ e.flags["affect_encoding"] = True
78
+ result = e.remember({"text": "flag probe", "role": "user"})
79
+ self.assertAlmostEqual(result["importance"], 0.40, places=3)
@@ -0,0 +1,332 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-146: user-selectable memory backend (json | sqlite | auto)."""
3
+ import ast
4
+ import json
5
+ import tempfile
6
+ import types
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from biocore_model import memory_store as ms
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+ MAIN = ROOT / "main.py"
14
+ SOURCE = MAIN.read_text(encoding="utf-8")
15
+ SCHEMA = json.loads((ROOT / "_conf_schema.json").read_text(encoding="utf-8"))
16
+ DASH = ROOT / "pages" / "dashboard"
17
+ SRC_HTML = (DASH / "index.src.html").read_text(encoding="utf-8")
18
+ APP_JS = (DASH / "app.js").read_text(encoding="utf-8")
19
+ INDEX_HTML = (DASH / "index.html").read_text(encoding="utf-8")
20
+
21
+ _HELPER_NAMES = ("_memory_store_module", "_memory_backend_attr",
22
+ "_json_entries_count", "_init_memory_backend",
23
+ "_memory_store_handle", "_memory_backend_switch")
24
+
25
+
26
+ def _load_helpers():
27
+ tree = ast.parse(SOURCE)
28
+ nodes = [item for item in tree.body
29
+ if isinstance(item, ast.FunctionDef) and item.name in _HELPER_NAMES]
30
+ log = []
31
+ ns = {"json": json, "Path": Path,
32
+ "logger": types.SimpleNamespace(
33
+ info=lambda *a, **k: log.append(a),
34
+ warning=lambda *a, **k: None,
35
+ debug=lambda *a, **k: None)}
36
+ exec(compile(ast.Module(body=nodes, type_ignores=[]), "<mem_backend>",
37
+ "exec"), ns)
38
+ return ns, log
39
+
40
+
41
+ def _load_save_method():
42
+ """AST-load BioCorePlugin._memory_save_doc_for_session (+ its helpers)."""
43
+ tree = ast.parse(SOURCE)
44
+ cls = next(node for node in tree.body
45
+ if isinstance(node, ast.ClassDef) and node.name == "BioCorePlugin")
46
+ method = next(item for item in cls.body
47
+ if isinstance(item, ast.FunctionDef)
48
+ and item.name == "_memory_save_doc_for_session")
49
+ clone = ast.FunctionDef(name=method.name, args=method.args, body=method.body,
50
+ decorator_list=[], returns=method.returns,
51
+ type_comment=None, lineno=method.lineno,
52
+ col_offset=method.col_offset,
53
+ end_lineno=method.end_lineno,
54
+ end_col_offset=method.end_col_offset)
55
+ import time as _time
56
+ ns = {"json": json, "Path": Path, "time": _time, "MEMORY_DIR": None,
57
+ "logger": types.SimpleNamespace(
58
+ info=lambda *a, **k: None, warning=lambda *a, **k: None,
59
+ debug=lambda *a, **k: None)}
60
+ for name in ("_memory_store_module", "_memory_store_handle",
61
+ "_json_entries_count"):
62
+ node = next(item for item in tree.body
63
+ if isinstance(item, ast.FunctionDef) and item.name == name)
64
+ exec(compile(ast.Module(body=[node], type_ignores=[]), "<helper>",
65
+ "exec"), ns)
66
+ exec(compile(ast.Module(body=[clone], type_ignores=[]), "<save>",
67
+ "exec"), ns)
68
+ return ns["_memory_save_doc_for_session"], ns
69
+
70
+
71
+ HELPERS, LOG = _load_helpers()
72
+
73
+
74
+ class FakePlugin:
75
+ def __init__(self, setting="auto"):
76
+ self.memory_storage_backend = setting
77
+ self._memory_backend = "json"
78
+ self._memory_store_cache = None
79
+
80
+
81
+ def _doc(name="friend", count=2):
82
+ return {"entries": [{"text": "%s-%d" % (name, i), "ts": float(i)}
83
+ for i in range(count)]}
84
+
85
+
86
+ def _write_json(memory_dir, name, doc):
87
+ path = Path(memory_dir) / (name + ".json")
88
+ path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
89
+
90
+
91
+ class NormalizeTest(unittest.TestCase):
92
+ def test_values_and_default(self):
93
+ self.assertEqual(ms.normalize_backend("json"), "json")
94
+ self.assertEqual(ms.normalize_backend("SQLite "), "sqlite")
95
+ self.assertEqual(ms.normalize_backend("auto"), "auto")
96
+ self.assertEqual(ms.normalize_backend("nope"), "auto")
97
+ self.assertEqual(ms.normalize_backend(None), "auto")
98
+
99
+ def test_resolve_backend_auto(self):
100
+ with tempfile.TemporaryDirectory() as tmp:
101
+ self.assertEqual(ms.resolve_backend("auto", tmp), "json")
102
+ _write_json(tmp, "friend", _doc())
103
+ ms.ensure_store(tmp)
104
+ self.assertEqual(ms.resolve_backend("auto", tmp), "sqlite")
105
+ self.assertEqual(ms.resolve_backend("json", tmp), "json")
106
+ self.assertEqual(ms.resolve_backend("sqlite", tmp), "sqlite")
107
+
108
+ def test_healthy_check_rejects_corruption(self):
109
+ with tempfile.TemporaryDirectory() as tmp:
110
+ (Path(tmp) / ms.SQLITE_FILE).write_bytes(b"garbage")
111
+ self.assertFalse(ms.db_present_healthy(tmp))
112
+ self.assertEqual(ms.resolve_backend("auto", tmp), "json")
113
+
114
+
115
+ class SwitchTest(unittest.TestCase):
116
+ def test_json_to_sqlite_counts_and_backup(self):
117
+ with tempfile.TemporaryDirectory() as tmp:
118
+ _write_json(tmp, "friend", _doc("friend", 3))
119
+ report = ms.switch_backend("sqlite", tmp)
120
+ self.assertTrue(report["ok"], report)
121
+ self.assertEqual(report["from"], "json")
122
+ self.assertEqual(report["to"], "sqlite")
123
+ self.assertEqual(report["state"], "imported")
124
+ self.assertEqual(report["counts_after"]["entries"], 3)
125
+ self.assertTrue(report["backup"]["dir"])
126
+ self.assertTrue(Path(report["backup"]["dir"]).is_dir())
127
+ self.assertEqual(ms.db_counts(tmp)["entries"], 3)
128
+
129
+ def test_switch_idempotent_both_directions(self):
130
+ with tempfile.TemporaryDirectory() as tmp:
131
+ _write_json(tmp, "friend", _doc("friend", 2))
132
+ _write_json(tmp, "group", _doc("group", 1))
133
+ first = ms.switch_backend("sqlite", tmp)
134
+ second = ms.switch_backend("sqlite", tmp)
135
+ self.assertEqual(first["counts_after"]["entries"], 3)
136
+ self.assertEqual(second["counts_after"]["entries"], 3)
137
+ self.assertEqual(second["state"], "noop")
138
+ back1 = ms.switch_backend("json", tmp)
139
+ self.assertTrue(back1["ok"], back1)
140
+ self.assertEqual(back1["to"], "json")
141
+ self.assertEqual(back1["counts_after"]["entries"], 3)
142
+ back2 = ms.switch_backend("json", tmp)
143
+ self.assertEqual(back2["counts_after"]["entries"], 3)
144
+ self.assertEqual(ms.json_counts(tmp)["entries"], 3)
145
+
146
+ def test_roundtrip_content_preserved(self):
147
+ with tempfile.TemporaryDirectory() as tmp:
148
+ _write_json(tmp, "friend", _doc("friend", 2))
149
+ ms.switch_backend("sqlite", tmp)
150
+ store = ms.open_store(tmp)
151
+ try:
152
+ before = [e["text"] for e in store.load_doc("friend")["entries"]]
153
+ finally:
154
+ store.close()
155
+ ms.switch_backend("json", tmp)
156
+ doc = json.loads((Path(tmp) / "friend.json").read_text(encoding="utf-8"))
157
+ after = [e["text"] for e in doc["entries"]]
158
+ self.assertEqual(before, after)
159
+
160
+ def test_switch_failure_keeps_previous_backend(self):
161
+ with tempfile.TemporaryDirectory() as tmp:
162
+ _write_json(tmp, "friend", _doc("friend", 2))
163
+ plugin = FakePlugin("json")
164
+ original = ms.switch_backend
165
+
166
+ def boom(*args, **kwargs):
167
+ raise RuntimeError("disk full")
168
+
169
+ ms.switch_backend = boom
170
+ try:
171
+ report = HELPERS["_memory_backend_switch"](
172
+ plugin, "sqlite", tmp)
173
+ finally:
174
+ ms.switch_backend = original
175
+ self.assertFalse(report["ok"])
176
+ self.assertEqual(report["backend"], "json")
177
+ self.assertTrue(report.get("fallback"))
178
+ self.assertEqual(plugin._memory_backend, "json")
179
+
180
+
181
+ class InitHelperTest(unittest.TestCase):
182
+ def test_json_setting_reads_json(self):
183
+ with tempfile.TemporaryDirectory() as tmp:
184
+ _write_json(tmp, "friend", _doc("friend", 3))
185
+ plugin = FakePlugin("json")
186
+ report = HELPERS["_init_memory_backend"](plugin, tmp)
187
+ self.assertEqual(report["backend"], "json")
188
+ self.assertEqual(report["entries"], 3)
189
+ self.assertEqual(plugin._memory_backend, "json")
190
+ self.assertIsNone(plugin._memory_store_cache)
191
+
192
+ def test_sqlite_setting_imports_and_logs(self):
193
+ with tempfile.TemporaryDirectory() as tmp:
194
+ _write_json(tmp, "friend", _doc("friend", 2))
195
+ plugin = FakePlugin("sqlite")
196
+ report = HELPERS["_init_memory_backend"](plugin, tmp)
197
+ self.assertEqual(report["backend"], "sqlite")
198
+ self.assertEqual(report["entries"], 2)
199
+ self.assertIsNotNone(plugin._memory_store_cache)
200
+ lines = [" ".join(str(part) for part in row) for row in LOG]
201
+ try:
202
+ self.assertTrue(any(
203
+ "memory backend=%s entries=%d" in line
204
+ and str(line).endswith("sqlite 2") for line in lines),
205
+ lines[-3:])
206
+ finally:
207
+ plugin._memory_store_cache.close()
208
+ plugin._memory_store_cache = None
209
+
210
+ def test_auto_setting_follows_db_presence(self):
211
+ with tempfile.TemporaryDirectory() as tmp:
212
+ _write_json(tmp, "friend", _doc("friend", 1))
213
+ plugin = FakePlugin("auto")
214
+ first = HELPERS["_init_memory_backend"](plugin, tmp)
215
+ self.assertEqual(first["backend"], "json")
216
+ self.assertEqual(first["entries"], 1)
217
+ ms.switch_backend("sqlite", tmp)
218
+ plugin2 = FakePlugin("auto")
219
+ second = HELPERS["_init_memory_backend"](plugin2, tmp)
220
+ self.assertEqual(second["backend"], "sqlite")
221
+ self.assertEqual(second["entries"], 1)
222
+ plugin2._memory_store_cache.close()
223
+
224
+ def test_init_failure_falls_back_to_json(self):
225
+ with tempfile.TemporaryDirectory() as tmp:
226
+ _write_json(tmp, "friend", _doc("friend", 2))
227
+ plugin = FakePlugin("sqlite")
228
+ original = ms.ensure_backend
229
+
230
+ def boom(*args, **kwargs):
231
+ raise RuntimeError("no sqlite")
232
+
233
+ ms.ensure_backend = boom
234
+ try:
235
+ report = HELPERS["_init_memory_backend"](plugin, tmp)
236
+ finally:
237
+ ms.ensure_backend = original
238
+ self.assertEqual(report["backend"], "json")
239
+ self.assertTrue(report["fallback"])
240
+ self.assertEqual(plugin._memory_backend, "json")
241
+ self.assertEqual(report["entries"], 2)
242
+
243
+
244
+ class FallbackConsistencyTest(unittest.TestCase):
245
+ def test_sqlite_open_failure_recounts_json_entries(self):
246
+ with tempfile.TemporaryDirectory() as tmp:
247
+ _write_json(tmp, "friend", _doc("friend", 3))
248
+ plugin = FakePlugin("sqlite")
249
+ original = ms.open_store
250
+
251
+ def boom(*args, **kwargs):
252
+ raise RuntimeError("db handle unavailable")
253
+
254
+ ms.open_store = boom
255
+ try:
256
+ report = HELPERS["_init_memory_backend"](plugin, tmp)
257
+ finally:
258
+ ms.open_store = original
259
+ self.assertEqual(report["backend"], "json")
260
+ self.assertTrue(report["fallback"])
261
+ self.assertEqual(report["entries"], 3) # counted from JSON
262
+ self.assertEqual(plugin._memory_backend, "json")
263
+
264
+ def test_sqlite_write_failure_degrades_to_json(self):
265
+ save, ns = _load_save_method()
266
+
267
+ class BoomStore:
268
+ def save_doc(self, session, doc):
269
+ raise RuntimeError("disk full")
270
+
271
+ with tempfile.TemporaryDirectory() as tmp:
272
+ ns["MEMORY_DIR"] = Path(tmp)
273
+ plugin = types.SimpleNamespace(
274
+ _memory_backend="sqlite", _memory_store_cache=BoomStore())
275
+ path = Path(tmp) / "friend.json"
276
+ doc = {"entries": [{"text": "新写入", "ts": 1.0}]}
277
+ save(plugin, path, doc)
278
+ self.assertTrue(path.is_file())
279
+ saved = json.loads(path.read_text(encoding="utf-8"))
280
+ self.assertEqual(saved["entries"][0]["text"], "新写入")
281
+ self.assertEqual(plugin._memory_backend, "json")
282
+ self.assertIsNone(plugin._memory_store_cache)
283
+
284
+
285
+ class WiringTest(unittest.TestCase):
286
+ def test_config_attr_and_startup_call(self):
287
+ self.assertIn('self.config.get("memory_storage_backend", "auto")',
288
+ SOURCE)
289
+ self.assertIn("_init_memory_backend(", SOURCE)
290
+ self.assertIn("self._memory_backend = \"json\"", SOURCE)
291
+
292
+ def test_handler_route_and_log_format(self):
293
+ self.assertIn("async def page_memory_backend", SOURCE)
294
+ self.assertIn("/astrbot_plugin_biocore/page/memory_backend", SOURCE)
295
+ self.assertIn("[biocore] memory backend=%s entries=%d", SOURCE)
296
+ self.assertIn("_memory_backend_switch(self, target, MEMORY_DIR", SOURCE)
297
+
298
+ def test_storage_helpers_wired_into_memory_paths(self):
299
+ for marker in ("_memory_store_handle(self, MEMORY_DIR)",
300
+ "store.save_doc(session, doc)",
301
+ "store.load_doc(safe)",
302
+ "store.load_doc(Path(path).stem)",
303
+ "store.stats()",
304
+ "store.all_docs()"):
305
+ self.assertIn(marker, SOURCE)
306
+
307
+ def test_status_payload_exposes_backend(self):
308
+ self.assertIn('"memory_storage_backend":', SOURCE)
309
+ self.assertIn('"memory_backend":', SOURCE)
310
+ routes = (ROOT / "biocore_model" / "plugin_routes.py").read_text(
311
+ encoding="utf-8")
312
+ self.assertIn('"memory_storage_backend":', routes)
313
+
314
+ def test_schema_entry(self):
315
+ cfg = SCHEMA.get("memory_storage_backend") or {}
316
+ self.assertEqual(cfg.get("type"), "string")
317
+ self.assertEqual(cfg.get("default"), "auto")
318
+ self.assertEqual(cfg.get("options"), ["json", "sqlite", "auto"])
319
+
320
+ def test_panel_dropdown(self):
321
+ self.assertIn('id="memoryStorageBackend"', SRC_HTML)
322
+ for label in ("JSON 文件", "SQLite 数据库", "自动"):
323
+ self.assertIn(label, SRC_HTML)
324
+ self.assertIn("page/memory_backend", APP_JS)
325
+ self.assertIn("renderMemoryBackend", APP_JS)
326
+ # the inlined dashboard ships the same control
327
+ self.assertIn('id="memoryStorageBackend"', INDEX_HTML)
328
+ self.assertIn("MEM_BACKEND_LABELS", INDEX_HTML)
329
+
330
+
331
+ if __name__ == "__main__":
332
+ unittest.main()