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,215 @@
1
+ # -*- coding: utf-8 -*-
2
+ """page_status autonomy: fake plugin variants ensure data.autonomy.state carries V/theta.
3
+
4
+ Regression for the live bug: when self.data_dir is missing at page_status time,
5
+ ui_settings.base_dir fell back to an old directory whose accumulator had None
6
+ values, overwriting the real state. The fix resolves the authority dir from
7
+ _data_dir/DATA_DIR and merges the accumulator outside the state try-block.
8
+ """
9
+ import asyncio
10
+ import json
11
+ import tempfile
12
+ import unittest
13
+ from pathlib import Path
14
+
15
+ from biocore_model import plugin_routes as routes
16
+ from biocore_model import ui_settings
17
+
18
+
19
+ class FakeCore:
20
+ def snapshot(self): return {"v": 0.0, "a": 0.4}
21
+ def intent(self): return {}
22
+ def load_state(self, state): return None
23
+ def autonomy_state(self, relationship=None, now=None, persist=True):
24
+ return {"enabled": True, "ready": False, "V": 0.71, "theta": 0.60}
25
+
26
+
27
+ class FakePlugin(routes.RoutesMixin):
28
+ def __init__(self, data_dir, *, with_data_dir=True, state_attr="method",
29
+ state_raises=False):
30
+ self._tmp = data_dir
31
+ if with_data_dir:
32
+ self.data_dir = str(data_dir)
33
+ self._data_dir = data_dir
34
+ self.context = type("Ctx", (), {"persona_manager": None})()
35
+ self.cores = {} # empty cores
36
+ self.profile = {}
37
+ self.persona_text = ""
38
+ self.self_model = {}
39
+ self.enabled = True
40
+ self.inject_intent = True
41
+ self.proactive_reply = False
42
+ self.proactive_autonomy = False
43
+ self.proactive_send = False
44
+ self.memory_dedup_embedding = True
45
+ self._migration_note = ""
46
+ if state_attr == "method":
47
+ if state_raises:
48
+ def _state():
49
+ raise RuntimeError("state source down")
50
+ else:
51
+ def _state():
52
+ return {"source": "method"}
53
+ self._autonomy_readonly_state = _state
54
+ self._relationship_of = lambda data=None: {}
55
+ self._working_set_preview = lambda key: {}
56
+ self._ensure_autonomy_task = lambda: None
57
+ self._functions_public = lambda: {}
58
+ self._llm_provider_options = lambda: []
59
+ self._public_api_config = lambda: {}
60
+ self._memory_stats = lambda: {}
61
+ self._decayed_merge_count = lambda *a, **k: 0
62
+ self._event_archive = lambda *a, **k: []
63
+ self._insights = lambda *a, **k: []
64
+ self._astrbot_llm_default = lambda: None
65
+ self._astrbot_embedding_default = lambda: None
66
+ self._astrbot_rerank_default = lambda: None
67
+ self._astrbot_tts_default = lambda: None
68
+ self._astrbot_image_default = lambda: None
69
+ async def _kb_available(): return False
70
+ self._kb_available = _kb_available
71
+
72
+
73
+ class PageStatusAutonomyTest(unittest.TestCase):
74
+ def setUp(self):
75
+ self._tmp = tempfile.TemporaryDirectory()
76
+ self.tmp = Path(self._tmp.name)
77
+ self.authority = self.tmp / "plugin_data" / "astrbot_plugin_biocore"
78
+ self.authority.mkdir(parents=True, exist_ok=True)
79
+ (self.authority / "ui_settings.json").write_text(json.dumps({
80
+ "autonomy": {"accumulator": {"V": 0.2838, "theta": 0.55,
81
+ "ticks_since_fire": 3}},
82
+ "schema": 1}, ensure_ascii=False), encoding="utf-8")
83
+ routes.DATA_DIR = self.authority
84
+ routes.MEMORY_DIR = self.tmp / "memory"
85
+ routes.STATE_DIR = self.tmp / "states"
86
+ routes.USER_DIR = self.tmp / "users"
87
+ routes.DEFAULT_PROFILE = {}
88
+ routes.DEFAULT_MEMORY_TRACE = {}
89
+ routes._memory_index_rows = lambda *a, **k: []
90
+ routes._relationship_rows = lambda *a, **k: []
91
+ routes.LLM_FUNCTIONS = ()
92
+ routes.LLM_FUNCTION_DEFAULTS = {}
93
+ for folder in (routes.MEMORY_DIR, routes.STATE_DIR, routes.USER_DIR):
94
+ folder.mkdir(parents=True, exist_ok=True)
95
+ # isolate from the real instance layout for attr-based tests
96
+ self._orig_module_file = routes._MODULE_FILE
97
+ routes._MODULE_FILE = (self.tmp / "no_instance" / "plugins" / "x"
98
+ / "biocore_model" / "plugin_routes.py")
99
+
100
+ def tearDown(self):
101
+ routes._MODULE_FILE = self._orig_module_file
102
+ self._tmp.cleanup()
103
+
104
+ def _run(self, plugin):
105
+ out = asyncio.run(plugin.page_status())
106
+ self.assertEqual(out.get("status"), "ok")
107
+ return out["data"]["autonomy"]
108
+
109
+ def test_data_dir_set_and_state_attr_is_method(self):
110
+ plugin = FakePlugin(self.authority, with_data_dir=True, state_attr="method")
111
+ path = routes._authority_settings_path(plugin)
112
+ self.assertEqual(Path(str(path)), self.authority / "ui_settings.json")
113
+ automation = self._run(plugin)
114
+ self.assertEqual(set(automation), {"enabled", "send", "state", "_debug"})
115
+ state = automation["state"]
116
+ self.assertEqual(state["V"], 0.2838)
117
+ self.assertEqual(state["theta"], 0.55)
118
+ self.assertEqual(state["ticks_since_fire"], 3)
119
+
120
+ def test_data_dir_missing_falls_back_to_authority(self):
121
+ plugin = FakePlugin(self.authority, with_data_dir=False, state_attr="method")
122
+ path = routes._authority_settings_path(plugin)
123
+ self.assertEqual(Path(str(path)), self.authority / "ui_settings.json")
124
+ state = self._run(plugin)["state"]
125
+ self.assertEqual(state["V"], 0.2838)
126
+ self.assertEqual(state["theta"], 0.55)
127
+
128
+ def test_stale_data_dir_reads_injected_authority(self):
129
+ stale = self.tmp / "stale_A"
130
+ stale.mkdir(parents=True, exist_ok=True)
131
+ (stale / "ui_settings.json").write_text(json.dumps({
132
+ "autonomy": {"accumulator": {"V": None, "theta": None,
133
+ "ticks_since_fire": None}}},
134
+ ensure_ascii=False), encoding="utf-8")
135
+ plugin = FakePlugin(self.authority, with_data_dir=False, state_attr="method")
136
+ plugin.data_dir = str(stale) # stale data_dir must not win
137
+ state = self._run(plugin)["state"]
138
+ self.assertEqual(state["V"], 0.2838)
139
+ self.assertEqual(state["theta"], 0.55)
140
+ self.assertIsNotNone(state["ticks_since_fire"])
141
+
142
+ def test_state_source_failure_still_merges_accumulator(self):
143
+ plugin = FakePlugin(self.authority, with_data_dir=False, state_attr="method",
144
+ state_raises=True)
145
+ state = self._run(plugin)["state"]
146
+ self.assertEqual(state["V"], 0.2838)
147
+ self.assertEqual(state["theta"], 0.55)
148
+
149
+ def test_authority_file_survives_old_accumulator_none(self):
150
+ old = self.tmp / "old_A"
151
+ old.mkdir(parents=True, exist_ok=True)
152
+ (old / "ui_settings.json").write_text(json.dumps({
153
+ "autonomy": {"accumulator": {"V": None, "theta": None,
154
+ "ticks_since_fire": None}}},
155
+ ensure_ascii=False), encoding="utf-8")
156
+ plugin = FakePlugin(self.authority, with_data_dir=False, state_attr="method")
157
+ state = self._run(plugin)["state"]
158
+ self.assertEqual(state["V"], 0.2838)
159
+ self.assertIsNotNone(state["theta"])
160
+
161
+
162
+ def test_module_candidates_follow_instance_layout(self):
163
+ data_root = self.tmp / "inst" / "core" / "data"
164
+ mod_file = (data_root / "plugins" / "astrbot_plugin_biocore_standalone"
165
+ / "biocore_model" / "plugin_routes.py")
166
+ routes._MODULE_FILE = mod_file
167
+ candidates = routes._module_candidate_paths()
168
+ self.assertEqual(Path(str(candidates[0])).resolve(),
169
+ (data_root / "plugin_data" / "astrbot_plugin_biocore"
170
+ / "ui_settings.json").resolve())
171
+ self.assertEqual(Path(str(candidates[1])).resolve(),
172
+ (data_root / "plugins" / "astrbot_plugin_biocore_standalone"
173
+ / "data" / "ui_settings.json").resolve())
174
+
175
+ def test_real_instance_authority_file(self):
176
+ authority = None
177
+ base = Path.home() / ".astrbot_launcher" / "instances"
178
+ for candidate in sorted(base.glob(
179
+ "*/core/data/plugin_data/astrbot_plugin_biocore/ui_settings.json")):
180
+ if candidate.is_file():
181
+ authority = candidate
182
+ break
183
+ if authority is None:
184
+ self.skipTest("no real AstrBot instance ui_settings.json")
185
+ data_root = authority.parents[2] # .../core/data
186
+ routes._MODULE_FILE = (data_root / "plugins" / "astrbot_plugin_biocore_standalone"
187
+ / "biocore_model" / "plugin_routes.py")
188
+ candidates = routes._module_candidate_paths()
189
+ self.assertEqual(candidates[0], authority)
190
+ self.assertTrue(candidates[0].exists())
191
+ acc = routes._read_accumulator(candidates[0])
192
+ self.assertTrue(acc)
193
+ self.assertTrue(any(value is not None for value in acc.values()))
194
+
195
+ def test_debug_field_reports_used_candidate(self):
196
+ data_root = self.tmp / "inst_debug" / "core" / "data"
197
+ authority = data_root / "plugin_data" / "astrbot_plugin_biocore"
198
+ authority.mkdir(parents=True, exist_ok=True)
199
+ (authority / "ui_settings.json").write_text(json.dumps({
200
+ "autonomy": {"accumulator": {"V": 0.2838, "theta": 0.55}},
201
+ "schema": 1}, ensure_ascii=False), encoding="utf-8")
202
+ routes._MODULE_FILE = (data_root / "plugins" / "x"
203
+ / "biocore_model" / "plugin_routes.py")
204
+ plugin = FakePlugin(self.authority, with_data_dir=False, state_attr="method")
205
+ automation = self._run(plugin)
206
+ self.assertEqual(automation["state"]["V"], 0.2838)
207
+ debug = automation["_debug"]
208
+ self.assertEqual(debug["used"], debug["candidates"][0])
209
+ self.assertTrue(debug["exists"][0])
210
+ self.assertIn("V", debug["acc_keys"])
211
+ self.assertIn("theta", debug["acc_keys"])
212
+
213
+
214
+ if __name__ == "__main__":
215
+ unittest.main()
@@ -0,0 +1,345 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import pipeline as pl
4
+
5
+
6
+ def stage(name, **kw):
7
+ kw.setdefault("run", lambda ctx: {name: True})
8
+ return pl.Stage(name, **kw)
9
+
10
+
11
+ class PipelineTest(unittest.TestCase):
12
+ def test_order(self):
13
+ order = []
14
+ p = pl.Pipeline([stage("a", run=lambda c: order.append("a") or {}), stage("b", run=lambda c: order.append("b") or {})])
15
+ p.run({})
16
+ self.assertEqual(order, ["a", "b"])
17
+ def test_skip_disabled(self):
18
+ p = pl.Pipeline([stage("a"), stage("b")])
19
+ p.set_enabled("a", False)
20
+ ctx = p.run({})
21
+ self.assertEqual(ctx["trace"]["stages"][0]["status"], "skipped")
22
+ def test_required_cannot_disable(self):
23
+ p = pl.Pipeline([stage("perceive")])
24
+ with self.assertRaises(ValueError):
25
+ p.set_enabled("perceive", False)
26
+ def test_exception_continues(self):
27
+ p = pl.Pipeline([stage("a", run=lambda c: 1 / 0), stage("b")])
28
+ ctx = p.run({})
29
+ self.assertEqual(ctx["trace"]["stages"][0]["status"], "failed")
30
+ self.assertEqual(ctx["trace"]["stages"][1]["status"], "ok")
31
+ def test_required_failure_aborts(self):
32
+ p = pl.Pipeline([stage("think", required=True, run=lambda c: 1 / 0)])
33
+ with self.assertRaises(pl.PipelineAbort):
34
+ p.run({})
35
+ def test_timeout_skipped(self):
36
+ import time
37
+ p = pl.Pipeline([stage("slow", timeout_ms=50, run=lambda c: time.sleep(0.2)), stage("b")])
38
+ ctx = p.run({})
39
+ self.assertEqual(ctx["trace"]["stages"][0]["status"], "timeout")
40
+ self.assertEqual(ctx["trace"]["stages"][1]["status"], "ok")
41
+ def test_required_timeout_aborts(self):
42
+ import time
43
+ p = pl.Pipeline([stage("express", timeout_ms=30, run=lambda c: time.sleep(0.15))])
44
+ with self.assertRaises(pl.PipelineAbort):
45
+ p.run({})
46
+ def test_on_error_abort(self):
47
+ p = pl.Pipeline([stage("a", on_error="abort", run=lambda c: 1 / 0)])
48
+ with self.assertRaises(pl.PipelineAbort):
49
+ p.run({})
50
+ def test_trace_fields(self):
51
+ p = pl.Pipeline([stage("a")])
52
+ ctx = p.run({})
53
+ record = ctx["trace"]["stages"][0]
54
+ for key in ("name", "ms", "status", "note"):
55
+ self.assertIn(key, record)
56
+ def test_ring_limit_50(self):
57
+ p = pl.Pipeline([stage("a")])
58
+ for _ in range(60):
59
+ p.run({})
60
+ self.assertEqual(len(p._ring), 50)
61
+ def test_get_status_fields(self):
62
+ p = pl.Pipeline([stage("a"), stage("b")])
63
+ status = p.get_status()
64
+ row = status["stages"][0]
65
+ for key in ("name", "required", "enabled", "last_status", "last_ms"):
66
+ self.assertIn(key, row)
67
+ def test_get_status_after_run(self):
68
+ p = pl.Pipeline([stage("a")])
69
+ p.run({})
70
+ status = p.get_status()
71
+ self.assertEqual(status["stages"][0]["last_status"], "ok")
72
+ self.assertIn("stages", status["last_trace"])
73
+ def test_each_non_required_disabled_still_valid(self):
74
+ names = ["a", "b", "c"]
75
+ for name in names:
76
+ p = pl.Pipeline([stage(n) for n in names])
77
+ p.set_enabled(name, False)
78
+ ctx = p.run({})
79
+ self.assertIn("trace", ctx)
80
+ def test_unknown_set_enabled(self):
81
+ p = pl.Pipeline([stage("a")])
82
+ with self.assertRaises(KeyError):
83
+ p.set_enabled("nope", False)
84
+ def test_ctx_updated_by_stage(self):
85
+ p = pl.Pipeline([stage("a", run=lambda c: {"value": 7})])
86
+ self.assertEqual(p.run({})["value"], 7)
87
+ def test_ctx_input_preserved(self):
88
+ p = pl.Pipeline([stage("a")])
89
+ ctx = p.run({"event": "m1"})
90
+ self.assertEqual(ctx["event"], "m1")
91
+ def test_default_stages_all_required(self):
92
+ stages = pl.default_stages()
93
+ self.assertEqual([s.name for s in stages], ["perceive", "think", "express", "present"])
94
+ self.assertTrue(all(s.required for s in stages))
95
+ def test_no_main_import(self):
96
+ import pathlib
97
+ source = pathlib.Path(__file__).resolve().parent.parent.joinpath("pipeline.py").read_text(encoding="utf-8")
98
+ self.assertNotIn("import main", source)
99
+
100
+
101
+ class FakeClock:
102
+ """Injected clock: deterministic seconds, advanced explicitly by a stage."""
103
+
104
+ def __init__(self):
105
+ self.now = 0.0
106
+
107
+ def __call__(self):
108
+ return self.now
109
+
110
+ def advance(self, seconds):
111
+ self.now += float(seconds)
112
+ return self.now
113
+
114
+
115
+ class PipelineContractTest(unittest.TestCase):
116
+ def test_ctx_contract_fields(self):
117
+ ctx = pl.make_ctx()
118
+ self.assertEqual(set(ctx.keys()), set(pl.CTX_FIELDS))
119
+ self.assertEqual(len(pl.CTX_FIELDS), 16)
120
+ self.assertEqual(ctx["trace"]["stages"], [])
121
+
122
+ def test_run_fills_all_ctx_fields(self):
123
+ p = pl.Pipeline([stage("a")])
124
+ ctx = p.run({"event": {"text": "hi"}})
125
+ for field in pl.CTX_FIELDS:
126
+ self.assertIn(field, ctx)
127
+ self.assertEqual(ctx["event"], {"text": "hi"})
128
+
129
+ def test_run_does_not_mutate_input(self):
130
+ p = pl.Pipeline([stage("a")])
131
+ given = {"event": "m1"}
132
+ p.run(given)
133
+ self.assertEqual(given, {"event": "m1"})
134
+
135
+ def test_register_appends(self):
136
+ p = pl.Pipeline([stage("a")])
137
+ p.register(stage("b"))
138
+ self.assertEqual(p.names(), ["a", "b"])
139
+
140
+ def test_register_accepts_dict(self):
141
+ p = pl.Pipeline()
142
+ p.register({"name": "a", "run": lambda ctx: None})
143
+ self.assertEqual(p.names(), ["a"])
144
+
145
+ def test_register_duplicate_raises(self):
146
+ p = pl.Pipeline([stage("a")])
147
+ with self.assertRaises(ValueError):
148
+ p.register(stage("a"))
149
+
150
+ def test_register_bad_type(self):
151
+ p = pl.Pipeline()
152
+ with self.assertRaises(TypeError):
153
+ p.register("nope")
154
+
155
+ def test_stage_rejects_empty_name(self):
156
+ with self.assertRaises(ValueError):
157
+ pl.Stage("", lambda ctx: None)
158
+
159
+ def test_stage_rejects_non_callable_run(self):
160
+ with self.assertRaises(TypeError):
161
+ pl.Stage("a", "not-callable")
162
+
163
+ def test_stage_rejects_negative_timeout(self):
164
+ with self.assertRaises(ValueError):
165
+ pl.Stage("a", lambda ctx: None, timeout_ms=-1)
166
+
167
+ def test_invalid_on_error_normalized(self):
168
+ st = pl.Stage("a", lambda ctx: None, on_error="explode")
169
+ self.assertEqual(st.on_error, "skip")
170
+
171
+ def test_start_by_name(self):
172
+ order = []
173
+ p = pl.Pipeline([stage("a", run=lambda c: order.append("a") or {}),
174
+ stage("b", run=lambda c: order.append("b") or {}),
175
+ stage("c", run=lambda c: order.append("c") or {})])
176
+ ctx = p.run({}, start="b")
177
+ self.assertEqual(order, ["b", "c"])
178
+ self.assertEqual([row["status"] for row in ctx["trace"]["stages"]],
179
+ ["skipped", "ok", "ok"])
180
+ self.assertEqual(ctx["trace"]["stages"][0]["note"], "before_start")
181
+
182
+ def test_start_by_index(self):
183
+ order = []
184
+ p = pl.Pipeline([stage("a", run=lambda c: order.append("a") or {}),
185
+ stage("b", run=lambda c: order.append("b") or {})])
186
+ p.run({}, start=1)
187
+ self.assertEqual(order, ["b"])
188
+
189
+ def test_start_unknown_raises(self):
190
+ p = pl.Pipeline([stage("a")])
191
+ with self.assertRaises(KeyError):
192
+ p.run({}, start="nope")
193
+
194
+ def test_start_out_of_range(self):
195
+ p = pl.Pipeline([stage("a")])
196
+ with self.assertRaises(IndexError):
197
+ p.run({}, start=5)
198
+
199
+ def test_status_enabled_reflects_toggle(self):
200
+ p = pl.Pipeline([stage("a"), stage("b")])
201
+ p.set_enabled("a", False)
202
+ rows = {row["name"]: row for row in p.get_status()["stages"]}
203
+ self.assertFalse(rows["a"]["enabled"])
204
+ self.assertTrue(rows["b"]["enabled"])
205
+ p.set_enabled("a", True)
206
+ rows = {row["name"]: row for row in p.get_status()["stages"]}
207
+ self.assertTrue(rows["a"]["enabled"])
208
+
209
+ def test_all_required_stages_cannot_disable(self):
210
+ for name in pl.REQUIRED_STAGES:
211
+ p = pl.Pipeline([stage(name)])
212
+ with self.assertRaises(ValueError):
213
+ p.set_enabled(name, False)
214
+ self.assertTrue(p.set_enabled(name, True))
215
+
216
+ def test_fallback_used_on_error(self):
217
+ p = pl.Pipeline([
218
+ pl.Stage("a", lambda c: 1 / 0, on_error="fallback",
219
+ fallback=lambda c: {"reply": "fb"}),
220
+ stage("b")])
221
+ ctx = p.run({})
222
+ first = ctx["trace"]["stages"][0]
223
+ self.assertEqual(first["status"], "failed")
224
+ self.assertIn("fallback:ok", first["note"])
225
+ self.assertEqual(ctx["reply"], "fb")
226
+ self.assertEqual(ctx["trace"]["stages"][1]["status"], "ok")
227
+
228
+ def test_fallback_failure_keeps_note(self):
229
+ def bad_fallback(ctx):
230
+ raise ValueError("boom")
231
+ p = pl.Pipeline([pl.Stage("a", lambda c: 1 / 0, on_error="fallback",
232
+ fallback=bad_fallback)])
233
+ ctx = p.run({})
234
+ self.assertIn("fallback:ValueError", ctx["trace"]["stages"][0]["note"])
235
+
236
+ def test_on_error_abort_non_required(self):
237
+ p = pl.Pipeline([pl.Stage("a", lambda c: 1 / 0, on_error="abort")])
238
+ with self.assertRaises(pl.PipelineAbort):
239
+ p.run({})
240
+
241
+ def test_clock_injected_ms_is_deterministic(self):
242
+ clock = FakeClock()
243
+
244
+ def run(ctx):
245
+ clock.advance(0.012)
246
+ return {}
247
+
248
+ p = pl.Pipeline([pl.Stage("a", run, timeout_ms=1000)], clock=clock)
249
+ ctx = p.run({})
250
+ self.assertEqual(ctx["trace"]["stages"][0]["ms"], 12.0)
251
+ self.assertEqual(p.get_status()["stages"][0]["last_ms"], 12.0)
252
+
253
+ def test_timeout_with_injected_clock(self):
254
+ clock = FakeClock()
255
+
256
+ def run(ctx):
257
+ clock.advance(0.2)
258
+ return {}
259
+
260
+ p = pl.Pipeline([pl.Stage("a", run, timeout_ms=50), stage("b")], clock=clock)
261
+ ctx = p.run({})
262
+ self.assertEqual(ctx["trace"]["stages"][0]["status"], "timeout")
263
+ self.assertEqual(ctx["trace"]["stages"][1]["status"], "ok")
264
+
265
+ def test_zero_timeout_never_times_out(self):
266
+ clock = FakeClock()
267
+
268
+ def run(ctx):
269
+ clock.advance(5.0)
270
+ return {}
271
+
272
+ p = pl.Pipeline([pl.Stage("a", run, timeout_ms=0)], clock=clock)
273
+ ctx = p.run({})
274
+ self.assertEqual(ctx["trace"]["stages"][0]["status"], "ok")
275
+
276
+ def test_explicit_timeout_error_maps_to_timeout(self):
277
+ def run(ctx):
278
+ raise TimeoutError("stage self-report")
279
+
280
+ p = pl.Pipeline([pl.Stage("a", run, timeout_ms=0)])
281
+ ctx = p.run({})
282
+ self.assertEqual(ctx["trace"]["stages"][0]["status"], "timeout")
283
+
284
+ def test_ring_keeps_last_50_in_order(self):
285
+ p = pl.Pipeline([stage("a"), stage("b")])
286
+ for _ in range(30):
287
+ p.run({})
288
+ self.assertEqual(len(p._ring), 50)
289
+ self.assertEqual(p._ring[-1]["name"], "b")
290
+ self.assertEqual(p._ring[-2]["name"], "a")
291
+
292
+ def test_abort_attaches_trace_and_updates_status(self):
293
+ p = pl.Pipeline([pl.Stage("perceive", lambda c: 1 / 0), stage("b")])
294
+ try:
295
+ p.run({})
296
+ except pl.PipelineAbort as exc:
297
+ self.assertEqual(exc.stage_name, "perceive")
298
+ self.assertEqual(exc.trace["stages"][0]["status"], "failed")
299
+ else:
300
+ self.fail("PipelineAbort not raised")
301
+ status = p.get_status()
302
+ self.assertEqual(status["stages"][0]["last_status"], "failed")
303
+ self.assertEqual(status["last_trace"]["stages"][0]["status"], "failed")
304
+
305
+ def test_disable_each_non_required_stage_still_valid(self):
306
+ for name in ("a", "b", "c"):
307
+ p = pl.Pipeline([stage("a"), stage("b"), stage("c")])
308
+ p.set_enabled(name, False)
309
+ ctx = p.run({})
310
+ rows = {row["name"]: row for row in ctx["trace"]["stages"]}
311
+ self.assertEqual(rows[name]["status"], "skipped")
312
+ self.assertEqual(rows[name]["note"], "disabled")
313
+ self.assertEqual(sum(1 for row in rows.values() if row["status"] == "ok"), 2)
314
+ for field in pl.CTX_FIELDS:
315
+ self.assertIn(field, ctx)
316
+
317
+ def test_skip_note_and_ms(self):
318
+ p = pl.Pipeline([stage("a")])
319
+ p.set_enabled("a", False)
320
+ ctx = p.run({})
321
+ row = ctx["trace"]["stages"][0]
322
+ self.assertEqual(row["ms"], 0.0)
323
+ self.assertEqual(p.get_status()["stages"][0]["last_status"], "skipped")
324
+
325
+ def test_default_stages_run_produces_reply(self):
326
+ p = pl.Pipeline(pl.default_stages())
327
+ ctx = p.run(pl.make_ctx(event={"text": "hi"}))
328
+ self.assertTrue(ctx["reply"])
329
+ self.assertEqual([row["status"] for row in ctx["trace"]["stages"]],
330
+ ["ok", "ok", "ok", "ok"])
331
+
332
+ def test_status_fields_and_trace_contract(self):
333
+ p = pl.Pipeline([stage("a")])
334
+ ctx = p.run({})
335
+ row = ctx["trace"]["stages"][0]
336
+ self.assertEqual(set(row.keys()), {"name", "ms", "status", "note"})
337
+ status = p.get_status()
338
+ self.assertEqual(set(status.keys()), {"stages", "last_trace"})
339
+ self.assertEqual(set(status["stages"][0].keys()),
340
+ {"name", "required", "enabled", "last_status", "last_ms"})
341
+ self.assertIn("ring", status["last_trace"])
342
+
343
+
344
+ if __name__ == "__main__":
345
+ unittest.main()