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,122 @@
1
+ # -*- coding: utf-8 -*-
2
+ """User-message classification + empathy/insult wiring tests."""
3
+ import ast
4
+ import types
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ from biocore_model import feedback_user as fu
9
+ import interpretation
10
+
11
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
12
+ SOURCE = MAIN.read_text(encoding="utf-8")
13
+ NOW = 1_700_000_000.0
14
+
15
+
16
+ def _load_insult_helper():
17
+ tree = ast.parse(SOURCE)
18
+ cls = next(n for n in tree.body
19
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
20
+ node = next(f for f in cls.body
21
+ if isinstance(f, ast.FunctionDef) and f.name == "_autonomy_insult_blocked")
22
+ ns = {}
23
+ exec(compile(ast.Module(body=[node], type_ignores=[]), "<insult>", "exec"), ns)
24
+ return ns[node.name]
25
+
26
+
27
+ INSULT_BLOCKED = _load_insult_helper()
28
+
29
+
30
+ class ClassifyTest(unittest.TestCase):
31
+ def test_flag_is_off_by_default(self):
32
+ self.assertIs(interpretation.USER_CLASSIFY_ENABLED, False)
33
+
34
+ def test_no_word_table_remains(self):
35
+ self.assertFalse(hasattr(interpretation, "_USER_WORDS"))
36
+
37
+ def test_insult_text_is_neutral(self):
38
+ out = interpretation.classify_user_msg("你真废物,闭嘴")
39
+ self.assertEqual(out["kind"], "neutral")
40
+ self.assertLess(out["confidence"], 0.4)
41
+
42
+ def test_reject_text_is_neutral(self):
43
+ self.assertEqual(
44
+ interpretation.classify_user_msg("不用了,别说了")["kind"],
45
+ "neutral")
46
+
47
+ def test_sad_text_is_neutral(self):
48
+ self.assertEqual(
49
+ interpretation.classify_user_msg("我今天特别难受,想哭")["kind"],
50
+ "neutral")
51
+
52
+ def test_praise_text_is_neutral(self):
53
+ self.assertEqual(
54
+ interpretation.classify_user_msg("你好厉害,谢谢")["kind"],
55
+ "neutral")
56
+
57
+ def test_affectionate_text_is_neutral(self):
58
+ self.assertEqual(
59
+ interpretation.classify_user_msg("想你了,抱抱")["kind"], "neutral")
60
+
61
+ def test_empty_is_neutral(self):
62
+ self.assertEqual(interpretation.classify_user_msg("")["kind"],
63
+ "neutral")
64
+
65
+ def test_bounds(self):
66
+ out = interpretation.classify_user_msg("谢谢你,你真好,太厉害了")
67
+ self.assertGreaterEqual(out["intensity"], 0.0)
68
+ self.assertLessEqual(out["intensity"], 1.0)
69
+ self.assertGreaterEqual(out["confidence"], 0.0)
70
+ self.assertLessEqual(out["confidence"], 1.0)
71
+
72
+ def test_all_kinds_share_neutral_shape(self):
73
+ for text in ("我很难受,你真废物", "嗯", "晚安"):
74
+ out = interpretation.classify_user_msg(text)
75
+ self.assertEqual(out["kind"], "neutral", text)
76
+
77
+ class FeedbackEventTest(unittest.TestCase):
78
+ def test_sad_empathy_raises_stress(self):
79
+ out = fu.on_message({"stress": 0.1}, {"kind": "sad_empathy",
80
+ "id": "m1", "ts": NOW}, NOW)
81
+ self.assertGreater(out["deltas"]["stress"], 0.0)
82
+ self.assertGreater(out["deltas"]["social_pain"], 0.0)
83
+
84
+ def test_affection_raises_social_lowers_stress(self):
85
+ out = fu.on_message({"social": 0.5, "stress": 0.2},
86
+ {"kind": "affection", "id": "m2", "ts": NOW}, NOW)
87
+ self.assertGreater(out["deltas"]["social"], 0.0)
88
+ self.assertLess(out["deltas"]["stress"], 0.0)
89
+
90
+ def test_classifier_kinds_are_accepted(self):
91
+ for kind in ("neutral", "sad", "affectionate", "insult", "reject", "praise"):
92
+ out = fu.on_message({}, {"kind": kind, "id": kind, "ts": NOW}, NOW)
93
+ self.assertEqual(out["rejected"], [], kind)
94
+
95
+
96
+ class InsultGateTest(unittest.TestCase):
97
+ def test_blocked_within_30_minutes(self):
98
+ plugin = types.SimpleNamespace(_last_insult_ts=NOW - 100)
99
+ self.assertTrue(INSULT_BLOCKED(plugin, NOW))
100
+
101
+ def test_not_blocked_after_30_minutes(self):
102
+ plugin = types.SimpleNamespace(_last_insult_ts=NOW - 2000)
103
+ self.assertFalse(INSULT_BLOCKED(plugin, NOW))
104
+
105
+ def test_no_insult_not_blocked(self):
106
+ self.assertFalse(INSULT_BLOCKED(types.SimpleNamespace(), NOW))
107
+
108
+
109
+ class WiringStaticTest(unittest.TestCase):
110
+ def test_main_keeps_helpers_behind_default_off_switch(self):
111
+ self.assertIn("classify_user_msg(text)", SOURCE)
112
+ self.assertIn('getattr(self, "lexical_fallback_enabled", False)',
113
+ SOURCE)
114
+ self.assertIn('self.config.get("lexical_fallback_enabled", False)',
115
+ SOURCE)
116
+ self.assertIn('"sad_empathy"', SOURCE)
117
+ self.assertIn('"affection"', SOURCE)
118
+ self.assertIn("insult_cooldown", SOURCE)
119
+ self.assertIn("[biocore] user kind=%s conf=%.2f deltas=%s", SOURCE)
120
+
121
+ if __name__ == "__main__":
122
+ unittest.main()
@@ -0,0 +1,117 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Value ordering: ten dimensions, loss aversion, sunk cost, status quo."""
3
+ import unittest
4
+
5
+ from biocore_model import value_order as vo
6
+
7
+ class DefaultsTest(unittest.TestCase):
8
+ def test_ten_dimensions(self):
9
+ table = vo.defaults()
10
+ self.assertEqual(len(table), 10)
11
+ self.assertEqual(set(table), set(vo.DIMENSIONS))
12
+
13
+ def test_normalize_fills_and_clamps(self):
14
+ table = vo.normalize({"security": 9, "benevolence": -3})
15
+ self.assertEqual(table["security"], vo.WEIGHT_CEIL)
16
+ self.assertEqual(table["benevolence"], vo.WEIGHT_FLOOR)
17
+ self.assertNotIn("honesty", table)
18
+ self.assertIn("universalism", table)
19
+
20
+ class UpdateTest(unittest.TestCase):
21
+ def test_update_moves_slowly(self):
22
+ before = vo.defaults()
23
+ after = vo.update(before, {"benevolence": 0.15})
24
+ self.assertLess(after["benevolence"] - before["benevolence"], 0.02)
25
+ self.assertGreater(after["benevolence"], before["benevolence"])
26
+
27
+ def test_update_bounded(self):
28
+ table = vo.defaults()
29
+ for _ in range(200):
30
+ table = vo.update(table, {"power": 5.0})
31
+ self.assertLessEqual(table["power"], vo.WEIGHT_CEIL)
32
+ self.assertGreaterEqual(table["power"], vo.WEIGHT_FLOOR)
33
+
34
+ def test_unknown_dimension_ignored(self):
35
+ table = vo.update(vo.defaults(), {"flying": 1.0})
36
+ self.assertEqual(set(table), set(vo.DIMENSIONS))
37
+
38
+ class LossAversionTest(unittest.TestCase):
39
+ def test_loss_counts_double(self):
40
+ table = vo.defaults()
41
+ gain = vo.rank([{"name": "gain", "gains": {"benevolence": 0.5}}],
42
+ table)["ranked"][0]["score"]
43
+ loss = vo.rank([{"name": "loss", "losses": {"benevolence": 0.5}}],
44
+ table)["ranked"][0]["score"]
45
+ self.assertAlmostEqual(loss, -vo.LOSS_MULTIPLIER * gain, places=4)
46
+
47
+ class SunkCostTest(unittest.TestCase):
48
+ def test_grows_with_age(self):
49
+ young = vo.sunk_cost(10)
50
+ old = vo.sunk_cost(730)
51
+ self.assertGreater(old, young)
52
+ self.assertLessEqual(old, vo.SUNK_COST_CAP)
53
+
54
+ def test_capped(self):
55
+ self.assertEqual(vo.sunk_cost(100000), vo.SUNK_COST_CAP)
56
+
57
+ class StatusQuoTest(unittest.TestCase):
58
+ def test_threshold_in_range(self):
59
+ value = vo.status_quo_threshold(vo.defaults(), 365)
60
+ self.assertGreaterEqual(value, 0.05)
61
+ self.assertLessEqual(value, vo.STATUS_QUO_CAP)
62
+
63
+ def test_small_gain_keeps_status_quo(self):
64
+ options = [
65
+ {"name": "保持现状", "keeps_status_quo": True},
66
+ {"name": "小改变", "gains": {"stimulation": 0.02}},
67
+ ]
68
+ out = vo.rank(options, vo.defaults(), {"investment": 0.8,
69
+ "relationship_age_days": 365})
70
+ self.assertEqual(out["chosen"], "保持现状")
71
+
72
+ def test_big_gain_wins(self):
73
+ options = [
74
+ {"name": "保持现状", "keeps_status_quo": True},
75
+ {"name": "大改变", "gains": {"security": 1.0, "benevolence": 1.0}},
76
+ ]
77
+ out = vo.rank(options, vo.defaults(), {"investment": 0.2,
78
+ "relationship_age_days": 30})
79
+ self.assertEqual(out["chosen"], "大改变")
80
+
81
+ class NecessityTest(unittest.TestCase):
82
+ def test_priority_order(self):
83
+ rows = vo.necessity_priority(vo.defaults())
84
+ self.assertEqual(len(rows), 4)
85
+ self.assertEqual(rows[0]["need"], "被需要")
86
+ self.assertGreaterEqual(rows[0]["value"], rows[-1]["value"])
87
+
88
+ def test_custom_table_reorders(self):
89
+ table = vo.defaults()
90
+ table["security"] = 1.0
91
+ table["benevolence"] = 0.2
92
+ rows = vo.necessity_priority(table)
93
+ self.assertEqual(rows[0]["need"], "安全感")
94
+
95
+ class FallbackTest(unittest.TestCase):
96
+ def test_disabled_keeps_ranking(self):
97
+ out = vo.rank([{"name": "a", "gains": {"security": 0.3}}],
98
+ vo.defaults(), {}, flags={"enabled": False})
99
+ self.assertFalse(out["enabled"])
100
+ self.assertEqual(out["chosen"], "a")
101
+
102
+ def test_empty_options(self):
103
+ out = vo.rank([], vo.defaults(), {})
104
+ self.assertIsNone(out["chosen"])
105
+ self.assertEqual(out["ranked"], [])
106
+
107
+ def test_malformed(self):
108
+ out = vo.rank([None, "x", {"name": "ok"}], "bad", "bad")
109
+ self.assertEqual(len(out["ranked"]), 3)
110
+
111
+ class DirectiveTest(unittest.TestCase):
112
+ def test_directive(self):
113
+ out = vo.directives({"enabled": True, "chosen": "保持现状"})
114
+ self.assertEqual(out["chosen"], "保持现状")
115
+
116
+ if __name__ == "__main__":
117
+ unittest.main()
@@ -0,0 +1,105 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Voice bank: loading, state matching, cooldown, picking."""
3
+ import random
4
+ import unittest
5
+
6
+ from biocore_model import voice_bank as vb
7
+
8
+ NOW = 1790000000.0
9
+
10
+ BANK = [
11
+ {"phrase": "少来。", "when": {"v_min": 0.0, "v_max": 1.0, "stages": ["close"]},
12
+ "weight": 2.0, "cooldown_s": 3600},
13
+ {"phrase": "有点累。", "when": {"v_min": -1.0, "v_max": 0.0,
14
+ "tension_min": 0.5}, "weight": 3.0},
15
+ {"phrase": "怎么了?", "when": {"v_min": -1.0, "v_max": 0.3},
16
+ "weight": 1.0, "cooldown_s": 60},
17
+ ]
18
+
19
+ class LoadTest(unittest.TestCase):
20
+ def test_drops_malformed_and_duplicates(self):
21
+ rows = vb.load([None, "x", {"phrase": ""}, {"phrase": "嗯"},
22
+ {"phrase": "嗯"}, {"phrase": "y" * 80}])
23
+ self.assertEqual([row["phrase"] for row in rows], ["嗯"])
24
+
25
+ def test_normalizes_stage_string_and_range(self):
26
+ row = vb.load([{"phrase": "好", "when": {"stages": "close",
27
+ "v_min": 0.8, "v_max": 0.2}}])[0]
28
+ self.assertEqual(row["when"]["stages"], ["close"])
29
+ self.assertEqual(row["when"]["v_min"], 0.2)
30
+ self.assertEqual(row["when"]["v_max"], 0.8)
31
+
32
+ def test_defaults_filled(self):
33
+ row = vb.load([{"phrase": "嗯"}])[0]
34
+ self.assertEqual(row["weight"], 1.0)
35
+ self.assertEqual(row["cooldown_s"], vb.DEFAULT_COOLDOWN_S)
36
+ self.assertEqual(len(row["when"]["stages"]), len(vb.STAGES))
37
+
38
+ class MatchTest(unittest.TestCase):
39
+ def test_v_range(self):
40
+ row = vb.load(BANK)[0]
41
+ self.assertTrue(vb.matches(row, {"v": 0.5}, "close"))
42
+ self.assertFalse(vb.matches(row, {"v": -0.5}, "close"))
43
+
44
+ def test_stage(self):
45
+ row = vb.load(BANK)[0]
46
+ self.assertFalse(vb.matches(row, {"v": 0.5}, "stranger"))
47
+
48
+ def test_tension_band(self):
49
+ row = vb.load(BANK)[1]
50
+ self.assertTrue(vb.matches(row, {"v": -0.2, "tension": 0.7}, "normal"))
51
+ self.assertFalse(vb.matches(row, {"v": -0.2, "tension": 0.2}, "normal"))
52
+
53
+ class CooldownTest(unittest.TestCase):
54
+ def test_cooldown_blocks(self):
55
+ out = vb.pick({"v": -0.2}, {"stage": "normal"},
56
+ {"怎么了?": NOW - 10}, NOW, BANK)
57
+ self.assertNotIn("怎么了?", out)
58
+
59
+ def test_cooldown_expires(self):
60
+ out = vb.pick({"v": -0.2}, {"stage": "normal"},
61
+ {"怎么了?": NOW - 120}, NOW, BANK)
62
+ self.assertIn("怎么了?", out)
63
+
64
+ class PickTest(unittest.TestCase):
65
+ def test_empty_bank(self):
66
+ self.assertEqual(vb.pick({}, {}, {}, NOW, []), [])
67
+ self.assertEqual(vb.pick({}, {}, {}, NOW), [])
68
+
69
+ def test_at_most_two_and_deterministic(self):
70
+ out = vb.pick({"v": -0.2, "tension": 0.8}, {"stage": "normal"},
71
+ {}, NOW, BANK)
72
+ self.assertLessEqual(len(out), 2)
73
+ self.assertEqual(out[0], "有点累。")
74
+
75
+ def test_rng_picks_within_candidates(self):
76
+ rng = random.Random(7)
77
+ out = vb.pick({"v": -0.2, "tension": 0.8}, {"stage": "normal"},
78
+ {}, NOW, BANK, rng=rng)
79
+ self.assertTrue(set(out).issubset({"有点累。", "怎么了?"}))
80
+ self.assertEqual(len(out), len(set(out)))
81
+
82
+ def test_mark_used_stamps(self):
83
+ used = vb.mark_used({}, ["嗯"], NOW)
84
+ self.assertEqual(used["嗯"], NOW)
85
+ used = vb.mark_used(used, ["啊"], NOW + 5)
86
+ self.assertEqual(used["啊"], NOW + 5)
87
+ self.assertEqual(used["嗯"], NOW)
88
+
89
+ def test_available_sorted_by_weight(self):
90
+ rows = vb.available(BANK, {"v": -0.2, "tension": 0.8},
91
+ {"stage": "normal"}, {}, NOW)
92
+ weights = [row["weight"] for row in rows]
93
+ self.assertEqual(weights, sorted(weights, reverse=True))
94
+
95
+ class StatsTest(unittest.TestCase):
96
+ def test_counts(self):
97
+ out = vb.stats(BANK)
98
+ self.assertEqual(out["total"], 3)
99
+ # Entries without an explicit stage list apply to every stage.
100
+ self.assertEqual(out["by_stage"]["close"], 3)
101
+ self.assertEqual(out["by_stage"]["normal"], 2)
102
+ self.assertEqual(out["by_stage"]["stranger"], 2)
103
+
104
+ if __name__ == "__main__":
105
+ unittest.main()
@@ -0,0 +1,216 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-180 等待感单测:对数饱和、夜间放大、亲近放大、四种感受、关闭。
3
+
4
+ 只依赖 biocore_model.waiting_sense 的纯函数;不导入 main.py。
5
+ """
6
+ import unittest
7
+
8
+ from biocore_model import waiting_sense as ws
9
+
10
+ CALM = {"gap_seconds": 300, "hour": 14}
11
+ FOND = {"gap_seconds": 4 * 3600, "relationship": "friend", "hour": 14}
12
+ ANXIOUS = {"gap_seconds": 8 * 3600,
13
+ "last_emotion": {"label": "sad", "valence": -0.6, "arousal": 0.3},
14
+ "relationship": "stranger", "hour": 23}
15
+ SULKY = {"gap_seconds": 8 * 3600,
16
+ "last_emotion": {"label": "anger", "valence": -0.7,
17
+ "arousal": 0.85},
18
+ "relationship": {"stage": "close", "closeness": 0.9}, "hour": 14}
19
+
20
+
21
+ class ContractTest(unittest.TestCase):
22
+ def test_result_keys(self):
23
+ out = ws.waiting_sense(**FOND)
24
+ for key in ("enabled", "waiting", "feeling", "thought", "threshold_hit",
25
+ "gap_seconds", "gap_hours", "saturation", "closeness",
26
+ "night", "factors", "reason"):
27
+ self.assertIn(key, out)
28
+
29
+ def test_feelings_and_thoughts(self):
30
+ self.assertEqual(set(ws.FEELINGS), {"平静", "惦记", "不安", "赌气"})
31
+ self.assertEqual(set(ws.THOUGHTS), set(ws.FEELINGS))
32
+
33
+ def test_values_bounded(self):
34
+ for args in (CALM, FOND, ANXIOUS, SULKY):
35
+ out = ws.waiting_sense(**args)
36
+ self.assertGreaterEqual(out["waiting"], 0.0)
37
+ self.assertLessEqual(out["waiting"], 1.0)
38
+ self.assertGreaterEqual(out["saturation"], 0.0)
39
+ self.assertLessEqual(out["saturation"], 1.0)
40
+ self.assertGreaterEqual(out["closeness"], 0.0)
41
+ self.assertLessEqual(out["closeness"], 1.0)
42
+
43
+ def test_flags_and_reason_present(self):
44
+ for args in (CALM, FOND, ANXIOUS, SULKY):
45
+ out = ws.waiting_sense(**args)
46
+ self.assertTrue(out["reason"])
47
+ self.assertTrue(out["thought"])
48
+
49
+
50
+ class FeelingTest(unittest.TestCase):
51
+ def test_short_gap_is_calm(self):
52
+ out = ws.waiting_sense(**CALM)
53
+ self.assertEqual(out["feeling"], "平静")
54
+ self.assertLess(out["waiting"], 0.22)
55
+ self.assertFalse(out["threshold_hit"])
56
+
57
+ def test_moderate_gap_is_fond(self):
58
+ self.assertEqual(ws.waiting_sense(**FOND)["feeling"], "惦记")
59
+
60
+ def test_long_gap_with_sad_mood_is_anxious(self):
61
+ out = ws.waiting_sense(**ANXIOUS)
62
+ self.assertEqual(out["feeling"], "不安")
63
+ self.assertGreaterEqual(out["waiting"], 0.55)
64
+ self.assertTrue(out["threshold_hit"])
65
+
66
+ def test_long_gap_with_anger_and_closeness_is_sulky(self):
67
+ out = ws.waiting_sense(**SULKY)
68
+ self.assertEqual(out["feeling"], "赌气")
69
+
70
+ def test_distant_negative_mood_is_not_sulky(self):
71
+ out = ws.waiting_sense(gap_seconds=8 * 3600, relationship="stranger",
72
+ last_emotion={"label": "anger",
73
+ "valence": -0.7,
74
+ "arousal": 0.85})
75
+ self.assertEqual(out["feeling"], "不安")
76
+
77
+ def test_not_waiting_stays_calm(self):
78
+ out = ws.waiting_sense(gap_seconds=8 * 3600, awaiting_reply=False,
79
+ last_emotion={"label": "anger"}, hour=23)
80
+ self.assertEqual(out["feeling"], "平静")
81
+ self.assertLess(out["waiting"], 0.22)
82
+ self.assertFalse(out["threshold_hit"])
83
+
84
+ def test_thoughts_are_short_and_distinct(self):
85
+ scenarios = (CALM, FOND, ANXIOUS, SULKY)
86
+ thoughts = set()
87
+ for args in scenarios:
88
+ out = ws.waiting_sense(**args)
89
+ self.assertTrue(out["thought"])
90
+ self.assertLessEqual(len(out["thought"]), 24)
91
+ thoughts.add(out["thought"])
92
+ self.assertEqual(len(thoughts), len(scenarios))
93
+
94
+
95
+ class SaturationTest(unittest.TestCase):
96
+ def test_log_saturation_monotone(self):
97
+ values = [ws.saturation(hours * 3600)
98
+ for hours in (1, 2, 3, 4, 8, 24, 72)]
99
+ self.assertEqual(values, sorted(values))
100
+ self.assertEqual(ws.saturation(0), 0.0)
101
+
102
+ def test_equal_windows_shrink(self):
103
+ first = ws.saturation(2 * 3600) - ws.saturation(1 * 3600)
104
+ second = ws.saturation(3 * 3600) - ws.saturation(2 * 3600)
105
+ self.assertGreater(first, second)
106
+ self.assertGreater(first, 0.0)
107
+
108
+ def test_seven_days_saturates(self):
109
+ self.assertAlmostEqual(ws.saturation(7 * 24 * 3600), 1.0, places=3)
110
+ self.assertLessEqual(ws.saturation(30 * 24 * 3600), 1.0)
111
+
112
+ def test_waiting_rises_with_gap(self):
113
+ values = [ws.waiting_sense(gap_seconds=h * 3600)["waiting"]
114
+ for h in (1, 2, 4, 8, 24)]
115
+ self.assertEqual(values, sorted(values))
116
+
117
+ def test_waiting_factor_product(self):
118
+ out = ws.waiting_sense(**FOND)
119
+ factors = out["factors"]
120
+ expected = (factors["saturation"] * factors["closeness"]
121
+ * factors["night"] * factors["mood"]
122
+ * factors["expectation"])
123
+ self.assertAlmostEqual(out["waiting"], min(1.0, expected), places=2)
124
+
125
+
126
+ class AmplificationTest(unittest.TestCase):
127
+ def test_night_amplifies(self):
128
+ day = ws.waiting_sense(gap_seconds=4 * 3600, relationship="friend",
129
+ hour=14)
130
+ night = ws.waiting_sense(gap_seconds=4 * 3600, relationship="friend",
131
+ hour=23)
132
+ self.assertFalse(day["night"])
133
+ self.assertTrue(night["night"])
134
+ self.assertGreater(night["waiting"], day["waiting"])
135
+
136
+ def test_closeness_amplifies(self):
137
+ far = ws.waiting_sense(gap_seconds=4 * 3600, relationship="stranger",
138
+ hour=14)
139
+ near = ws.waiting_sense(gap_seconds=4 * 3600,
140
+ relationship={"closeness": 0.95}, hour=14)
141
+ self.assertGreater(near["waiting"], far["waiting"])
142
+
143
+ def test_stage_strings_map_to_closeness(self):
144
+ friend = ws.waiting_sense(gap_seconds=4 * 3600, relationship="friend",
145
+ hour=14)
146
+ close = ws.waiting_sense(gap_seconds=4 * 3600, relationship="close",
147
+ hour=14)
148
+ self.assertGreater(close["closeness"], friend["closeness"])
149
+ self.assertGreater(close["waiting"], friend["waiting"])
150
+
151
+ def test_negative_last_emotion_amplifies(self):
152
+ neutral = ws.waiting_sense(gap_seconds=4 * 3600,
153
+ relationship="friend", hour=14)
154
+ negative = ws.waiting_sense(
155
+ gap_seconds=4 * 3600, relationship="friend", hour=14,
156
+ last_emotion={"valence": -0.8, "arousal": 0.8})
157
+ self.assertGreater(negative["waiting"], neutral["waiting"])
158
+
159
+
160
+ class ThresholdTest(unittest.TestCase):
161
+ def test_threshold_hit_on_long_gap(self):
162
+ self.assertTrue(ws.waiting_sense(gap_seconds=8 * 3600)["threshold_hit"])
163
+
164
+ def test_custom_threshold_can_suppress(self):
165
+ out = ws.waiting_sense(**FOND, flags={"threshold": 0.99})
166
+ self.assertFalse(out["threshold_hit"])
167
+ self.assertGreater(out["waiting"], 0.5)
168
+
169
+ def test_threshold_seconds_rule(self):
170
+ out = ws.waiting_sense(gap_seconds=6 * 3600 + 1,
171
+ flags={"threshold": 0.99})
172
+ self.assertTrue(out["threshold_hit"])
173
+
174
+ def test_not_waiting_has_no_gap_alert(self):
175
+ out = ws.waiting_sense(gap_seconds=8 * 3600, awaiting_reply=False)
176
+ self.assertFalse(out["threshold_hit"])
177
+
178
+
179
+ class SwitchAndRobustnessTest(unittest.TestCase):
180
+ def test_disabled_is_neutral(self):
181
+ out = ws.waiting_sense(gap_seconds=8 * 3600,
182
+ flags={"enabled": False})
183
+ self.assertFalse(out["enabled"])
184
+ self.assertEqual(out["waiting"], 0.0)
185
+ self.assertEqual(out["feeling"], "平静")
186
+ self.assertFalse(out["threshold_hit"])
187
+ self.assertIn("关闭", out["reason"])
188
+
189
+ def test_odd_gaps(self):
190
+ for gap in (None, "x", [], 3, float("nan"), -100):
191
+ out = ws.waiting_sense(gap_seconds=gap)
192
+ self.assertEqual(out["enabled"], True)
193
+ self.assertGreaterEqual(out["waiting"], 0.0)
194
+ self.assertLessEqual(out["waiting"], 1.0)
195
+
196
+ def test_odd_emotions_and_relationships(self):
197
+ for emotion in (None, "anger", "sad", "happy", "weird", 3, []):
198
+ out = ws.waiting_sense(gap_seconds=4 * 3600, last_emotion=emotion)
199
+ self.assertIn(out["feeling"], ws.FEELINGS)
200
+ for rel in (None, "x", 3, [], {"stage": "friend"},
201
+ {"closeness": -5}):
202
+ out = ws.waiting_sense(gap_seconds=4 * 3600, relationship=rel)
203
+ self.assertGreaterEqual(out["closeness"], 0.0)
204
+ self.assertLessEqual(out["closeness"], 1.0)
205
+
206
+ def test_hour_and_night(self):
207
+ self.assertFalse(ws.waiting_sense(gap_seconds=3600, hour=None)["night"])
208
+ self.assertTrue(ws.waiting_sense(gap_seconds=3600, hour=2)["night"])
209
+ self.assertTrue(ws.waiting_sense(gap_seconds=3600, hour=23)["night"])
210
+ self.assertFalse(ws.waiting_sense(gap_seconds=3600, hour=12)["night"])
211
+ self.assertEqual(ws.night_factor(None), 1.0)
212
+ self.assertGreater(ws.night_factor(23), 1.0)
213
+
214
+
215
+ if __name__ == "__main__":
216
+ unittest.main()
@@ -0,0 +1,101 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Cross-layer directives: normalize, merge, conservative precedence."""
3
+ import unittest
4
+
5
+ from biocore_model import x_directives as xd
6
+
7
+
8
+ class NormalizeTest(unittest.TestCase):
9
+ def test_defaults(self):
10
+ out = xd.normalize({})
11
+ self.assertEqual(out["length_scale"], 1.0)
12
+ self.assertEqual(out["initiative"], 0.5)
13
+ self.assertEqual(out["tone"], "")
14
+ self.assertFalse(out["allow_correction"])
15
+
16
+ def test_clamps_length_and_initiative(self):
17
+ out = xd.normalize({"length_scale": 9, "initiative": -3})
18
+ self.assertEqual(out["length_scale"], xd.LENGTH_MAX)
19
+ self.assertEqual(out["initiative"], 0.0)
20
+
21
+ def test_legacy_proactive_maps_to_initiative(self):
22
+ out = xd.normalize({"proactive": 0.8})
23
+ self.assertEqual(out["initiative"], 0.8)
24
+
25
+ def test_legacy_allow_fields(self):
26
+ self.assertTrue(xd.normalize({"allow_self_correction": True})["allow_correction"])
27
+ self.assertTrue(xd.normalize({"allow_clarify": True})["allow_correction"])
28
+
29
+ def test_note_from_reason(self):
30
+ self.assertEqual(xd.normalize({"reason": "因为累"}).get("note"), "因为累")
31
+
32
+ def test_source_recorded(self):
33
+ self.assertEqual(xd.normalize({}, "metacog")["sources"], ["metacog"])
34
+
35
+ def test_malformed(self):
36
+ out = xd.normalize("nope")
37
+ self.assertEqual(out["length_scale"], 1.0)
38
+
39
+
40
+ class MergeTest(unittest.TestCase):
41
+ def test_empty(self):
42
+ out = xd.merge([])
43
+ self.assertEqual(out["length_scale"], 1.0)
44
+ self.assertEqual(out["sources"], [])
45
+
46
+ def test_takes_min_length(self):
47
+ out = xd.merge([{"length_scale": 1.0}, {"length_scale": 0.8},
48
+ {"length_scale": 0.9}])
49
+ self.assertEqual(out["length_scale"], 0.8)
50
+
51
+ def test_takes_min_initiative(self):
52
+ out = xd.merge([{"proactive": 0.8}, {"initiative": 0.3}])
53
+ self.assertEqual(out["initiative"], 0.3)
54
+
55
+ def test_tone_priority(self):
56
+ out = xd.merge_named([("metacog", {"tone": "低置信"}),
57
+ ("dissonance", {"tone": "认一句"}),
58
+ ("value_order", {"tone": "按选定的来"})])
59
+ self.assertEqual(out["tone"], "认一句")
60
+
61
+ def test_tone_ignores_empty(self):
62
+ out = xd.merge_named([("value_order", {"tone": ""}),
63
+ ("metacog", {"tone": "少说"})])
64
+ self.assertEqual(out["tone"], "少说")
65
+
66
+ def test_correction_is_or(self):
67
+ out = xd.merge([{"allow_self_correction": False},
68
+ {"allow_correction": True}])
69
+ self.assertTrue(out["allow_correction"])
70
+
71
+ def test_notes_joined_and_deduped(self):
72
+ out = xd.merge_named([("metacog", {"reason": "不确定"}),
73
+ ("attributor", {"reason": "不确定"}),
74
+ ("narrative", {"note": "故事有点矛盾"})])
75
+ self.assertEqual(out["note"].count("不确定"), 1)
76
+ self.assertIn("故事有点矛盾", out["note"])
77
+
78
+ def test_sources_listed(self):
79
+ out = xd.merge_named([("metacog", {}), ("metacog", {}),
80
+ ("dissonance", {})])
81
+ self.assertEqual(out["sources"], ["metacog", "dissonance"])
82
+
83
+ def test_idempotent(self):
84
+ first = xd.merge_named([("metacog", {"tone": "少说"})])
85
+ first["_x_normalized"] = True
86
+ second = xd.merge([first])
87
+ self.assertEqual(second["length_scale"], first["length_scale"])
88
+ self.assertEqual(second["tone"], first["tone"])
89
+
90
+ def test_malformed_entries_skipped(self):
91
+ out = xd.merge([None, "x", {"length_scale": 0.7}])
92
+ self.assertEqual(out["length_scale"], 0.7)
93
+ self.assertEqual(len(out["sources"]), 1)
94
+
95
+ def test_note_capped(self):
96
+ out = xd.merge([{"note": "字" * 500}])
97
+ self.assertLessEqual(len(out["note"]), xd.NOTE_LIMIT)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ unittest.main()