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,342 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-199 通用 embedding provider 单测。
3
+
4
+ 覆盖四种模式(off / remote / local / remote_with_local_fallback)、通用
5
+ OpenAI 兼容请求体与鉴权头、超时、维度校验与降级、off 无 IO、旧键兼容。
6
+ 用 stub 取代真实网络与本地模型,不需要 onnxruntime。
7
+ """
8
+ import json
9
+ import os
10
+ import unittest
11
+ from unittest import mock
12
+
13
+ from biocore_memory.recall import embedding as emb
14
+
15
+ ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
+ ENV_KEYS = (
17
+ "BIOCORE_EMBEDDING_MODE", "BIOCORE_EMBEDDING_BASE_URL",
18
+ "BIOCORE_EMBEDDING_KEY", "BIOCORE_EMBEDDING_MODEL",
19
+ "BIOCORE_EMBEDDING_DIM", "BIOCORE_EMBEDDING_TIMEOUT",
20
+ "BIOCORE_EMBEDDING_LOCAL_MODEL", "BIOCORE_EMBEDDING_LOCAL_FALLBACK",
21
+ "BIOCORE_EMBEDDING_LOCAL_TOKENIZER", "BIOCORE_EMBEDDING_LOCAL_THREADS",
22
+ )
23
+
24
+
25
+ class FakeResponse:
26
+ def __init__(self, payload):
27
+ self._data = json.dumps(payload).encode("utf-8")
28
+ self.closed = False
29
+
30
+ def read(self):
31
+ return self._data
32
+
33
+ def close(self):
34
+ self.closed = True
35
+
36
+
37
+ def payload(vector, model="m"):
38
+ return {"data": [{"embedding": list(vector)}], "model": model}
39
+
40
+
41
+ class StubLocal:
42
+ def __init__(self, vector=None, dim=4, fail=False):
43
+ self.vector = list(vector if vector is not None else [1.0, 0.0, 0.0, 0.0])
44
+ self.dim = int(dim)
45
+ self.fail = bool(fail)
46
+ self.calls = 0
47
+ self.model_path = "stub.onnx"
48
+ self.tokenizer_path = ""
49
+ self.threads = 2
50
+
51
+ def encode(self, texts):
52
+ self.calls += 1
53
+ if self.fail:
54
+ raise RuntimeError("local boom")
55
+ return [list(self.vector) for _ in texts]
56
+
57
+
58
+ class EnvIsolated(unittest.TestCase):
59
+ def setUp(self):
60
+ self._env = mock.patch.dict(os.environ, {key: "" for key in ENV_KEYS},
61
+ clear=False)
62
+ self._env.start()
63
+ emb._LOCAL_WARNED.clear()
64
+
65
+ def tearDown(self):
66
+ emb._LOCAL_WARNED.clear()
67
+ self._env.stop()
68
+
69
+
70
+ class ModeDispatchTest(EnvIsolated):
71
+ def test_off_mode_returns_none_without_network_or_local_load(self):
72
+ with mock.patch("urllib.request.urlopen",
73
+ side_effect=AssertionError("no network")), \
74
+ mock.patch.object(emb, "_load_local_embedder",
75
+ side_effect=AssertionError("no local load")):
76
+ provider = emb.load_provider({
77
+ "embedding_mode": "off",
78
+ "embedding_base_url": "https://api.example.com/v1",
79
+ "embedding_model": "bge-m3",
80
+ "embedding_local_model_path": "stub.onnx",
81
+ })
82
+ self.assertIsNone(provider)
83
+
84
+ def test_unknown_mode_behaves_like_off(self):
85
+ with mock.patch("urllib.request.urlopen") as urlopen, \
86
+ mock.patch.object(emb, "_load_local_embedder") as loader:
87
+ self.assertIsNone(emb.load_provider({
88
+ "embedding_mode": "wired-mode",
89
+ "embedding_base_url": "https://api.example.com/v1",
90
+ "embedding_model": "m",
91
+ "embedding_local_model_path": "stub.onnx",
92
+ }))
93
+ self.assertFalse(urlopen.called)
94
+ self.assertFalse(loader.called)
95
+
96
+ def test_legacy_config_without_mode_stays_remote(self):
97
+ provider = emb.load_provider({
98
+ "base_url": "http://127.0.0.1:9/v1", "model": "legacy-model"})
99
+ self.assertIsNotNone(provider)
100
+ self.assertEqual(provider.mode, "remote")
101
+ self.assertEqual(provider.endpoint, "http://127.0.0.1:9/v1/embeddings")
102
+ self.assertEqual(provider.dim, 0)
103
+
104
+ def test_legacy_local_only_config_still_loads(self):
105
+ stub = StubLocal()
106
+ with mock.patch.object(emb, "_load_local_embedder", return_value=stub):
107
+ provider = emb.load_provider({
108
+ "embedding_local_fallback_enabled": True,
109
+ "embedding_local_model_path": "stub.onnx",
110
+ })
111
+ self.assertIsNotNone(provider)
112
+ self.assertTrue(provider.local_only)
113
+ self.assertEqual(emb.local_embedding_status(provider), "ok")
114
+
115
+ def test_mode_aliases_are_accepted(self):
116
+ with mock.patch.object(emb, "_load_local_embedder", return_value=StubLocal()):
117
+ provider = emb.load_provider({
118
+ "embedding_mode": "local_onnx",
119
+ "embedding_local_model_path": "stub.onnx",
120
+ })
121
+ self.assertIsNotNone(provider)
122
+ self.assertEqual(provider.mode, "local")
123
+ remote = emb.load_provider({
124
+ "embedding_mode": "remote_generic",
125
+ "embedding_base_url": "https://api.example.com/v1",
126
+ "embedding_model": "m",
127
+ })
128
+ self.assertIsNotNone(remote)
129
+ self.assertEqual(remote.mode, "remote")
130
+
131
+
132
+ class RemoteGenericModeTest(EnvIsolated):
133
+ def _provider(self, **extra):
134
+ cfg = {"embedding_mode": "remote",
135
+ "embedding_base_url": "https://api.example.com/v1",
136
+ "embedding_model": "bge-m3"}
137
+ cfg.update(extra)
138
+ return emb.load_provider(cfg)
139
+
140
+ def test_remote_request_is_generic_openai_compatible(self):
141
+ provider = self._provider(embedding_api_key="sk-test")
142
+ self.assertIsNotNone(provider)
143
+ with mock.patch("urllib.request.urlopen",
144
+ return_value=FakeResponse(payload([1.0, 0.0, 0.0, 0.0]))) as urlopen:
145
+ vector = provider.embed_text(" 你好 世界 ")
146
+ self.assertEqual(vector, [1.0, 0.0, 0.0, 0.0])
147
+ request = urlopen.call_args[0][0]
148
+ self.assertEqual(request.full_url, "https://api.example.com/v1/embeddings")
149
+ body = json.loads(request.data.decode("utf-8"))
150
+ self.assertEqual(body["model"], "bge-m3")
151
+ self.assertEqual(body["input"], ["你好 世界"])
152
+ self.assertEqual(body["encoding_format"], "float")
153
+ headers = {key.lower(): value for key, value in request.headers.items()}
154
+ self.assertEqual(headers.get("authorization"), "Bearer sk-test")
155
+ self.assertEqual(urlopen.call_args.kwargs.get("timeout"), emb.DEFAULT_TIMEOUT)
156
+
157
+ def test_remote_without_key_sends_no_authorization_header(self):
158
+ provider = self._provider()
159
+ with mock.patch("urllib.request.urlopen",
160
+ return_value=FakeResponse(payload([1.0, 0.0]))) as urlopen:
161
+ provider.embed_text("hi")
162
+ request = urlopen.call_args[0][0]
163
+ headers = {key.lower(): value for key, value in request.headers.items()}
164
+ self.assertNotIn("authorization", headers)
165
+
166
+ def test_base_url_already_ending_in_embeddings_is_not_duplicated(self):
167
+ provider = self._provider(
168
+ embedding_base_url="https://gateway.example.com/v1/embeddings")
169
+ self.assertEqual(provider.endpoint,
170
+ "https://gateway.example.com/v1/embeddings")
171
+
172
+ def test_timeout_from_seconds_and_from_milliseconds(self):
173
+ provider = self._provider(embedding_timeout_s=2.5)
174
+ self.assertAlmostEqual(provider.timeout, 2.5)
175
+ with mock.patch("urllib.request.urlopen",
176
+ return_value=FakeResponse(payload([1.0]))) as urlopen:
177
+ provider.embed_text("hi")
178
+ self.assertAlmostEqual(urlopen.call_args.kwargs.get("timeout"), 2.5)
179
+ provider_ms = self._provider(embedding_timeout_ms=1500)
180
+ self.assertAlmostEqual(provider_ms.timeout, 1.5)
181
+
182
+ def test_dim_match_accepts_vector(self):
183
+ provider = self._provider(embedding_dim=4)
184
+ with mock.patch("urllib.request.urlopen",
185
+ return_value=FakeResponse(payload([0.1, 0.2, 0.3, 0.4]))):
186
+ self.assertEqual(provider.embed_text("hi"), [0.1, 0.2, 0.3, 0.4])
187
+
188
+ def test_dim_mismatch_degrades_to_no_vector(self):
189
+ provider = self._provider(embedding_dim=8)
190
+ with mock.patch("urllib.request.urlopen",
191
+ return_value=FakeResponse(payload([1.0, 0.0, 0.0, 0.0]))):
192
+ with self.assertRaises(ValueError):
193
+ provider.embed_text("hi")
194
+ self.assertIsNone(emb.embed("hi", provider))
195
+
196
+ def test_remote_missing_base_or_model_returns_none(self):
197
+ self.assertIsNone(emb.load_provider({"embedding_mode": "remote"}))
198
+ self.assertIsNone(emb.load_provider({
199
+ "embedding_mode": "remote",
200
+ "embedding_base_url": "https://api.example.com/v1"}))
201
+ self.assertIsNone(emb.load_provider({
202
+ "embedding_mode": "remote", "embedding_model": "m"}))
203
+
204
+
205
+ class LocalModeTest(EnvIsolated):
206
+ def test_local_mode_uses_local_embedder_without_network(self):
207
+ stub = StubLocal(dim=4)
208
+ with mock.patch.object(emb, "_load_local_embedder",
209
+ return_value=stub) as loader, \
210
+ mock.patch("urllib.request.urlopen") as urlopen:
211
+ provider = emb.load_provider({
212
+ "embedding_mode": "local",
213
+ "embedding_local_model_path": "stub.onnx",
214
+ })
215
+ self.assertIsNotNone(provider)
216
+ self.assertEqual(provider.mode, "local")
217
+ vector = emb.embed("你好", provider)
218
+ self.assertEqual(vector, [1.0, 0.0, 0.0, 0.0])
219
+ self.assertEqual(stub.calls, 1)
220
+ self.assertTrue(loader.called)
221
+ self.assertFalse(urlopen.called)
222
+
223
+ def test_local_mode_without_model_path_returns_none(self):
224
+ with mock.patch.object(emb, "_load_local_embedder") as loader:
225
+ self.assertIsNone(emb.load_provider({"embedding_mode": "local"}))
226
+ self.assertFalse(loader.called)
227
+
228
+ def test_local_mode_load_failure_returns_none(self):
229
+ with mock.patch.object(emb, "_load_local_embedder", return_value=False):
230
+ self.assertIsNone(emb.load_provider({
231
+ "embedding_mode": "local",
232
+ "embedding_local_model_path": "missing.onnx",
233
+ }))
234
+
235
+ def test_local_mode_dim_mismatch_degrades(self):
236
+ stub = StubLocal(dim=4, vector=[1.0, 0.0, 0.0])
237
+ with mock.patch.object(emb, "_load_local_embedder", return_value=stub):
238
+ provider = emb.load_provider({
239
+ "embedding_mode": "local",
240
+ "embedding_local_model_path": "stub.onnx",
241
+ "embedding_dim": 4,
242
+ })
243
+ self.assertIsNone(emb.embed("你好", provider))
244
+
245
+ def test_local_mode_keeps_legacy_local_keys(self):
246
+ captured = {}
247
+
248
+ def fake_load(cfg):
249
+ captured.update(cfg)
250
+ return StubLocal(dim=4)
251
+
252
+ with mock.patch.object(emb, "_load_local_embedder", side_effect=fake_load):
253
+ provider = emb.load_provider({
254
+ "embedding_mode": "local",
255
+ "embedding_local_model_path": "stub.onnx",
256
+ "embedding_local_tokenizer_path": "tok",
257
+ "embedding_local_threads": 3,
258
+ })
259
+ self.assertIsNotNone(provider)
260
+ self.assertEqual(captured.get("model_path"), "stub.onnx")
261
+ self.assertEqual(captured.get("tokenizer_path"), "tok")
262
+ self.assertEqual(captured.get("threads"), 3)
263
+
264
+
265
+ class FallbackModeTest(EnvIsolated):
266
+ def _config(self, **extra):
267
+ cfg = {"embedding_mode": "remote_with_local_fallback",
268
+ "embedding_base_url": "https://api.example.com/v1",
269
+ "embedding_model": "m",
270
+ "embedding_local_model_path": "stub.onnx"}
271
+ cfg.update(extra)
272
+ return cfg
273
+
274
+ def test_fallback_remote_success_skips_local(self):
275
+ stub = StubLocal(dim=4)
276
+ with mock.patch.object(emb, "_load_local_embedder", return_value=stub):
277
+ provider = emb.load_provider(self._config())
278
+ with mock.patch("urllib.request.urlopen",
279
+ return_value=FakeResponse(payload([0.0, 1.0, 0.0, 0.0]))):
280
+ vector = emb.embed("你好", provider)
281
+ self.assertEqual(vector, [0.0, 1.0, 0.0, 0.0])
282
+ self.assertEqual(stub.calls, 0)
283
+
284
+ def test_fallback_remote_failure_uses_local(self):
285
+ stub = StubLocal(dim=4)
286
+ with mock.patch.object(emb, "_load_local_embedder", return_value=stub):
287
+ provider = emb.load_provider(self._config())
288
+ with mock.patch("urllib.request.urlopen", side_effect=OSError("remote down")):
289
+ vector = emb.embed("你好", provider)
290
+ self.assertEqual(vector, [1.0, 0.0, 0.0, 0.0])
291
+ self.assertEqual(stub.calls, 1)
292
+
293
+ def test_fallback_dim_mismatch_uses_local(self):
294
+ stub = StubLocal(dim=8, vector=[1.0] + [0.0] * 7)
295
+ with mock.patch.object(emb, "_load_local_embedder", return_value=stub):
296
+ provider = emb.load_provider(self._config(embedding_dim=8))
297
+ with mock.patch("urllib.request.urlopen",
298
+ return_value=FakeResponse(payload([1.0, 0.0, 0.0, 0.0]))):
299
+ vector = emb.embed("你好", provider)
300
+ self.assertEqual(vector, [1.0] + [0.0] * 7)
301
+ self.assertEqual(stub.calls, 1)
302
+
303
+ def test_fallback_without_remote_uses_local_only(self):
304
+ stub = StubLocal(dim=4)
305
+ with mock.patch.object(emb, "_load_local_embedder", return_value=stub):
306
+ provider = emb.load_provider({
307
+ "embedding_mode": "remote_with_local_fallback",
308
+ "embedding_local_model_path": "stub.onnx",
309
+ })
310
+ self.assertIsNotNone(provider)
311
+ self.assertTrue(provider.local_only)
312
+ self.assertEqual(emb.embed("你好", provider), [1.0, 0.0, 0.0, 0.0])
313
+
314
+ def test_fallback_without_local_path_is_disabled(self):
315
+ provider = emb.load_provider({
316
+ "embedding_mode": "remote_with_local_fallback",
317
+ "embedding_base_url": "https://api.example.com/v1",
318
+ "embedding_model": "m",
319
+ })
320
+ self.assertIsNotNone(provider)
321
+ self.assertFalse(provider.fallback_config["enabled"])
322
+
323
+
324
+ class SchemaDefaultsTest(unittest.TestCase):
325
+ def test_new_keys_exist_and_mode_defaults_auto(self):
326
+ with open(os.path.join(ROOT, "_conf_schema.json"),
327
+ encoding="utf-8-sig") as handle:
328
+ schema = json.loads(handle.read())
329
+ mode = schema["embedding_mode"]
330
+ self.assertEqual(mode["default"], "auto")
331
+ self.assertEqual(set(mode["options"]),
332
+ {"auto", "off", "remote", "local",
333
+ "remote_with_local_fallback"})
334
+ for key in ("embedding_base_url", "embedding_model",
335
+ "embedding_api_key"):
336
+ self.assertIn(key, schema)
337
+ self.assertEqual(schema[key]["default"], "")
338
+ self.assertEqual(schema["embedding_dim"]["default"], 0)
339
+
340
+
341
+ if __name__ == "__main__":
342
+ unittest.main()
@@ -0,0 +1,114 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Emotion geometry tests: prototypes, mixed distributions, tone mixing."""
3
+ import unittest
4
+
5
+ from biocore_model import emotion_geometry as eg
6
+ from biocore_model import expr_tone as et
7
+ from biocore_model import stages as st
8
+
9
+
10
+ def core_state(proto):
11
+ state = {"v": proto.get("v", 0.0)}
12
+ for name in ("a", "d", "social", "stress", "trust"):
13
+ if name in proto:
14
+ state[name] = (float(proto[name]) + 1.0) / 2.0
15
+ return state
16
+
17
+
18
+ class PrototypeTest(unittest.TestCase):
19
+ def test_each_prototype_wins(self):
20
+ for label, proto in eg.PROTOTYPES.items():
21
+ out = eg.classify(core_state(proto))
22
+ self.assertEqual(out["label"], label, label)
23
+ self.assertGreater(out["probs"][label], 0.5, label)
24
+
25
+ def test_probs_sum_to_one(self):
26
+ out = eg.classify(core_state(eg.PROTOTYPES["joy"]))
27
+ self.assertAlmostEqual(sum(out["probs"].values()), 1.0, places=3)
28
+
29
+ def test_mixed_state_has_low_confidence(self):
30
+ a = core_state(eg.PROTOTYPES["sadness"])
31
+ b = core_state(eg.PROTOTYPES["anger"])
32
+ mixed = {key: (a.get(key, 0.5) + b.get(key, 0.5)) / 2.0 for key in a}
33
+ out = eg.classify(mixed)
34
+ single = eg.classify(a)
35
+ self.assertLess(out["confidence"], single["confidence"])
36
+ self.assertLess(out["probs"][out["label"]], 0.8)
37
+
38
+ def test_missing_dimensions_do_not_crash_and_lower_confidence(self):
39
+ full = eg.classify(core_state(eg.PROTOTYPES["joy"]))
40
+ partial = eg.classify({"v": 0.7, "a": 0.7})
41
+ self.assertIn(partial["label"], eg.LABELS)
42
+ self.assertLess(partial["confidence"], full["confidence"])
43
+
44
+ def test_temperature_changes_entropy(self):
45
+ state = core_state(eg.PROTOTYPES["fear"])
46
+ cold = eg.classify(state, temperature=0.08)
47
+ hot = eg.classify(state, temperature=0.8)
48
+ self.assertLess(eg.entropy(cold["probs"]), eg.entropy(hot["probs"]))
49
+ self.assertGreaterEqual(cold["probs"][cold["label"]],
50
+ hot["probs"][hot["label"]])
51
+
52
+ def test_confidence_and_prob_bounds(self):
53
+ out = eg.classify({"v": 0.0, "a": 0.5, "d": 0.5})
54
+ self.assertGreaterEqual(out["confidence"], 0.0)
55
+ self.assertLessEqual(out["confidence"], 1.0)
56
+ for prob in out["probs"].values():
57
+ self.assertGreaterEqual(prob, 0.0)
58
+ self.assertLessEqual(prob, 1.0)
59
+
60
+ def test_single_prototype_has_low_entropy(self):
61
+ out = eg.classify(core_state(eg.PROTOTYPES["joy"]))
62
+ self.assertLess(eg.entropy(out["probs"]), 2.0)
63
+
64
+ def test_top2_structure(self):
65
+ out = eg.classify(core_state(eg.PROTOTYPES["trust"]))
66
+ self.assertEqual(len(out["top2"]), 2)
67
+ self.assertIn("label", out["top2"][0])
68
+ self.assertEqual(eg.top_probs(out, 3)[0]["label"], out["label"])
69
+
70
+
71
+ class ToneTest(unittest.TestCase):
72
+ def test_no_probs_is_backward_compatible(self):
73
+ old = et.tone_block("sadness", emotion="sadness", intensity=0.6,
74
+ self_esteem=0.6)
75
+ self.assertEqual(old["tendency"], "sadness")
76
+ self.assertIn("少说两句", old["text"])
77
+ self.assertFalse(old["mixed"])
78
+
79
+ def test_single_distribution_keeps_tendency(self):
80
+ probs = {"joy": 0.8, "trust": 0.1, "sadness": 0.05,
81
+ "anger": 0.02, "fear": 0.02, "loneliness": 0.01}
82
+ out = et.tone_block("", probs=probs)
83
+ self.assertEqual(out["tendency"], "joy")
84
+ self.assertIn("分享", out["text"])
85
+
86
+ def test_mixed_distribution_uses_mixed_branch(self):
87
+ probs = {"sadness": 0.36, "anger": 0.33, "fear": 0.12,
88
+ "joy": 0.08, "loneliness": 0.07, "trust": 0.04}
89
+ out = et.tone_block("", probs=probs)
90
+ self.assertTrue(out["mixed"])
91
+ self.assertEqual(out["tendency"], "mixed")
92
+ self.assertIn("混合情绪", out["text"])
93
+ for token in ("轻", "慢", "留点空白"):
94
+ self.assertIn(token, out["text"])
95
+
96
+ def test_tone_stage_attaches_distribution(self):
97
+ ctx = {"state": core_state(eg.PROTOTYPES["joy"]),
98
+ "meta": {"template_opts": {}}}
99
+ out = st.expr_tone().run(ctx)
100
+ self.assertIn("dist", out["meta"]["expr_tone"])
101
+ self.assertIn("top1", out["meta"]["expr_tone"])
102
+
103
+
104
+ class WiringStaticTest(unittest.TestCase):
105
+ def test_page_debug_exposes_emotion_dist(self):
106
+ from pathlib import Path
107
+ source = (Path(__file__).resolve().parents[2] / "main.py").read_text(
108
+ encoding="utf-8")
109
+ self.assertIn("emotion_geometry", source)
110
+ self.assertIn("emotion_dist", source)
111
+
112
+
113
+ if __name__ == "__main__":
114
+ unittest.main()
@@ -0,0 +1,214 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Emotion probe tests: model-chosen deltas, protections, feedback integration."""
3
+ import asyncio
4
+ import json
5
+ import re
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from biocore_model import emotion_probe as ep
10
+ from biocore_model import feedback_flow as ff
11
+
12
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
13
+ FENCE = "\x60\x60\x60"
14
+
15
+
16
+ class FakeCore:
17
+ def __init__(self):
18
+ self._snapshot = {"v": 0.1, "a": 0.4, "stress": 0.2,
19
+ "self_esteem": 0.5, "social": 0.5, "trust": 0.5}
20
+
21
+ def snapshot(self):
22
+ return dict(self._snapshot)
23
+
24
+ def load_state(self, state):
25
+ self._snapshot.update(state)
26
+
27
+
28
+ def payload(deltas, c=0.8, why="有点被扎到"):
29
+ return json.dumps({"deltas": deltas, "why": why, "c": c})
30
+
31
+
32
+ class ParseTest(unittest.TestCase):
33
+ def setUp(self):
34
+ ep.reset_rate()
35
+
36
+ def test_normal_deltas(self):
37
+ out = ep.parse(payload({"v": -0.3, "a": 0.1}), core=object(), now=1000.0)
38
+ self.assertEqual(out["deltas"], {"v": -0.2, "a": 0.1})
39
+ self.assertEqual(out["why"], "有点被扎到")
40
+ self.assertEqual(out["source"], "probe")
41
+
42
+ def test_empty_deltas_is_valid_but_no_write(self):
43
+ out = ep.parse(payload({}, c=0.9, why="没影响"), core=object(), now=1000.0)
44
+ self.assertEqual(out["deltas"], {})
45
+ self.assertEqual(ep.to_feedback_payload(out), {})
46
+
47
+ def test_unknown_fields_dropped(self):
48
+ out = ep.parse(payload({"mood": 0.5, "v": -0.1}), core=object(), now=1000.0)
49
+ self.assertEqual(out["deltas"], {"v": -0.1})
50
+
51
+ def test_per_item_clamp(self):
52
+ out = ep.parse(payload({"v": 0.9, "trust": -0.5}), core=object(), now=1000.0)
53
+ self.assertEqual(out["deltas"], {"v": 0.2, "trust": -0.2})
54
+
55
+ def test_rate_limit_same_minute(self):
56
+ core = object()
57
+ first = ep.parse(payload({"v": 0.2, "a": 0.2}), core=core, now=1000.0)
58
+ self.assertEqual(first["deltas"], {"v": 0.2, "a": 0.2})
59
+ second = ep.parse(payload({"v": 0.2}), core=core, now=1001.0)
60
+ self.assertEqual(second["deltas"], {})
61
+ third = ep.parse(payload({"v": 0.2}), core=core, now=1002.0)
62
+ self.assertEqual(third["deltas"], {})
63
+
64
+ def test_rate_limit_resets_next_minute(self):
65
+ core = object()
66
+ ep.parse(payload({"v": 0.2, "a": 0.2}), core=core, now=1000.0)
67
+ later = ep.parse(payload({"v": 0.2}), core=core, now=1060.0)
68
+ self.assertEqual(later["deltas"], {"v": 0.2})
69
+
70
+ def test_low_confidence_dropped(self):
71
+ self.assertEqual(ep.parse(payload({"v": -0.2}, c=0.39), core=object(),
72
+ now=1000.0), {})
73
+
74
+ def test_garbage_json(self):
75
+ self.assertEqual(ep.parse("我不是 JSON", core=object(), now=1000.0), {})
76
+ self.assertEqual(ep.parse("", core=object(), now=1000.0), {})
77
+
78
+ def test_fenced_json(self):
79
+ raw = "好的\n" + FENCE + "json\n" + payload({"v": -0.1}) + "\n" + FENCE
80
+ out = ep.parse(raw, core=object(), now=1000.0)
81
+ self.assertEqual(out["deltas"], {"v": -0.1})
82
+
83
+ def test_why_truncated_to_30(self):
84
+ out = ep.parse(payload({}, why="x" * 80), core=object(), now=1000.0)
85
+ self.assertEqual(len(out["why"]), ep.WHY_MAX_CHARS)
86
+
87
+
88
+ class ProbeAsyncTest(unittest.IsolatedAsyncioTestCase):
89
+ def setUp(self):
90
+ ep.reset_rate()
91
+
92
+ async def test_success(self):
93
+ async def call(prompt):
94
+ return payload({"v": -0.2, "trust": -0.05}, c=0.9, why="被质疑")
95
+ out = await ep.probe("好。", "你什么意思", call, core=object(), now=2000.0)
96
+ self.assertEqual(out["deltas"], {"v": -0.2, "trust": -0.05})
97
+ self.assertEqual(out["why"], "被质疑")
98
+
99
+ async def test_timeout_returns_empty(self):
100
+ async def call(prompt):
101
+ await asyncio.sleep(1.0)
102
+ return payload({"v": -0.2})
103
+ self.assertEqual(await ep.probe("x", "y", call, timeout=0.05,
104
+ core=object(), now=2000.0), {})
105
+
106
+ async def test_exception_returns_empty(self):
107
+ async def call(prompt):
108
+ raise RuntimeError("boom")
109
+ self.assertEqual(await ep.probe("x", "y", call, core=object(),
110
+ now=2000.0), {})
111
+
112
+ async def test_prompt_short_and_has_contract(self):
113
+ seen = {}
114
+ async def call(prompt):
115
+ seen["prompt"] = prompt
116
+ return payload({}, c=0.9)
117
+ await ep.probe("回复", "消息", call, core=object(), now=2000.0)
118
+ self.assertLessEqual(len(ep._INSTRUCTION), 120)
119
+ for token in ("deltas", "why", "self_esteem", "无影响", "对方刚说", "回复"):
120
+ self.assertIn(token, seen["prompt"])
121
+
122
+
123
+ class FeedbackIntegrationTest(unittest.TestCase):
124
+ def setUp(self):
125
+ ep.reset_rate()
126
+ ff.reset_seen()
127
+
128
+ def tearDown(self):
129
+ ff.reset_seen()
130
+
131
+ def test_write_takes_effect(self):
132
+ core = FakeCore()
133
+ out = ep.parse(payload({"v": -0.2, "stress": 0.1, "trust": -0.05},
134
+ c=1.0), core=object(), now=3000.0)
135
+ meta = ep.to_feedback_payload(out)
136
+ self.assertIn("valence_delta", meta)
137
+ self.assertIn("self_esteem_delta" if False else "stress_delta", meta)
138
+ result = ff.apply_llm(meta, core, now=3000.0)
139
+ self.assertTrue(result["written"])
140
+ self.assertLess(core.snapshot()["v"], 0.1)
141
+
142
+ def test_idempotent_same_now(self):
143
+ core = FakeCore()
144
+ meta = ep.to_feedback_payload(ep.parse(payload({"v": -0.2}, c=1.0),
145
+ core=object(), now=4000.0))
146
+ first = ff.apply_llm(meta, core, now=4000.0)
147
+ second = ff.apply_llm(meta, core, now=4000.0)
148
+ self.assertTrue(first["written"])
149
+ self.assertEqual(second["deltas"], {})
150
+
151
+ def test_empty_deltas_no_write(self):
152
+ core = FakeCore()
153
+ meta = ep.to_feedback_payload(ep.parse(payload({}), core=object(), now=5000.0))
154
+ self.assertEqual(meta, {})
155
+ result = ff.apply_llm(meta, core, now=5000.0)
156
+ self.assertFalse(result["written"])
157
+
158
+
159
+ class _Recorder:
160
+ """Minimal logger stub; keeps tests free of the stdlib logging import."""
161
+
162
+ def __init__(self):
163
+ self.lines = []
164
+
165
+ def _log(self, msg, *args, **kwargs):
166
+ try:
167
+ self.lines.append(str(msg) % args if args else str(msg))
168
+ except Exception:
169
+ self.lines.append(str(msg))
170
+
171
+ debug = info = warning = error = exception = critical = _log
172
+
173
+
174
+ class RawDiagnosticTest(unittest.TestCase):
175
+ def _with_recorder(self, call):
176
+ rec = _Recorder()
177
+ saved = ep.logger
178
+ ep.logger = rec
179
+ try:
180
+ call()
181
+ finally:
182
+ ep.logger = saved
183
+ return rec
184
+
185
+ def test_invalid_raw_is_logged(self):
186
+ rec = self._with_recorder(
187
+ lambda: ep.parse("我不是JSON", core=object(), now=1000.0))
188
+ self.assertTrue(any("emotion probe raw=" in line for line in rec.lines))
189
+
190
+ def test_low_confidence_raw_is_logged(self):
191
+ rec = self._with_recorder(
192
+ lambda: ep.parse(payload({"v": -0.2}, c=0.1), core=object(), now=1000.0))
193
+ self.assertTrue(any("emotion probe raw=" in line for line in rec.lines))
194
+
195
+
196
+ class WiringStaticTest(unittest.TestCase):
197
+ def test_main_consumes_new_contract(self):
198
+ source = MAIN.read_text(encoding="utf-8")
199
+ self.assertIn("emotion_probe_enabled", source)
200
+ self.assertIn("feedback_payload", source)
201
+ self.assertIn("appraisal src=reply dims=%s deltas=%s c=%.2f", source)
202
+ self.assertIn("core=_core, now=time.time()", source)
203
+ self.assertIn("asyncio.create_task(_emotion_probe_task())", source)
204
+ self.assertIn("_probe_raw", source)
205
+ self.assertIn("isinstance(_raw, str)", source)
206
+ self.assertIn("[biocore] emotion probe raw=%r", source)
207
+ self.assertIn("[biocore] emotion probe start", source)
208
+ self.assertIn("emotion probe task failed", source)
209
+ self.assertIn("emotion probe llm returned None", source)
210
+ self.assertIn("exc_info=True", source)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ unittest.main()