dsh-biocore 0.2.0 → 0.3.1

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,192 @@
1
+ # -*- coding: utf-8 -*-
2
+ """PIPE-3 pipeline API tests: status, toggle, persistence, dry-run, routes."""
3
+ import json
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest import mock
8
+
9
+ from biocore_model import pipeline_api as pa
10
+ from biocore_model import runtime as rt
11
+
12
+
13
+ class FakePlugin:
14
+ def __init__(self, data_dir, pipeline_mode="shadow"):
15
+ self.data_dir = str(data_dir)
16
+ self.pipeline_mode = pipeline_mode
17
+
18
+
19
+ class Base(unittest.TestCase):
20
+ def setUp(self):
21
+ self._tmp = tempfile.TemporaryDirectory()
22
+ self.tmp = Path(self._tmp.name)
23
+ self.plugin = FakePlugin(self.tmp)
24
+ rt.reset_pipeline()
25
+ rt.apply_switches({})
26
+ pa.reset_boot()
27
+
28
+ def tearDown(self):
29
+ rt.apply_switches({})
30
+ rt.reset_pipeline()
31
+ pa.reset_boot()
32
+ self._tmp.cleanup()
33
+
34
+ def settings(self):
35
+ return json.loads((self.tmp / "ui_settings.json").read_text(encoding="utf-8"))
36
+
37
+
38
+ class StatusTest(Base):
39
+ def test_status_payload_structure(self):
40
+ payload = pa.payload_status(self.plugin)
41
+ for key in ("mode", "stages", "last_trace", "updated_ts", "schema", "switches"):
42
+ self.assertIn(key, payload)
43
+ self.assertEqual(payload["mode"], "shadow")
44
+ self.assertEqual(len(payload["stages"]), 11)
45
+ for row in payload["stages"]:
46
+ self.assertEqual(set(row), {"name", "required", "enabled",
47
+ "last_status", "last_ms"})
48
+
49
+ def test_status_stage_order_matches_runtime(self):
50
+ payload = pa.payload_status(self.plugin)
51
+ self.assertEqual([row["name"] for row in payload["stages"]],
52
+ list(rt.get_pipeline().names()))
53
+
54
+ def test_status_updated_ts_and_schema(self):
55
+ payload = pa.payload_status(self.plugin)
56
+ self.assertIsInstance(payload["updated_ts"], float)
57
+ self.assertEqual(payload["schema"], pa.SCHEMA)
58
+
59
+ def test_status_reflects_applied_switches(self):
60
+ rt.apply_switches({"memory": False})
61
+ payload = pa.payload_status(self.plugin)
62
+ rows = {row["name"]: row for row in payload["stages"]}
63
+ self.assertFalse(rows["memory"]["enabled"])
64
+ self.assertFalse(payload["switches"]["memory"])
65
+
66
+
67
+ class ToggleTest(Base):
68
+ def test_toggle_non_required_ok(self):
69
+ result = pa.payload_toggle(self.plugin, "expr_intent", False)
70
+ self.assertTrue(result["ok"])
71
+ self.assertTrue(result["saved"])
72
+ rows = {row["name"]: row for row in result["stages"]}
73
+ self.assertFalse(rows["expr_intent"]["enabled"])
74
+
75
+ def test_toggle_required_rejected(self):
76
+ for name in pa.REQUIRED_STAGES:
77
+ result = pa.payload_toggle(self.plugin, name, False)
78
+ self.assertFalse(result["ok"], name)
79
+ self.assertEqual(result["message"], "required_stage")
80
+ rows = {row["name"]: row for row in result["stages"]}
81
+ self.assertTrue(rows[name]["enabled"])
82
+
83
+ def test_toggle_think_rejected(self):
84
+ result = pa.payload_toggle(self.plugin, "think", False)
85
+ self.assertFalse(result["ok"])
86
+ self.assertEqual(result["message"], "required_stage")
87
+
88
+ def test_toggle_unknown_stage(self):
89
+ result = pa.payload_toggle(self.plugin, "nope", True)
90
+ self.assertFalse(result["ok"])
91
+ self.assertEqual(result["message"], "unknown_stage")
92
+
93
+ def test_toggle_name_required(self):
94
+ result = pa.payload_toggle(self.plugin, "", True)
95
+ self.assertFalse(result["ok"])
96
+ self.assertEqual(result["message"], "name_required")
97
+
98
+ def test_toggle_persists_to_ui_settings(self):
99
+ pa.payload_toggle(self.plugin, "expr_tone", False)
100
+ data = self.settings()
101
+ stages = data["autonomy"]["pipeline_stages"]
102
+ self.assertFalse(stages["expr_tone"])
103
+ self.assertEqual(data["schema"], 1)
104
+
105
+ def test_persistence_reload(self):
106
+ pa.payload_toggle(self.plugin, "memory", False)
107
+ pa.reset_boot()
108
+ rt.reset_pipeline()
109
+ other = FakePlugin(self.tmp)
110
+ payload = pa.payload_status(other)
111
+ rows = {row["name"]: row for row in payload["stages"]}
112
+ self.assertFalse(rows["memory"]["enabled"])
113
+
114
+ def test_switches_roundtrip(self):
115
+ saved = pa.save_switches(self.plugin, {"memory": False, "bio": True})
116
+ self.assertTrue(saved["ok"])
117
+ self.assertEqual(pa.load_switches(self.plugin),
118
+ {"memory": False, "bio": True})
119
+
120
+ def test_boot_is_idempotent(self):
121
+ pa.reset_boot()
122
+ self.assertTrue(pa.boot(self.plugin))
123
+ self.assertFalse(pa.boot(self.plugin))
124
+
125
+
126
+ class DryrunTest(Base):
127
+ def test_dryrun_structure(self):
128
+ result = pa.payload_dryrun(self.plugin, "今天有点累", now=1000.0)
129
+ self.assertTrue(result["ok"])
130
+ self.assertEqual(result["mode"], "dryrun")
131
+ self.assertEqual(result["stages"], 11)
132
+ self.assertEqual(len(result["trace"]["stages"]), 11)
133
+ self.assertTrue(result["out_preview"])
134
+ self.assertIsNone(result["error"])
135
+
136
+ def test_dryrun_empty_text(self):
137
+ result = pa.payload_dryrun(self.plugin, " ")
138
+ self.assertFalse(result["ok"])
139
+ self.assertEqual(result["message"], "sample_text_required")
140
+
141
+ def test_dryrun_has_no_side_effects(self):
142
+ captured = {}
143
+
144
+ def fake_make_ctx(**kwargs):
145
+ captured.update(kwargs)
146
+ return {"meta": {"dryrun": True, "write_core": False,
147
+ "observe": False, "llm": None, "now": 1000.0},
148
+ "text": kwargs.get("text") or "", "trace": {"stages": []}}
149
+
150
+ with mock.patch.object(rt, "make_request_ctx", side_effect=fake_make_ctx), \
151
+ mock.patch.object(rt, "run_reply", return_value={
152
+ "trace": {"stages": []}, "reply": "", "state_text": "状态块",
153
+ "meta": {"dryrun": True}}):
154
+ result = pa.payload_dryrun(self.plugin, "样例", now=1000.0)
155
+ self.assertTrue(result["ok"])
156
+ self.assertIsNone(captured.get("core"))
157
+ meta = captured.get("meta")
158
+ self.assertTrue(meta["dryrun"])
159
+ self.assertFalse(meta["write_core"])
160
+ self.assertFalse(meta["observe"])
161
+ self.assertIsNone(meta["llm"])
162
+ self.assertFalse((self.tmp / "ui_settings.json").exists())
163
+
164
+ def test_dryrun_does_not_create_settings_file(self):
165
+ pa.payload_dryrun(self.plugin, "你好", now=5.0)
166
+ self.assertFalse((self.tmp / "ui_settings.json").exists())
167
+
168
+
169
+ class RouteTest(unittest.TestCase):
170
+ def test_main_routes_and_auth_present(self):
171
+ root = Path(__file__).resolve().parent.parent.parent
172
+ source = (root / "main.py").read_text(encoding="utf-8")
173
+ for path in ('/page/pipeline"', '/page/pipeline_toggle"',
174
+ '/page/pipeline_dryrun"'):
175
+ self.assertIn(path, source)
176
+ self.assertIn("self.page_pipeline, [\"GET\"]", source)
177
+ self.assertIn("self.page_pipeline_toggle, [\"POST\"]", source)
178
+ self.assertIn("self.page_pipeline_dryrun, [\"POST\"]", source)
179
+ self.assertGreaterEqual(source.count("_pa.payload_dryrun"), 1)
180
+ toggle_block = source.split("async def page_pipeline_toggle", 1)[1].split(
181
+ "async def page_pipeline_dryrun", 1)[0]
182
+ dry_block = source.split("async def page_pipeline_dryrun", 1)[1].split(
183
+ "async def generate_manual_insight", 1)[0]
184
+ self.assertIn("_page_is_local_admin", toggle_block)
185
+ self.assertIn("_page_is_local_admin", dry_block)
186
+ status_block = source.split("async def page_pipeline(self)", 1)[1].split(
187
+ "async def page_pipeline_toggle", 1)[0]
188
+ self.assertIn("payload_status", status_block)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ unittest.main()
@@ -0,0 +1,222 @@
1
+ # -*- coding: utf-8 -*-
2
+ """PIPE-4 mode tests: shadow/active switching, prompt output, fallback, safety."""
3
+ import json
4
+ import tempfile
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ from biocore_model import pipeline as pl
9
+ from biocore_model import pipeline_api as pa
10
+ from biocore_model import runtime as rt
11
+
12
+
13
+ class FakePlugin:
14
+ def __init__(self, data_dir):
15
+ self.data_dir = str(data_dir)
16
+
17
+
18
+ class FakeEvent:
19
+ def __init__(self, text="", umo="umo-7", sender="u-7"):
20
+ self.message_str = text
21
+ self.unified_msg_origin = umo
22
+ self._sender = sender
23
+
24
+ def get_sender_id(self):
25
+ return self._sender
26
+
27
+
28
+ class FakeCore:
29
+ def __init__(self):
30
+ self._snapshot = {"emotion": "sadness", "intensity": 0.6, "self_esteem": 0.4,
31
+ "regulation": "suppress", "v": 0.1, "a": 0.4, "stress": 0.3}
32
+ self.observed = []
33
+ self.loaded = []
34
+
35
+ def snapshot(self):
36
+ return dict(self._snapshot)
37
+
38
+ def observe(self, text, embedding=None):
39
+ self.observed.append(text)
40
+
41
+ def load_state(self, state):
42
+ self.loaded.append(dict(state))
43
+
44
+
45
+ def make_ctx(text="今天好累", core=None, meta=None):
46
+ return rt.make_request_ctx(plugin=None, event=FakeEvent(text),
47
+ core=core if core is not None else FakeCore(),
48
+ text=text, memory=[], now=1000.0, meta=meta or {})
49
+
50
+
51
+ class Base(unittest.TestCase):
52
+ def setUp(self):
53
+ self._tmp = tempfile.TemporaryDirectory()
54
+ self.tmp = Path(self._tmp.name)
55
+ self.plugin = FakePlugin(self.tmp)
56
+ rt.reset_mode()
57
+ rt.apply_switches({})
58
+ rt.reset_pipeline()
59
+ pa.reset_boot()
60
+
61
+ def tearDown(self):
62
+ rt.reset_mode()
63
+ rt.apply_switches({})
64
+ rt.reset_pipeline()
65
+ pa.reset_boot()
66
+ self._tmp.cleanup()
67
+
68
+
69
+ class ModeTest(Base):
70
+ def test_default_mode_is_shadow(self):
71
+ self.assertEqual(rt.MODE_DEFAULT, "shadow")
72
+ self.assertEqual(rt.get_mode(), "shadow")
73
+
74
+ def test_apply_mode_switches_without_restart(self):
75
+ before = rt.get_pipeline()
76
+ self.assertEqual(rt.apply_mode("active"), "active")
77
+ self.assertEqual(rt.get_mode(), "active")
78
+ self.assertIs(rt.get_pipeline(), before)
79
+ self.assertEqual(rt.apply_mode("shadow"), "shadow")
80
+
81
+ def test_apply_mode_invalid_rejected(self):
82
+ with self.assertRaises(ValueError):
83
+ rt.apply_mode("turbo")
84
+ self.assertEqual(rt.get_mode(), "shadow")
85
+
86
+ def test_save_mode_persists_to_ui_settings(self):
87
+ saved = pa.save_mode(self.plugin, "active")
88
+ self.assertTrue(saved["ok"])
89
+ data = json.loads((self.tmp / "ui_settings.json").read_text(encoding="utf-8"))
90
+ self.assertEqual(data["autonomy"]["pipeline_mode"], "active")
91
+ self.assertEqual(rt.get_mode(), "active")
92
+
93
+ def test_load_mode_default_and_roundtrip(self):
94
+ self.assertEqual(pa.load_mode(self.plugin), "shadow")
95
+ pa.save_mode(self.plugin, "active")
96
+ self.assertEqual(pa.load_mode(self.plugin), "active")
97
+
98
+ def test_boot_applies_persisted_active_mode(self):
99
+ pa.save_mode(self.plugin, "active")
100
+ rt.reset_mode()
101
+ pa.reset_boot()
102
+ other = FakePlugin(self.tmp)
103
+ self.assertTrue(pa.boot(other))
104
+ self.assertEqual(rt.get_mode(), "active")
105
+
106
+ def test_payload_mode_get_and_set(self):
107
+ self.assertEqual(pa.payload_mode(self.plugin)["mode"], "shadow")
108
+ result = pa.payload_mode(self.plugin, "active")
109
+ self.assertTrue(result["ok"])
110
+ self.assertEqual(result["mode"], "active")
111
+ self.assertEqual(pa.payload_mode(self.plugin)["mode"], "active")
112
+
113
+ def test_payload_mode_invalid(self):
114
+ result = pa.payload_mode(self.plugin, "loud")
115
+ self.assertFalse(result["ok"])
116
+ self.assertEqual(result["message"], "invalid_mode")
117
+
118
+
119
+ class PromptTest(Base):
120
+ def test_shadow_run_reply_has_no_prompt_keys(self):
121
+ out = rt.run_reply(make_ctx())
122
+ self.assertNotIn("prompt_system", out)
123
+ self.assertNotIn("prompt_user", out)
124
+ self.assertIn("trace", out)
125
+
126
+ def test_active_run_prompt_structure(self):
127
+ rt.apply_mode("active")
128
+ out = rt.run_prompt(make_ctx())
129
+ self.assertIsNotNone(out)
130
+ for key in ("prompt_system", "prompt_user", "trace", "order"):
131
+ self.assertIn(key, out)
132
+ self.assertTrue(out["prompt_user"])
133
+ self.assertEqual(len(out["trace"]["stages"]), 11)
134
+
135
+ def test_active_prompt_contains_state_and_protocol(self):
136
+ rt.apply_mode("active")
137
+ out = rt.run_prompt(make_ctx())
138
+ self.assertIn("sadness", out["prompt_user"])
139
+ self.assertNotIn("【输出格式", out["prompt_system"])
140
+ self.assertNotIn("<bc>", out["prompt_system"])
141
+ self.assertNotIn("【输出格式", out["prompt_user"])
142
+
143
+ def test_active_prompt_uses_persona_opts(self):
144
+ rt.apply_mode("active")
145
+ out = rt.run_prompt(make_ctx(meta={"template_opts": {
146
+ "persona": "你是小助手", "speak_style": "口语"}}))
147
+ self.assertIn("小助手", out["prompt_system"])
148
+ self.assertIn("口语", out["prompt_system"])
149
+
150
+ def test_active_failure_returns_none(self):
151
+ rt.apply_mode("active")
152
+ bad = pl.Pipeline()
153
+ bad.register(pl.Stage("perceive", lambda ctx: 1 / 0))
154
+ self.assertIsNone(rt.run_prompt(make_ctx(), pipeline=bad))
155
+
156
+ def test_active_empty_ctx_returns_none(self):
157
+ rt.apply_mode("active")
158
+ self.assertIsNone(rt.run_prompt(rt.make_request_ctx()))
159
+
160
+ def test_mode_switch_routes_behavior_in_one_process(self):
161
+ shadow = rt.run_reply(make_ctx())
162
+ self.assertNotIn("prompt_user", shadow)
163
+ rt.apply_mode("active")
164
+ active = rt.run_prompt(make_ctx())
165
+ self.assertTrue(active and active["prompt_user"])
166
+ rt.apply_mode("shadow")
167
+ again = rt.run_reply(make_ctx())
168
+ self.assertNotIn("prompt_user", again)
169
+
170
+
171
+ def test_active_run_reply_returns_prompt_payload(self):
172
+ rt.apply_mode("active")
173
+ out = rt.run_reply(make_ctx())
174
+ for key in ("prompt_system", "prompt_user", "trace"):
175
+ self.assertIn(key, out)
176
+ self.assertTrue(out["prompt_user"])
177
+
178
+ def test_active_run_reply_failure_returns_empty_dict(self):
179
+ rt.apply_mode("active")
180
+ bad = pl.Pipeline()
181
+ bad.register(pl.Stage("perceive", lambda ctx: 1 / 0))
182
+ self.assertEqual(rt.run_reply(make_ctx(), pipeline=bad), {})
183
+
184
+
185
+ class SafetyTest(Base):
186
+ def test_shadow_does_not_write_core(self):
187
+ core = FakeCore()
188
+ rt.run_reply(make_ctx(core=core, meta={"observe_core": True}))
189
+ self.assertEqual(core.observed, [])
190
+ self.assertEqual(core.loaded, [])
191
+
192
+ def test_active_observe_core_default_false(self):
193
+ rt.apply_mode("active")
194
+ core = FakeCore()
195
+ out = rt.run_prompt(make_ctx(core=core))
196
+ self.assertIsNotNone(out)
197
+ self.assertEqual(core.observed, [])
198
+ self.assertEqual(core.loaded, [])
199
+
200
+ def test_active_observe_core_true_calls_observe(self):
201
+ rt.apply_mode("active")
202
+ core = FakeCore()
203
+ out = rt.run_prompt(make_ctx(core=core, meta={"observe_core": True}))
204
+ self.assertIsNotNone(out)
205
+ self.assertEqual(core.observed, ["今天好累"])
206
+ self.assertEqual(core.loaded, [])
207
+
208
+ def test_trace_has_stage_ms(self):
209
+ rt.apply_mode("active")
210
+ out = rt.run_prompt(make_ctx())
211
+ stages = out["trace"]["stages"]
212
+ self.assertEqual([row["name"] for row in stages],
213
+ ["perceive", "memory", "bio", "decide", "expr_state",
214
+ "expr_tone", "expr_intent", "think", "parse",
215
+ "feedback", "present"])
216
+ for row in stages:
217
+ self.assertIsInstance(row["ms"], float)
218
+ self.assertGreaterEqual(row["ms"], 0.0)
219
+
220
+
221
+ if __name__ == "__main__":
222
+ unittest.main()
@@ -0,0 +1,85 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json, tempfile, unittest
3
+ from pathlib import Path
4
+ from biocore_model import plugin_config
5
+
6
+
7
+ class PluginConfigPersistTest(unittest.TestCase):
8
+ def _file(self, tmp, data, bom=False):
9
+ path = Path(tmp) / plugin_config.FILE_NAME
10
+ text = json.dumps(data, ensure_ascii=False)
11
+ path.write_text(text, encoding="utf-8-sig" if bom else "utf-8")
12
+ return path
13
+
14
+ def test_persist_updates_and_reads_back(self):
15
+ with tempfile.TemporaryDirectory() as tmp:
16
+ path = self._file(tmp, {"reply_split_enabled": False, "reply_split_mode": "punct"})
17
+ old = plugin_config.find_config
18
+ plugin_config.find_config = lambda plugin=None: path
19
+ try:
20
+ result = plugin_config.persist_updates({"reply_split_enabled": True, "reply_split_mode": "astrbot"})
21
+ finally:
22
+ plugin_config.find_config = old
23
+ self.assertTrue(result["ok"])
24
+ data = json.loads(path.read_text(encoding="utf-8"))
25
+ self.assertTrue(data["reply_split_enabled"]); self.assertEqual(data["reply_split_mode"], "astrbot")
26
+
27
+ def test_bom_preserved_when_present(self):
28
+ with tempfile.TemporaryDirectory() as tmp:
29
+ path = self._file(tmp, {"x": 1}, bom=True)
30
+ old = plugin_config.find_config
31
+ plugin_config.find_config = lambda plugin=None: path
32
+ try:
33
+ plugin_config.persist_updates({"y": 2})
34
+ finally:
35
+ plugin_config.find_config = old
36
+ self.assertTrue(path.read_bytes().startswith(b"\xef\xbb\xbf"))
37
+ self.assertEqual(json.loads(path.read_text(encoding="utf-8-sig"))["y"], 2)
38
+
39
+ def test_no_bom_written_without_bom(self):
40
+ with tempfile.TemporaryDirectory() as tmp:
41
+ path = self._file(tmp, {"x": 1})
42
+ old = plugin_config.find_config
43
+ plugin_config.find_config = lambda plugin=None: path
44
+ try:
45
+ plugin_config.persist_updates({"y": 2})
46
+ finally:
47
+ plugin_config.find_config = old
48
+ self.assertFalse(path.read_bytes().startswith(b"\xef\xbb\xbf"))
49
+
50
+ def test_backup_created(self):
51
+ with tempfile.TemporaryDirectory() as tmp:
52
+ path = self._file(tmp, {"x": 1})
53
+ old = plugin_config.find_config
54
+ plugin_config.find_config = lambda plugin=None: path
55
+ try:
56
+ plugin_config.persist_updates({"x": 2})
57
+ finally:
58
+ plugin_config.find_config = old
59
+ self.assertEqual(len(list(Path(tmp).glob(plugin_config.FILE_NAME + ".bak_replysplit_*"))), 1)
60
+
61
+ def test_backup_rotation_keeps_three(self):
62
+ with tempfile.TemporaryDirectory() as tmp:
63
+ path = self._file(tmp, {"x": 1})
64
+ old = plugin_config.find_config
65
+ plugin_config.find_config = lambda plugin=None: path
66
+ try:
67
+ for index in range(5):
68
+ plugin_config.persist_updates({"x": index})
69
+ finally:
70
+ plugin_config.find_config = old
71
+ self.assertLessEqual(len(list(Path(tmp).glob(plugin_config.FILE_NAME + ".bak_replysplit_*"))), 3)
72
+
73
+ def test_missing_file_reports_failure(self):
74
+ with tempfile.TemporaryDirectory() as tmp:
75
+ old = plugin_config.find_config
76
+ plugin_config.find_config = lambda plugin=None: None
77
+ try:
78
+ result = plugin_config.persist_updates({"x": 1})
79
+ finally:
80
+ plugin_config.find_config = old
81
+ self.assertFalse(result["ok"])
82
+
83
+
84
+ if __name__ == "__main__":
85
+ unittest.main()
@@ -0,0 +1,116 @@
1
+ # -*- coding: utf-8 -*-
2
+ import random, unittest
3
+ from biocore_model import policy_learning as pl
4
+
5
+ class PolicyLearningTest(unittest.TestCase):
6
+ def table(self):
7
+ t = {}
8
+ t = pl.update(t, "expect|secure|day", "approach", 1.0)
9
+ for _ in range(2):
10
+ t = pl.update(t, "expect|secure|day", "approach", 1.0)
11
+ for _ in range(4):
12
+ t = pl.update(t, "expect|secure|day", "share", -1.0)
13
+ return t
14
+
15
+ def test_update_positive(self):
16
+ t = pl.update({}, "c", "approach", 1.0)
17
+ self.assertGreater(t["c"]["approach"]["q"], 0.0)
18
+ self.assertEqual(t["c"]["approach"]["n"], 1)
19
+
20
+ def test_update_negative(self):
21
+ t = pl.update({}, "c", "share", -1.0)
22
+ self.assertLess(t["c"]["share"]["q"], 0.0)
23
+
24
+ def test_update_alpha(self):
25
+ low = pl.update({}, "c", "approach", 1.0, alpha=0.1)
26
+ high = pl.update({}, "c", "approach", 1.0, alpha=0.5)
27
+ self.assertGreater(high["c"]["approach"]["q"], low["c"]["approach"]["q"])
28
+
29
+ def test_update_immutable(self):
30
+ original = {}
31
+ updated = pl.update(original, "c", "approach", 1.0)
32
+ self.assertEqual(original, {})
33
+ self.assertNotIn("c", original)
34
+ self.assertIn("c", updated)
35
+
36
+ def test_reward_responded(self):
37
+ self.assertGreater(pl.reward(True, 1.0, 0.0), pl.reward(False, 1.0, 0.0))
38
+
39
+ def test_reward_speed(self):
40
+ self.assertGreater(pl.reward(True, 1.0, 0.0), pl.reward(True, 100.0, 0.0))
41
+
42
+ def test_reward_clamped(self):
43
+ self.assertLessEqual(pl.reward(True, 0.0, 5.0), 1.0)
44
+ self.assertGreaterEqual(pl.reward(True, 0.0, -5.0), -0.3)
45
+
46
+ def test_choose_greedy(self):
47
+ t = self.table()
48
+ result = pl.choose("expect|secure|day", t, epsilon=0.0, rng=random.Random(1))
49
+ self.assertEqual(result["action"], "approach")
50
+ self.assertFalse(result["explored"])
51
+
52
+ def test_choose_epsilon_seed(self):
53
+ t = self.table()
54
+ rng = random.Random(42)
55
+ explored = sum(1 for _ in range(1000) if pl.choose("expect|secure|day", t, epsilon=0.3, rng=rng)["explored"])
56
+ self.assertGreater(explored, 200)
57
+ self.assertLess(explored, 400)
58
+
59
+ def test_choose_unseen_q_zero(self):
60
+ result = pl.choose("loss|avoidant|night", {}, epsilon=0.0, rng=random.Random(2))
61
+ self.assertIn(result["action"], pl.ACTIONS)
62
+ self.assertEqual(result["q"], 0.0)
63
+
64
+ def test_policy_view_sorted(self):
65
+ rows = pl.policy_view(self.table(), "expect|secure|day")
66
+ self.assertEqual(len(rows), len(pl.ACTIONS))
67
+ self.assertEqual(rows[0]["action"], "approach")
68
+ self.assertGreaterEqual(rows[0]["q"], rows[-1]["q"])
69
+
70
+ def test_policy_view_unseen(self):
71
+ rows = pl.policy_view({}, "unknown")
72
+ self.assertEqual(len(rows), 6)
73
+ self.assertTrue(all(row["q"] == 0 for row in rows))
74
+
75
+ def test_context_buckets(self):
76
+ self.assertEqual(pl.hour_bucket(9), "day")
77
+ self.assertEqual(pl.hour_bucket(20), "evening")
78
+ self.assertEqual(pl.hour_bucket(2), "night")
79
+ self.assertIn("|", pl.make_context("protest", "anxious", 20))
80
+
81
+ def test_reward_slow_vs_fast(self):
82
+ self.assertAlmostEqual(pl.reward(True, 0.0, 0.0), 0.7, places=6)
83
+ self.assertAlmostEqual(pl.reward(True, 120.0, 0.0), 0.5, places=6)
84
+
85
+ def test_choice_after_history(self):
86
+ t = self.table()
87
+ first = pl.choose("expect|secure|day", t, epsilon=0.0, rng=random.Random(3))
88
+ second = pl.choose("expect|secure|day", t, epsilon=0.0, rng=random.Random(4))
89
+ self.assertEqual(first["action"], "approach")
90
+ self.assertEqual(second["action"], "approach")
91
+
92
+ def test_update_incremental_values(self):
93
+ t = pl.update({}, "c", "approach", 1.0, alpha=0.2)
94
+ self.assertAlmostEqual(t["c"]["approach"]["q"], 0.2, places=6)
95
+ t = pl.update(t, "c", "approach", 1.0, alpha=0.2)
96
+ self.assertAlmostEqual(t["c"]["approach"]["q"], 0.36, places=6)
97
+ self.assertEqual(t["c"]["approach"]["n"], 2)
98
+
99
+ def test_choose_same_seed_reproducible(self):
100
+ t = self.table()
101
+ first = [pl.choose("expect|secure|day", t, epsilon=0.5, rng=random.Random(11))
102
+ for _ in range(20)]
103
+ second = [pl.choose("expect|secure|day", t, epsilon=0.5, rng=random.Random(11))
104
+ for _ in range(20)]
105
+ self.assertEqual(first, second)
106
+
107
+ def test_reward_extremes(self):
108
+ self.assertAlmostEqual(pl.reward(True, 0.0, 1.0), 1.0, places=6)
109
+ self.assertAlmostEqual(pl.reward(False, 9999.0, -1.0), -0.3, places=6)
110
+
111
+ def test_make_context_hour_buckets_differ(self):
112
+ day = pl.make_context("expect", "secure", 9)
113
+ night = pl.make_context("expect", "secure", 2)
114
+ self.assertTrue(day.endswith("|day"))
115
+ self.assertTrue(night.endswith("|night"))
116
+ self.assertNotEqual(day, night)