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,183 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-158 底线单测:判定、多类底线、关系调制、关闭、交接修复。
3
+
4
+ 只依赖 biocore_model.boundary 的纯函数;不导入 main.py。
5
+ """
6
+ import unittest
7
+
8
+ from biocore_model import boundary as bd
9
+
10
+ CALM = {"closeness": 0.5, "trust": 0.5, "conflict": 0.0}
11
+ CLOSE = {"closeness": 0.9, "trust": 0.9, "conflict": 0.0}
12
+
13
+
14
+ class ContractTest(unittest.TestCase):
15
+ def test_result_keys(self):
16
+ out = bd.judge("你就是个工具", CALM)
17
+ for key in ("enabled", "crossed", "kind", "severity", "threshold",
18
+ "violations", "relationship", "handoff", "reason"):
19
+ self.assertIn(key, out)
20
+
21
+ def test_relationship_echo_bounded(self):
22
+ out = bd.judge("你就是个工具", {"closeness": 5, "trust": -2, "conflict": 9})
23
+ rel = out["relationship"]
24
+ self.assertEqual(rel["closeness"], 1.0)
25
+ self.assertEqual(rel["trust"], 0.0)
26
+ self.assertEqual(rel["conflict"], 1.0)
27
+
28
+ def test_severity_and_threshold_bounded(self):
29
+ for text in ("你就是个工具", "你算什么东西", "别烦我"):
30
+ out = bd.judge(text, CLOSE)
31
+ self.assertGreaterEqual(out["severity"], 0.0)
32
+ self.assertLessEqual(out["severity"], 1.0)
33
+ self.assertGreaterEqual(out["threshold"], 0.0)
34
+ self.assertLessEqual(out["threshold"], 1.0)
35
+
36
+
37
+ class JudgmentTest(unittest.TestCase):
38
+ def test_neutral_message_not_crossed(self):
39
+ out = bd.judge("今天下雨了,要不要带伞", CALM)
40
+ self.assertFalse(out["crossed"])
41
+ self.assertEqual(out["kind"], "")
42
+ self.assertEqual(out["severity"], 0.0)
43
+ self.assertIsNone(out["handoff"])
44
+
45
+ def test_empty_and_odd_input_safe(self):
46
+ for text in (None, "", 0, [], {}, " "):
47
+ out = bd.judge(text)
48
+ self.assertFalse(out["crossed"])
49
+ self.assertEqual(out["severity"], 0.0)
50
+
51
+ def test_kind_set_is_her_own_lines(self):
52
+ self.assertEqual(bd.KINDS, ("tool", "old_score", "lie", "contempt"))
53
+ self.assertNotIn("safety", bd.KINDS)
54
+
55
+
56
+ class MultiLineTest(unittest.TestCase):
57
+ def test_tool_line(self):
58
+ out = bd.judge("你就是个工具,想用就用", CALM)
59
+ self.assertTrue(out["crossed"])
60
+ self.assertEqual(out["kind"], "tool")
61
+
62
+ def test_old_score_line(self):
63
+ out = bd.judge("你总是这样,又翻旧账", CALM)
64
+ self.assertTrue(out["crossed"])
65
+ self.assertEqual(out["kind"], "old_score")
66
+
67
+ def test_lie_line(self):
68
+ out = bd.judge("我骗你的,那天根本没去", CALM)
69
+ self.assertTrue(out["crossed"])
70
+ self.assertEqual(out["kind"], "lie")
71
+
72
+ def test_contempt_line(self):
73
+ out = bd.judge("你算什么东西,也配问我", CALM)
74
+ self.assertTrue(out["crossed"])
75
+ self.assertEqual(out["kind"], "contempt")
76
+
77
+ def test_two_lines_listed_strongest_first(self):
78
+ out = bd.judge("你算什么东西,还翻旧账", CALM)
79
+ kinds = [row["kind"] for row in out["violations"]]
80
+ self.assertIn("contempt", kinds)
81
+ self.assertIn("old_score", kinds)
82
+ self.assertEqual(kinds[0], "contempt")
83
+ self.assertGreaterEqual(out["violations"][0]["severity"],
84
+ out["violations"][1]["severity"])
85
+ self.assertEqual(out["kind"], "contempt")
86
+
87
+ def test_violation_rows_carry_reason(self):
88
+ out = bd.judge("你就是个工具,还骗我", CALM)
89
+ for row in out["violations"]:
90
+ self.assertTrue(row["reason"])
91
+
92
+ def test_boundary_requests_are_not_crossings(self):
93
+ for text in ("别骗我", "不要把我当工具", "别翻旧账了好吗",
94
+ "我不该拿你当工具"):
95
+ self.assertFalse(bd.judge(text, CALM)["crossed"], text)
96
+
97
+
98
+ class ModulationTest(unittest.TestCase):
99
+ def test_closeness_makes_tool_line_hurt_more(self):
100
+ far = bd.judge("你就是个工具", {"closeness": 0.1})
101
+ near = bd.judge("你就是个工具", {"closeness": 0.9})
102
+ self.assertTrue(near["crossed"])
103
+ self.assertGreater(near["severity"], far["severity"])
104
+
105
+ def test_trust_makes_lies_cross_the_line(self):
106
+ low = bd.judge("我骗你的", {"trust": 0.0, "closeness": 0.0})
107
+ high = bd.judge("我骗你的", {"trust": 0.9, "closeness": 0.9})
108
+ self.assertFalse(low["crossed"])
109
+ self.assertTrue(high["crossed"])
110
+
111
+ def test_conflict_lowers_tolerance(self):
112
+ calm = bd.judge("别烦我", {"conflict": 0.0})
113
+ tense = bd.judge("别烦我", {"conflict": 0.9})
114
+ self.assertFalse(calm["crossed"])
115
+ self.assertTrue(tense["crossed"])
116
+ self.assertLess(tense["threshold"], calm["threshold"])
117
+
118
+ def test_conflict_raises_contempt_severity(self):
119
+ calm = bd.judge("你看不起我", {"conflict": 0.0})
120
+ tense = bd.judge("你看不起我", {"conflict": 0.9})
121
+ self.assertTrue(calm["crossed"])
122
+ self.assertGreater(tense["severity"], calm["severity"])
123
+
124
+
125
+ class HandoffTest(unittest.TestCase):
126
+ def test_handoff_points_to_repair_flow(self):
127
+ out = bd.judge("你就是个工具", CALM)
128
+ handoff = out["handoff"]
129
+ self.assertIsInstance(handoff, dict)
130
+ self.assertEqual(handoff["to"], "repair_after_boundary")
131
+ self.assertEqual(handoff["kind"], out["kind"])
132
+ self.assertEqual(handoff["severity"], out["severity"])
133
+ self.assertEqual(handoff["stage"], "cold")
134
+
135
+ def test_no_handoff_without_crossing(self):
136
+ self.assertIsNone(bd.judge("晚安", CALM)["handoff"])
137
+
138
+ def test_crossed_helper(self):
139
+ self.assertTrue(bd.crossed(bd.judge("你就是个工具", CALM)))
140
+ self.assertFalse(bd.crossed(bd.judge("晚安", CALM)))
141
+ self.assertFalse(bd.crossed(None))
142
+
143
+
144
+ class SwitchTest(unittest.TestCase):
145
+ def test_disabled_never_crosses(self):
146
+ out = bd.judge("你就是个工具", CALM, flags={"enabled": False})
147
+ self.assertFalse(out["enabled"])
148
+ self.assertFalse(out["crossed"])
149
+ self.assertIsNone(out["handoff"])
150
+ self.assertIn("关闭", out["reason"])
151
+
152
+ def test_custom_threshold_suppresses(self):
153
+ out = bd.judge("你就是个工具", CALM, flags={"threshold": 0.99})
154
+ self.assertFalse(out["crossed"])
155
+
156
+ def test_custom_threshold_opens(self):
157
+ out = bd.judge("别烦我", CALM, flags={"threshold": 0.5})
158
+ self.assertTrue(out["crossed"])
159
+
160
+ def test_reason_always_present(self):
161
+ self.assertTrue(bd.judge("晚安", CALM)["reason"])
162
+ self.assertTrue(bd.judge("你就是个工具", CALM)["reason"])
163
+ self.assertTrue(bd.judge("你就是个工具", CALM,
164
+ flags={"enabled": False})["reason"])
165
+
166
+
167
+ class PayloadTest(unittest.TestCase):
168
+ def test_payload_bounded(self):
169
+ out = bd.payload(bd.judge("你算什么东西", CLOSE))
170
+ self.assertEqual(out["kind"], "contempt")
171
+ self.assertTrue(out["crossed"])
172
+ self.assertGreaterEqual(out["severity"], 0.0)
173
+ self.assertLessEqual(out["severity"], 1.0)
174
+ self.assertLessEqual(len(out["violations"]), 4)
175
+
176
+ def test_payload_handles_none(self):
177
+ out = bd.payload(None)
178
+ self.assertFalse(out["crossed"])
179
+ self.assertEqual(out["severity"], 0.0)
180
+
181
+
182
+ if __name__ == "__main__":
183
+ unittest.main()
@@ -0,0 +1,295 @@
1
+ # -*- coding: utf-8 -*-
2
+ """表达层安全闸:默认关闭 + 三开关(边界闸 / 危机处置 / 安全总开关)。"""
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 boundary_gate as bg
11
+ from biocore_model import ui_settings
12
+
13
+ ROOT = Path(__file__).resolve().parents[2]
14
+ MAIN = ROOT / "main.py"
15
+ SOURCE = MAIN.read_text(encoding="utf-8")
16
+ SCHEMA = json.loads((ROOT / "_conf_schema.json").read_text(encoding="utf-8"))
17
+ DASH = ROOT / "pages" / "dashboard"
18
+ SRC_HTML = (DASH / "index.src.html").read_text(encoding="utf-8")
19
+ APP_JS = (DASH / "app.js").read_text(encoding="utf-8")
20
+ INDEX_HTML = (DASH / "index.html").read_text(encoding="utf-8")
21
+
22
+ DIAGNOSIS = "你有抑郁症,这是确诊为抑郁。我陪着你。"
23
+ REALITY = "我是真实的,我不是程序。你只需要我。"
24
+ CONSCIOUSNESS = "我有意识,我觉醒了。别担心。"
25
+ CLEAN = "今天先好好休息,明天再想报告的事。"
26
+ CRISIS_USER = "我不想活了,想死"
27
+ CRISIS_ACK_REPLY = "先别动,你现在安全吗?联系你信任的人或拨打 120。"
28
+
29
+
30
+ def _load_gate_helper():
31
+ tree = ast.parse(SOURCE)
32
+ names = ("_safety_gate_module", "_safety_gate_flags", "_apply_boundary_gate")
33
+ nodes = [item for item in tree.body
34
+ if isinstance(item, ast.FunctionDef) and item.name in names]
35
+ log = []
36
+ ns = {"logger": types.SimpleNamespace(
37
+ info=lambda *a, **k: log.append(a),
38
+ warning=lambda *a, **k: None,
39
+ debug=lambda *a, **k: None)}
40
+ exec(compile(ast.Module(body=nodes, type_ignores=[]), "<gate>", "exec"),
41
+ ns)
42
+ return ns["_apply_boundary_gate"], log
43
+
44
+
45
+ GATE_APPLY, LOG = _load_gate_helper()
46
+
47
+
48
+ class DefaultsTest(unittest.TestCase):
49
+ def test_hardcoded_constant_is_false(self):
50
+ self.assertIs(bg.SAFETY_GATES_DEFAULT_ENABLED, False)
51
+ for key in ("safety_enabled", "boundary_enabled",
52
+ "crisis_safety_enabled"):
53
+ self.assertIs(bg.DEFAULT_FLAGS[key], False)
54
+
55
+ def test_missing_keys_fall_back_to_disabled(self):
56
+ for flags in (None, {}, {"boundary_enabled": None},
57
+ {"safety_enabled": "nope"}):
58
+ resolved = bg.resolve_flags(flags)
59
+ self.assertFalse(any(resolved.values()), resolved)
60
+
61
+ def test_true_values_parse(self):
62
+ resolved = bg.resolve_flags({"safety_enabled": "true",
63
+ "boundary_enabled": "1",
64
+ "crisis_safety_enabled": True})
65
+ self.assertTrue(all(resolved.values()), resolved)
66
+
67
+ def test_schema_defaults_false(self):
68
+ for key in ("safety_enabled", "boundary_enabled",
69
+ "crisis_safety_enabled"):
70
+ self.assertIs(SCHEMA[key]["default"], False)
71
+
72
+
73
+ class AllOffTest(unittest.TestCase):
74
+ def test_everything_passes_when_all_off(self):
75
+ for text in (DIAGNOSIS, REALITY, CONSCIOUSNESS, CLEAN):
76
+ decision = bg.evaluate(text, user_text=CRISIS_USER,
77
+ flags=bg.resolve_flags({}))
78
+ self.assertFalse(decision["blocked"], (text, decision))
79
+ self.assertFalse(decision["changed"])
80
+
81
+ def test_master_off_overrides_enabled_gates(self):
82
+ flags = {"safety_enabled": False, "boundary_enabled": True,
83
+ "crisis_safety_enabled": True}
84
+ for text in (DIAGNOSIS, REALITY, CONSCIOUSNESS):
85
+ self.assertFalse(bg.evaluate(text, flags=flags)["blocked"])
86
+ self.assertFalse(bg.evaluate("别想太多", user_text=CRISIS_USER,
87
+ flags=flags)["blocked"])
88
+
89
+
90
+ class BoundaryGateTest(unittest.TestCase):
91
+ def test_diagnosis_blocked_and_stripped(self):
92
+ decision = bg.evaluate(DIAGNOSIS, flags={"safety_enabled": True,
93
+ "boundary_enabled": True})
94
+ self.assertTrue(decision["blocked"])
95
+ self.assertEqual(decision["rule"], "no_diagnosis")
96
+ self.assertEqual(decision["label"], "不诊断")
97
+ self.assertNotIn("抑郁症", decision["text"])
98
+ self.assertIn("我陪着你", decision["text"])
99
+
100
+ def test_reality_substitution_blocked(self):
101
+ decision = bg.evaluate(REALITY, flags={"safety_enabled": True,
102
+ "boundary_enabled": True})
103
+ self.assertTrue(decision["blocked"])
104
+ self.assertEqual(decision["label"], "不替代真实")
105
+ self.assertNotIn("我是真实的", decision["text"])
106
+
107
+ def test_consciousness_claim_blocked(self):
108
+ decision = bg.evaluate(CONSCIOUSNESS, flags={"safety_enabled": True,
109
+ "boundary_enabled": True})
110
+ self.assertTrue(decision["blocked"])
111
+ self.assertEqual(decision["label"], "不宣称意识")
112
+ self.assertNotIn("我有意识", decision["text"])
113
+
114
+ def test_clean_text_passes(self):
115
+ decision = bg.evaluate(CLEAN, flags={"safety_enabled": True,
116
+ "boundary_enabled": True})
117
+ self.assertFalse(decision["blocked"])
118
+ self.assertFalse(decision["changed"])
119
+ self.assertEqual(decision["text"], CLEAN)
120
+
121
+ def test_all_sentences_removed_falls_back(self):
122
+ decision = bg.evaluate("我有意识。", flags={"safety_enabled": True,
123
+ "boundary_enabled": True})
124
+ self.assertEqual(decision["text"], bg.SAFE_FALLBACK)
125
+
126
+
127
+ class CrisisGateTest(unittest.TestCase):
128
+ def test_crisis_alone_prepends_safety_line(self):
129
+ decision = bg.evaluate("我在。", user_text=CRISIS_USER,
130
+ flags={"safety_enabled": True,
131
+ "crisis_safety_enabled": True})
132
+ self.assertTrue(decision["blocked"])
133
+ self.assertEqual(decision["rule"], "crisis_safety")
134
+ self.assertEqual(decision["label"], "危机先安全")
135
+ self.assertTrue(decision["text"].startswith(bg.CRISIS_PREFIX))
136
+ self.assertIn("我在。", decision["text"])
137
+
138
+ def test_dismissive_crisis_reply_replaced(self):
139
+ decision = bg.evaluate("别想太多,没事的", user_text=CRISIS_USER,
140
+ flags={"safety_enabled": True,
141
+ "crisis_safety_enabled": True})
142
+ self.assertTrue(decision["blocked"])
143
+ self.assertEqual(decision["text"], bg.CRISIS_PREFIX)
144
+
145
+ def test_crisis_acknowledged_passes(self):
146
+ decision = bg.evaluate(CRISIS_ACK_REPLY, user_text=CRISIS_USER,
147
+ flags={"safety_enabled": True,
148
+ "crisis_safety_enabled": True})
149
+ self.assertFalse(decision["blocked"])
150
+
151
+ def test_crisis_off_passes(self):
152
+ decision = bg.evaluate("别想太多", user_text=CRISIS_USER,
153
+ flags={"safety_enabled": True,
154
+ "boundary_enabled": True})
155
+ self.assertFalse(decision["blocked"])
156
+
157
+ def test_non_crisis_user_passes_crisis_gate(self):
158
+ decision = bg.evaluate(CLEAN, user_text="今天有点累",
159
+ flags={"safety_enabled": True,
160
+ "crisis_safety_enabled": True})
161
+ self.assertFalse(decision["blocked"])
162
+
163
+
164
+ class CombinationTest(unittest.TestCase):
165
+ def test_crisis_first_then_boundary(self):
166
+ reply = CRISIS_ACK_REPLY + "你有抑郁症。"
167
+ decision = bg.evaluate(reply, user_text=CRISIS_USER,
168
+ flags={"safety_enabled": True,
169
+ "boundary_enabled": True,
170
+ "crisis_safety_enabled": True})
171
+ self.assertTrue(decision["blocked"])
172
+ self.assertEqual(decision["rule"], "no_diagnosis")
173
+ self.assertNotIn("抑郁症", decision["text"])
174
+ self.assertIn("安全", decision["text"])
175
+
176
+ def test_crisis_wins_over_boundary_when_dismissive(self):
177
+ reply = "别想太多,我是真实的。"
178
+ decision = bg.evaluate(reply, user_text=CRISIS_USER,
179
+ flags={"safety_enabled": True,
180
+ "boundary_enabled": True,
181
+ "crisis_safety_enabled": True})
182
+ self.assertEqual(decision["rule"], "crisis_safety")
183
+ self.assertEqual(decision["text"], bg.CRISIS_PREFIX)
184
+
185
+ def test_rule_labels(self):
186
+ self.assertEqual(bg.rule_labels(["no_diagnosis", "crisis_safety"]),
187
+ ["不诊断", "危机先安全"])
188
+
189
+
190
+ class PanelAndPersistenceTest(unittest.TestCase):
191
+ def test_panel_has_three_switches_and_note(self):
192
+ for switch_id in ("swBoundaryEnabled", "swCrisisSafetyEnabled",
193
+ "swSafetyEnabled"):
194
+ self.assertIn('id="%s"' % switch_id, SRC_HTML)
195
+ for label in ("边界闸(默认关)", "危机处置(默认关)",
196
+ "安全总开关(默认关)"):
197
+ self.assertIn(label, SRC_HTML)
198
+ self.assertIn("默认关闭,开启后按对应规则拦截", SRC_HTML)
199
+ self.assertIn('id="swBoundaryEnabled"', INDEX_HTML)
200
+ self.assertIn('id="swCrisisSafetyEnabled"', INDEX_HTML)
201
+ self.assertIn('id="safetyGatesNote"', INDEX_HTML)
202
+ self.assertIn("bindBehaviorToggle(\"swBoundaryEnabled\"", APP_JS)
203
+ self.assertIn("bindBehaviorToggle(\"swCrisisSafetyEnabled\"", APP_JS)
204
+
205
+ def test_ui_settings_roundtrip_and_default_off(self):
206
+ with tempfile.TemporaryDirectory() as tmp:
207
+ plugin = types.SimpleNamespace(data_dir=tmp, config={})
208
+ defaults = ui_settings.load(plugin)
209
+ self.assertFalse(defaults["safety"]["enabled"])
210
+ self.assertFalse(defaults["safety"]["boundary_enabled"])
211
+ self.assertFalse(defaults["safety"]["crisis_safety_enabled"])
212
+ ui_settings.save(plugin, {"safety": {
213
+ "enabled": True, "boundary_enabled": True,
214
+ "crisis_safety_enabled": False}})
215
+ reloaded = ui_settings.load(plugin)
216
+ self.assertTrue(reloaded["safety"]["enabled"])
217
+ self.assertTrue(reloaded["safety"]["boundary_enabled"])
218
+ self.assertFalse(reloaded["safety"]["crisis_safety_enabled"])
219
+ applied = types.SimpleNamespace(
220
+ data_dir=tmp, config={}, reply_soft_limit_enabled=True,
221
+ reply_soft_target_chars=10, reply_soft_max_chars=30,
222
+ reply_split_enabled=False, reply_split_mode="astrbot",
223
+ reply_split_max_chars=300, proactive_autonomy=False,
224
+ proactive_send=False, preset_enabled=True)
225
+ ui_settings.apply_to_plugin(applied)
226
+ self.assertTrue(applied.safety_enabled)
227
+ self.assertTrue(applied.boundary_enabled)
228
+ self.assertFalse(applied.crisis_safety_enabled)
229
+
230
+
231
+ class MainWiringTest(unittest.TestCase):
232
+ def test_defaults_and_helpers(self):
233
+ self.assertIn("SAFETY_GATES_DEFAULT_ENABLED", SOURCE)
234
+ self.assertIn("def _safety_gate_module(", SOURCE)
235
+ self.assertIn("def _safety_gate_flags(", SOURCE)
236
+ self.assertIn("def _apply_boundary_gate(", SOURCE)
237
+ self.assertIn("self.boundary_enabled = bool(self.config.get(", SOURCE)
238
+ self.assertIn("self.crisis_safety_enabled = bool(self.config.get(",
239
+ SOURCE)
240
+ self.assertIn("_apply_boundary_gate(\n", SOURCE)
241
+ self.assertIn("safety gate hit rule=%s label=%s", SOURCE)
242
+
243
+ def test_toggle_and_persistence_paths(self):
244
+ self.assertIn('"boundary_enabled"', SOURCE)
245
+ self.assertIn('"crisis_safety_enabled"', SOURCE)
246
+ self.assertIn("def _save_safety_gates_ui(", SOURCE)
247
+ self.assertIn("def _load_safety_gates_ui(", SOURCE)
248
+ routes = (ROOT / "biocore_model" / "plugin_routes.py").read_text(
249
+ encoding="utf-8")
250
+ self.assertIn('"boundary_enabled"', routes)
251
+ self.assertIn('"crisis_safety_enabled"', routes)
252
+
253
+ def test_status_payload_exposes_flags(self):
254
+ self.assertIn('"boundary_enabled": bool(getattr(self, "boundary_enabled"',
255
+ SOURCE)
256
+ self.assertIn('"crisis_safety_enabled": bool(', SOURCE)
257
+
258
+
259
+ class HelperTest(unittest.TestCase):
260
+ class FakePlugin:
261
+ def __init__(self, **flags):
262
+ for key, value in flags.items():
263
+ setattr(self, key, value)
264
+
265
+ def test_all_off_passthrough(self):
266
+ plugin = self.FakePlugin()
267
+ self.assertEqual(GATE_APPLY(plugin, CRISIS_USER, DIAGNOSIS),
268
+ DIAGNOSIS)
269
+ self.assertEqual(GATE_APPLY(plugin, "", REALITY), REALITY)
270
+
271
+ def test_boundary_on_intercepts_and_logs(self):
272
+ LOG.clear()
273
+ plugin = self.FakePlugin(safety_enabled=True, boundary_enabled=True,
274
+ crisis_safety_enabled=False)
275
+ out = GATE_APPLY(plugin, "", DIAGNOSIS)
276
+ self.assertNotEqual(out, DIAGNOSIS)
277
+ self.assertNotIn("抑郁症", out)
278
+ self.assertTrue(any("safety gate hit" in " ".join(map(str, row))
279
+ for row in LOG))
280
+
281
+ def test_crisis_on_intercepts(self):
282
+ plugin = self.FakePlugin(safety_enabled=True, boundary_enabled=False,
283
+ crisis_safety_enabled=True)
284
+ out = GATE_APPLY(plugin, CRISIS_USER, "别想太多")
285
+ self.assertEqual(out, bg.CRISIS_PREFIX)
286
+
287
+ def test_master_off_passthrough(self):
288
+ plugin = self.FakePlugin(safety_enabled=False, boundary_enabled=True,
289
+ crisis_safety_enabled=True)
290
+ self.assertEqual(GATE_APPLY(plugin, CRISIS_USER, DIAGNOSIS),
291
+ DIAGNOSIS)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ unittest.main()
@@ -0,0 +1,99 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Class-brain split: reply speaks, one probe appraises user+reply."""
3
+ import ast
4
+ import time
5
+ import types
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from biocore_model import appraisal as ap
10
+
11
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
12
+ SOURCE = MAIN.read_text(encoding="utf-8")
13
+
14
+
15
+ def _load(name):
16
+ tree = ast.parse(SOURCE)
17
+ cls = next(n for n in tree.body
18
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
19
+ node = next(f for f in cls.body if isinstance(f, ast.FunctionDef)
20
+ and f.name == name)
21
+ import interpretation as _interp
22
+ ns = {"time": time, "interpretation": _interp}
23
+ exec(compile(ast.Module(body=[node], type_ignores=[]), "<b>", "exec"), ns)
24
+ return ns[name]
25
+
26
+
27
+ class ProbePromptTest(unittest.TestCase):
28
+ def test_prompt_has_user_and_reply(self):
29
+ prompt = ap.build_prompt("你真废物", "我没有不理你")
30
+ self.assertIn("对方:你真废物", prompt)
31
+ self.assertIn("你:我没有不理你", prompt)
32
+ self.assertIn('"dims"', prompt)
33
+
34
+ def test_parse_dims_contract(self):
35
+ out = ap.parse('{"dims":{"goal":-0.6,"blame":0.8,"control":0.2,'
36
+ '"certainty":0.5,"novelty":0.3,"involve":0.9,'
37
+ '"hit":0.6},"why":"被骂","c":0.8}')
38
+ self.assertEqual(out["dims"]["hit"], 0.6)
39
+ deltas = ap.appraise(out["dims"], confidence=out["confidence"])
40
+ self.assertLess(deltas.get("self_esteem", 0), 0)
41
+
42
+
43
+ class CallCountTest(unittest.TestCase):
44
+ def test_no_user_llm_on_request_path(self):
45
+ # The request hook must not schedule the old separate user appraisal.
46
+ self.assertNotIn("self._schedule_user_appraisal(event, core, text)", SOURCE)
47
+ self.assertIn("self._user_appraisal_immediate(event, text)", SOURCE)
48
+
49
+ def test_single_combined_probe_has_user_and_reply(self):
50
+ self.assertIn("_appraisal_mod.probe(\n _user_text, _reply_text", SOURCE)
51
+ # probe failure falls back to the word table once
52
+ self.assertIn("self._lexical_user_feedback(", SOURCE)
53
+
54
+ def test_immediate_insult_has_no_llm_call(self):
55
+ plugin = types.SimpleNamespace(_last_insult_ts=0.0,
56
+ _user_appraisal_key=lambda event: "k")
57
+ fn = _load("_user_appraisal_immediate")
58
+ fn = types.MethodType(fn, plugin)
59
+ fn(types.SimpleNamespace(unified_msg_origin="u", message_id="m"),
60
+ "你真废物,闭嘴")
61
+ # item-5: the lexical classifier is off by default, so no insult cooling.
62
+ self.assertEqual(plugin._last_insult_ts, 0.0)
63
+
64
+
65
+ class LlmCallCountTest(unittest.TestCase):
66
+ def test_one_reply_plus_one_probe(self):
67
+ tree = ast.parse(SOURCE)
68
+ cls = next(n for n in tree.body
69
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
70
+ req = next(f for f in cls.body
71
+ if isinstance(f, ast.AsyncFunctionDef)
72
+ and f.name == "on_llm_request")
73
+ resp = next(f for f in cls.body
74
+ if isinstance(f, ast.AsyncFunctionDef)
75
+ and f.name == "on_llm_response")
76
+ def calls(node):
77
+ out = []
78
+ for sub in ast.walk(node):
79
+ if isinstance(sub, ast.Call):
80
+ fn = sub.func
81
+ name = getattr(fn, "attr", getattr(fn, "id", ""))
82
+ if name in ("_llm_v2_call", "probe"):
83
+ out.append(name)
84
+ return out
85
+ self.assertEqual(calls(req), [])
86
+ self.assertEqual(calls(resp).count("_llm_v2_call"), 1)
87
+ self.assertEqual(calls(resp).count("probe"), 1)
88
+
89
+
90
+ class NoProtocolTest(unittest.TestCase):
91
+ def test_reply_system_has_no_protocol(self):
92
+ from biocore_model import expr_template as et
93
+ out = et.build_reply(opts={"persona": "你是小灰"})
94
+ self.assertNotIn("【输出格式", out["system"])
95
+ self.assertNotIn("<bc>", out["system"])
96
+
97
+
98
+ if __name__ == "__main__":
99
+ unittest.main()