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,88 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Possible selves: gaps, weights, one-line sentence."""
3
+ import unittest
4
+
5
+ from biocore_model import possible_selves as ps
6
+
7
+ SELVES = [
8
+ {"name": "更会照顾人", "target": {"benevolence": 0.95}, "importance": 0.9},
9
+ {"name": "更安定", "target": {"security": 0.95}, "importance": 0.5},
10
+ {"name": "更会说", "target": {"power": 0.9}, "importance": 0.3},
11
+ ]
12
+ CURRENT = {"benevolence": 0.5, "security": 0.9, "power": 0.85}
13
+
14
+
15
+ class GapTest(unittest.TestCase):
16
+ def test_gap_positive(self):
17
+ value = ps.gap({"benevolence": 0.9}, {"benevolence": 0.4})
18
+ self.assertAlmostEqual(value, 0.5)
19
+
20
+ def test_gap_zero_when_matching(self):
21
+ self.assertEqual(ps.gap({"security": 0.5}, {"security": 0.5}), 0.0)
22
+
23
+ def test_gap_bounded(self):
24
+ self.assertLessEqual(ps.gap({"power": 1.0}, {"power": 0.0}), 1.0)
25
+
26
+ def test_empty_target(self):
27
+ self.assertEqual(ps.gap({}, {"power": 1.0}), 0.0)
28
+
29
+
30
+ class RankTest(unittest.TestCase):
31
+ def test_sorted_by_weight(self):
32
+ out = ps.rank(SELVES, CURRENT)
33
+ weights = [item["weight"] for item in out["selves"]]
34
+ self.assertEqual(weights, sorted(weights, reverse=True))
35
+
36
+ def test_top_is_biggest_gap(self):
37
+ out = ps.rank(SELVES, CURRENT)
38
+ self.assertEqual(out["top"], "更会照顾人")
39
+
40
+ def test_shares_sum_to_one(self):
41
+ out = ps.rank(SELVES, CURRENT)
42
+ self.assertAlmostEqual(sum(item["share"] for item in out["selves"]), 1.0, places=2)
43
+
44
+ def test_importance_scales_weight(self):
45
+ low = {"name": "a", "target": {"benevolence": 0.9}, "importance": 0.1}
46
+ high = {"name": "b", "target": {"benevolence": 0.9}, "importance": 0.9}
47
+ out = ps.rank([low, high], {"benevolence": 0.1})
48
+ self.assertEqual(out["top"], "b")
49
+
50
+ def test_sentence(self):
51
+ out = ps.rank(SELVES, CURRENT)
52
+ self.assertIn("她还想成为", out["sentence"])
53
+ self.assertLessEqual(len(out["sentence"]), ps.SENTENCE_LIMIT)
54
+
55
+ def test_caps_self_count(self):
56
+ many = [{"name": "s%d" % i, "target": {"benevolence": 0.9}} for i in range(20)]
57
+ self.assertLessEqual(len(ps.rank(many, CURRENT)["selves"]), ps.MAX_SELVES)
58
+
59
+
60
+ class FallbackTest(unittest.TestCase):
61
+ def test_disabled(self):
62
+ out = ps.rank(SELVES, CURRENT, flags={"enabled": False})
63
+ self.assertFalse(out["enabled"])
64
+ self.assertEqual(out["selves"], [])
65
+ self.assertEqual(out["sentence"], "")
66
+
67
+ def test_malformed(self):
68
+ out = ps.rank([None, "x", {"name": ""}, {"name": "ok", "target": {}}], "bad")
69
+ self.assertEqual(out["selves"], [])
70
+
71
+ def test_no_current_uses_default(self):
72
+ out = ps.rank([{"name": "a", "target": {"security": 1.0}}], None)
73
+ self.assertGreater(out["selves"][0]["gap"], 0.0)
74
+
75
+
76
+ class DirectiveTest(unittest.TestCase):
77
+ def test_pull_matches_top(self):
78
+ result = ps.rank(SELVES, CURRENT)
79
+ out = ps.directives(result)
80
+ self.assertIn("更会照顾人", out["tone"])
81
+ self.assertGreater(out["pull"], 0.0)
82
+
83
+ def test_disabled(self):
84
+ self.assertEqual(ps.directives({"enabled": False})["tone"], "")
85
+
86
+
87
+ if __name__ == "__main__":
88
+ unittest.main()
@@ -0,0 +1,227 @@
1
+ # -*- coding: utf-8 -*-
2
+ """P1-1 predictive coding tests (predictor.py + main wiring)."""
3
+ import ast
4
+ import json
5
+ import types
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from biocore_model import predictor as pr
10
+ from biocore_model import policy_learning as pl
11
+
12
+ ROOT = Path(__file__).resolve().parents[2]
13
+ MAIN = ROOT / "main.py"
14
+ SOURCE = MAIN.read_text(encoding="utf-8")
15
+ SCHEMA = json.loads((ROOT / "_conf_schema.json").read_text(encoding="utf-8"))
16
+
17
+ STATE = {"v": 0.2, "a": 0.5, "d": 0.5, "social": 0.6, "stress": 0.3,
18
+ "energy": 0.6, "trust": 0.6, "emotion": "joy",
19
+ "needs": {"belonging": 0.5, "self_esteem": 0.6, "control": 0.4,
20
+ "meaningful_existence": 0.7}}
21
+
22
+
23
+ def _load_cycle():
24
+ tree = ast.parse(SOURCE)
25
+ node = next(item for item in tree.body
26
+ if isinstance(item, ast.FunctionDef)
27
+ and item.name == "_predictive_cycle")
28
+ clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
29
+ decorator_list=[], returns=node.returns,
30
+ type_comment=None, lineno=node.lineno,
31
+ col_offset=node.col_offset,
32
+ end_lineno=node.end_lineno,
33
+ end_col_offset=node.end_col_offset)
34
+ state_node = next(item for item in tree.body
35
+ if isinstance(item, ast.FunctionDef)
36
+ and item.name == "_prediction_state")
37
+ state_clone = ast.FunctionDef(name=state_node.name, args=state_node.args,
38
+ body=state_node.body, decorator_list=[],
39
+ returns=state_node.returns, type_comment=None,
40
+ lineno=state_node.lineno,
41
+ col_offset=state_node.col_offset,
42
+ end_lineno=state_node.end_lineno,
43
+ end_col_offset=state_node.end_col_offset)
44
+ log = []
45
+ ns = {"logger": types.SimpleNamespace(
46
+ info=lambda *a, **k: log.append(a),
47
+ warning=lambda *a, **k: None,
48
+ debug=lambda *a, **k: None)}
49
+ exec(compile(ast.Module(body=[state_clone, clone], type_ignores=[]),
50
+ "<_predictive_cycle>", "exec"), ns)
51
+ return ns[node.name], log
52
+
53
+
54
+ CYCLE, LOG = _load_cycle()
55
+
56
+
57
+ class FakeCore:
58
+ def __init__(self, state=None):
59
+ for key, value in (state or STATE).items():
60
+ setattr(self, key, value)
61
+ self.labels = {"emotion": (state or STATE).get("emotion", "joy")}
62
+
63
+
64
+ class FakePlugin:
65
+ def __init__(self, enabled=True):
66
+ self.predictive_enabled = enabled
67
+ self._next_prediction = None
68
+ self._predict_history = []
69
+
70
+
71
+ class VectorTest(unittest.TestCase):
72
+ def test_vector_clamps_and_orders(self):
73
+ vec = pr.vector({"v": 9, "a": -3, "d": "x", "social": 0.4})
74
+ self.assertEqual(list(vec), list(pr.CHANNELS))
75
+ self.assertEqual(vec["v"], 1.0)
76
+ self.assertEqual(vec["a"], 0.0)
77
+ self.assertEqual(vec["d"], 0.0)
78
+ self.assertEqual(vec["social"], 0.4)
79
+
80
+ def test_predict_flat_state_is_current(self):
81
+ pred = pr.predict(STATE)
82
+ self.assertEqual(pred["channels"], pr.vector(STATE))
83
+ self.assertEqual(pred["emotion"], "joy")
84
+
85
+ def test_predict_follows_damped_trend(self):
86
+ history = [dict(pr.vector(STATE), v=0.0), pr.vector(STATE)]
87
+ pred = pr.predict(STATE, history=history)
88
+ self.assertEqual(pred["channels"]["v"], 0.3)
89
+ rising = [pr.vector({"v": 0.0}), pr.vector({"v": 0.1})]
90
+ pred2 = pr.predict({"v": 0.1}, history=rising)
91
+ self.assertAlmostEqual(pred2["channels"]["v"], 0.15)
92
+ self.assertGreater(pred2["confidence"], pr.predict(STATE)["confidence"])
93
+
94
+ def test_slope_is_clamped(self):
95
+ history = [pr.vector({"v": -1.0}), pr.vector({"v": 1.0})]
96
+ pred = pr.predict({"v": 1.0}, history=history)
97
+ self.assertLessEqual(pred["channels"]["v"], 1.0)
98
+ self.assertAlmostEqual(pred["channels"]["v"], 1.0)
99
+
100
+ def test_needs_drift_toward_baseline(self):
101
+ pred = pr.predict(STATE)
102
+ self.assertGreater(pred["needs"]["belonging"], 0.5)
103
+ self.assertLess(pred["needs"]["meaningful_existence"], 0.7)
104
+
105
+
106
+ class CompareTest(unittest.TestCase):
107
+ def test_identical_state_has_no_surprise(self):
108
+ pred = pr.predict(STATE)
109
+ report = pr.compare(pred, STATE)
110
+ self.assertEqual(report["error"], 0.0)
111
+ self.assertEqual(report["surprise"], 0.0)
112
+ self.assertTrue(report["emotion_hit"])
113
+
114
+ def test_large_change_raises_surprise(self):
115
+ pred = pr.predict(STATE)
116
+ changed = dict(STATE, v=-0.6, a=0.95, stress=0.9, emotion="anger")
117
+ report = pr.compare(pred, changed)
118
+ self.assertGreater(report["error"], 0.1)
119
+ self.assertGreaterEqual(report["surprise"], 0.5)
120
+ self.assertFalse(report["emotion_hit"])
121
+ self.assertEqual(report["actual_emotion"], "anger")
122
+ self.assertEqual(report["channels"]["v"]["error"], 0.8)
123
+
124
+ def test_needs_error_reported(self):
125
+ pred = pr.predict(STATE)
126
+ changed = dict(STATE, needs={"belonging": 0.1, "self_esteem": 0.6,
127
+ "control": 0.4, "meaningful_existence": 0.7})
128
+ report = pr.compare(pred, changed)
129
+ self.assertGreater(report["needs_error"], 0.0)
130
+
131
+ def test_surprise_bounds_and_monotonicity(self):
132
+ values = [pr.surprise(x) for x in (0.0, 0.05, 0.1, 0.25, 1.0)]
133
+ self.assertEqual(values, sorted(values))
134
+ self.assertEqual(values[0], 0.0)
135
+ self.assertEqual(values[-1], 1.0)
136
+
137
+
138
+ class EffectScaleTest(unittest.TestCase):
139
+ def test_attention_boost_monotone_bounded(self):
140
+ values = [pr.attention_boost(x) for x in (0.0, 0.2, 0.5, 1.0)]
141
+ self.assertEqual(values, sorted(values))
142
+ self.assertEqual(values[0], 0.0)
143
+ self.assertEqual(values[-1], 1.0)
144
+
145
+ def test_learning_rate_scale_monotone_bounded(self):
146
+ low = pr.learning_rate_scale(0.0)
147
+ high = pr.learning_rate_scale(1.0)
148
+ self.assertEqual(low, 0.6)
149
+ self.assertEqual(high, 1.8)
150
+ self.assertLess(low, high)
151
+ custom = pr.learning_rate_scale(0.5, low=0.5, high=2.0)
152
+ self.assertAlmostEqual(custom, 1.25)
153
+
154
+ def test_scale_feeds_policy_alpha(self):
155
+ # The main wiring multiplies the policy-learning alpha by the scale.
156
+ self.assertIn("_prediction_lr_scale", SOURCE)
157
+ self.assertIn("alpha=_lr", SOURCE)
158
+ slow_table = pl.update({}, "ctx", "approach", 1.0, alpha=0.1)
159
+ fast_table = pl.update({}, "ctx", "approach", 1.0, alpha=0.4)
160
+ slow_q = float(slow_table["ctx"]["approach"]["q"])
161
+ fast_q = float(fast_table["ctx"]["approach"]["q"])
162
+ self.assertLess(slow_q, fast_q)
163
+
164
+
165
+ class HistoryTest(unittest.TestCase):
166
+ def test_append_history_bounded_and_non_mutating(self):
167
+ history = [pr.vector({"v": i / 20.0}) for i in range(6)]
168
+ snapshot = list(history)
169
+ grown = pr.append_history(history, pr.vector({"v": 0.9}))
170
+ self.assertEqual(history, snapshot)
171
+ self.assertEqual(len(grown), pr.MAX_HISTORY)
172
+ self.assertEqual(grown[-1]["v"], 0.9)
173
+
174
+ def test_invalid_inputs_are_safe(self):
175
+ self.assertTrue(all(v == 0.0 for v in pr.vector(None).values()))
176
+ self.assertEqual(pr.predict(None)["emotion"], "neutral")
177
+ self.assertEqual(pr.compare(None, None)["surprise"], 0.0)
178
+ self.assertEqual(pr.append_history("x", "y"), [])
179
+ self.assertEqual(pr.summary(None)["surprise"], 0.0)
180
+
181
+
182
+ class WiringTest(unittest.TestCase):
183
+ def test_main_wiring_markers(self):
184
+ self.assertIn("def _predictive_cycle(plugin, core)", SOURCE)
185
+ self.assertIn("_predictive_cycle(self, core)", SOURCE)
186
+ self.assertIn("attention_boost", SOURCE)
187
+ self.assertIn("_retrieval_limit + 1", SOURCE)
188
+ self.assertIn("predictor as _predictor", SOURCE)
189
+ self.assertIn("[biocore] predict surprise=", SOURCE)
190
+
191
+ def test_schema_and_config_default(self):
192
+ self.assertIn("predictive_enabled", SCHEMA)
193
+ self.assertTrue(SCHEMA["predictive_enabled"]["default"])
194
+ self.assertIn('self.config.get("predictive_enabled", True)', SOURCE)
195
+
196
+ def _run(self, plugin, core):
197
+ return CYCLE(plugin, core)
198
+
199
+ def test_cycle_records_error_and_predicts_next(self):
200
+ plugin = FakePlugin()
201
+ core = FakeCore()
202
+ LOG.clear()
203
+ self.assertEqual(self._run(plugin, core), {})
204
+ self.assertIsNotNone(plugin._next_prediction)
205
+ changed = dict(STATE, v=-0.7, stress=0.9, emotion="anger")
206
+ core2 = FakeCore(changed)
207
+ report = self._run(plugin, core2)
208
+ row = pr.summary(report)
209
+ self.assertGreater(report["surprise"], 0.0)
210
+ self.assertEqual(core2.prediction_surprise, row["surprise"])
211
+ self.assertAlmostEqual(core2.learning_rate_scale,
212
+ row["learning_rate_scale"], places=3)
213
+ self.assertGreaterEqual(core2.attention_boost, 0.0)
214
+ self.assertTrue(any("predict" in str(row) for row in LOG))
215
+ self.assertEqual(plugin._prediction_lr_scale,
216
+ core2.learning_rate_scale)
217
+
218
+ def test_cycle_disabled_is_noop(self):
219
+ plugin = FakePlugin(enabled=False)
220
+ core = FakeCore()
221
+ self.assertEqual(self._run(plugin, core), {})
222
+ self.assertIsNone(plugin._next_prediction)
223
+ self.assertFalse(hasattr(core, "prediction_surprise"))
224
+
225
+
226
+ if __name__ == "__main__":
227
+ unittest.main()
@@ -0,0 +1,142 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-219: repeated topics slowly drift value_order (preference drift)."""
3
+ import unittest
4
+
5
+ from biocore_memory.core import config as engine_config
6
+ from biocore_model import preference_drift as pd
7
+ from biocore_model import value_order as vo
8
+
9
+
10
+ def memory(text):
11
+ return {"id": "m", "text": text, "role": "user", "ts": 1700000000.0}
12
+
13
+
14
+ class SwitchTest(unittest.TestCase):
15
+ def test_flag_defaults_off(self):
16
+ self.assertIs(engine_config.FLAGS.get("preference_drift"), False)
17
+
18
+ def test_enabled_reads_flag_and_env(self):
19
+ self.assertFalse(pd.enabled({}))
20
+ self.assertTrue(pd.enabled({"preference_drift": True}))
21
+ self.assertTrue(pd.enabled({}, {"BIOCORE_PREFERENCE_DRIFT": "1"}))
22
+ self.assertFalse(pd.enabled({}, {"BIOCORE_PREFERENCE_DRIFT": "0"}))
23
+
24
+ def test_disabled_leaves_weights_alone(self):
25
+ before = vo.defaults()
26
+ out = pd.step(before, {"安全": 50}, {}, enabled=False)
27
+ self.assertEqual(out["reason"], "disabled")
28
+ self.assertEqual(out["drifted"], {})
29
+ self.assertEqual(out["weights"], vo.normalize(before))
30
+
31
+
32
+ class TopicTest(unittest.TestCase):
33
+ def test_topics_extract_cjk_and_latin(self):
34
+ found = pd.topics("今天陪我聊聊天 happy")
35
+ self.assertIn("陪我", found)
36
+ self.assertIn("happy", found)
37
+ self.assertNotIn("今天", found) # stopword
38
+
39
+ def test_dimension_mapping(self):
40
+ self.assertEqual(pd.dimension_of("安全"), "security")
41
+ self.assertEqual(pd.dimension_of("陪我"), "benevolence")
42
+ self.assertEqual(pd.dimension_of("开心"), "hedonism")
43
+ self.assertEqual(pd.dimension_of("诚实"), "universalism")
44
+ self.assertIsNone(pd.dimension_of("zzz"))
45
+
46
+ def test_accumulate_counts_once_per_entry(self):
47
+ counts = pd.accumulate({}, [memory("安全安全"), memory("注意安全"),
48
+ memory("安全第一")])
49
+ self.assertEqual(counts["安全"], 3)
50
+ self.assertIn("陪我", pd.count_topics([memory("陪我聊天")]))
51
+
52
+ def test_entries_are_not_mutated(self):
53
+ entry = memory("陪我聊天")
54
+ snapshot = dict(entry)
55
+ pd.accumulate({}, [entry])
56
+ self.assertEqual(entry, snapshot)
57
+
58
+
59
+ class DriftTest(unittest.TestCase):
60
+ def test_below_threshold_does_nothing(self):
61
+ out = pd.step(vo.defaults(), {"安全": 5}, {}, days=1)
62
+ self.assertEqual(out["drifted"], {})
63
+ self.assertEqual(out["weights"], vo.defaults())
64
+
65
+ def test_repeated_topic_raises_dimension(self):
66
+ counts = pd.count_topics([memory("注意安全") for _ in range(6)])
67
+ self.assertEqual(counts["安全"], 6)
68
+ out = pd.step(vo.defaults(), counts, {}, days=1)
69
+ self.assertEqual(out["drifted"].get("security"), 0.01)
70
+ self.assertAlmostEqual(out["weights"]["security"], 0.71, places=4)
71
+
72
+ def test_daily_cap_is_respected(self):
73
+ out = pd.step(vo.defaults(), {"安全": 50}, {}, days=1)
74
+ self.assertLessEqual(out["drifted"]["security"], pd.DAILY_CAP)
75
+ self.assertAlmostEqual(out["drifted"]["security"], 0.01, places=4)
76
+
77
+ def test_replay_is_idempotent(self):
78
+ counts = {"安全": 6}
79
+ first = pd.step(vo.defaults(), counts, {}, days=1)
80
+ second = pd.step(first["weights"], counts, first["counts"], days=1)
81
+ self.assertEqual(second["drifted"], {})
82
+ self.assertEqual(second["weights"], first["weights"])
83
+ self.assertEqual(second["weights"]["security"], first["weights"]["security"])
84
+
85
+ def test_multi_month_accumulation(self):
86
+ weights = vo.defaults()
87
+ counts = {}
88
+ last = {}
89
+ for day in range(1, 91):
90
+ if day % 3 == 0:
91
+ counts = pd.accumulate(counts, [memory("还是要注意安全")])
92
+ out = pd.step(weights, counts, last, days=3)
93
+ weights, last = out["weights"], out["counts"]
94
+ # 30 mentions, first 5 under threshold, 25 steps x 0.002
95
+ self.assertAlmostEqual(weights["security"], 0.75, places=3)
96
+
97
+
98
+ class BoundsTest(unittest.TestCase):
99
+ def test_ceiling(self):
100
+ weights = vo.defaults()
101
+ counts = {}
102
+ last = {}
103
+ for day in range(1, 301):
104
+ counts = pd.accumulate(counts, [memory("注意安全")])
105
+ out = pd.step(weights, counts, last, days=1)
106
+ weights, last = out["weights"], out["counts"]
107
+ self.assertLessEqual(weights["security"], vo.WEIGHT_CEIL)
108
+ self.assertGreaterEqual(weights["security"], vo.WEIGHT_FLOOR)
109
+
110
+ def test_floor_via_normalize(self):
111
+ out = pd.step({"security": -5.0}, {}, {}, days=1)
112
+ self.assertGreaterEqual(out["weights"]["security"], vo.WEIGHT_FLOOR)
113
+
114
+ def test_decay_returns_toward_default(self):
115
+ table = vo.defaults()
116
+ table["security"] = 0.90
117
+ out = pd.step(table, {}, {}, days=30)
118
+ self.assertIn("security", out["decayed"])
119
+ self.assertAlmostEqual(out["weights"]["security"], 0.885, places=4)
120
+
121
+ def test_decay_settles_at_default(self):
122
+ table = vo.defaults()
123
+ table["security"] = 0.90
124
+ for _ in range(200):
125
+ table = pd.step(table, {}, {}, days=30)["weights"]
126
+ self.assertAlmostEqual(table["security"], vo.DEFAULTS["security"], places=3)
127
+
128
+
129
+ class DegradedTest(unittest.TestCase):
130
+ def test_missing_value_order_skips(self):
131
+ saved = pd._vo
132
+ pd._vo = None
133
+ try:
134
+ out = pd.step({"security": 0.7}, {"安全": 9}, {}, days=1)
135
+ finally:
136
+ pd._vo = saved
137
+ self.assertEqual(out["reason"], "value_order_unavailable")
138
+ self.assertEqual(out["weights"], {"security": 0.7})
139
+
140
+
141
+ if __name__ == "__main__":
142
+ unittest.main()
@@ -0,0 +1,142 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Preset structure tests: positions, order, conditions, depth, empty skip."""
3
+ import sys
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ from biocore_model import preset
8
+
9
+ PKG = Path(__file__).resolve().parents[1]
10
+ IDS = ["situation", "layered_memory", "graph_memory", "state_block",
11
+ "relationship", "gap", "speak_style", "numeric_legend"]
12
+ FULL = {"situation": "对方在质问你。", "layered_memory": "[分层记忆] 常驻事实:他姓王", "graph_memory": "你知道的:\n- 小夏 —害怕→ 打雷",
13
+ "state_block": "她此刻:紧张。", "relationship": "关系:亲近,信任中等。",
14
+ "gap": "已 3 小时没说话。", "speak_style": "一次只说一句,10 字以内。",
15
+ "numeric_legend": "v 正面度 / a 唤醒度"}
16
+
17
+
18
+ def _rec(**extra):
19
+ base = {"id": "x", "name": "X", "role": "system",
20
+ "position": "before_history", "depth": 4, "order": 100,
21
+ "enabled": True, "condition": "", "content": "{{ value }}"}
22
+ base.update(extra)
23
+ return base
24
+
25
+
26
+ class StructureTest(unittest.TestCase):
27
+ def test_default_record_ids(self):
28
+ self.assertEqual([r["id"] for r in preset.PRESET], IDS)
29
+
30
+ def test_default_record_fields(self):
31
+ required = {"id", "name", "role", "position", "depth", "order",
32
+ "enabled", "condition", "content"}
33
+ for record in preset.PRESET:
34
+ self.assertTrue(required.issubset(record.keys()), record["id"])
35
+ self.assertIn(record["position"], preset.POSITIONS)
36
+ self.assertTrue(record["enabled"])
37
+
38
+ def test_default_positions(self):
39
+ by_id = {r["id"]: r for r in preset.PRESET}
40
+ for key in ("situation", "layered_memory", "state_block",
41
+ "relationship", "gap"):
42
+ self.assertEqual(by_id[key]["position"], "before_history")
43
+ self.assertEqual(by_id["speak_style"]["position"], "depth")
44
+ self.assertEqual(by_id["speak_style"]["depth"], 2)
45
+ self.assertEqual(by_id["numeric_legend"]["position"], "after_history")
46
+
47
+ def test_before_history_order(self):
48
+ out = preset.assemble(FULL, history_len=10)
49
+ self.assertEqual([b["id"] for b in out["before_history"]],
50
+ ["situation", "layered_memory", "graph_memory",
51
+ "state_block", "relationship", "gap"])
52
+
53
+ def test_depth_group(self):
54
+ out = preset.assemble(FULL, history_len=10)
55
+ self.assertEqual(len(out["depth"]), 1)
56
+ self.assertEqual(out["depth"][0]["depth"], 2)
57
+ self.assertEqual(out["depth"][0]["blocks"][0]["id"], "speak_style")
58
+
59
+ def test_after_history_group(self):
60
+ out = preset.assemble(FULL, history_len=10)
61
+ self.assertEqual([b["id"] for b in out["after_history"]],
62
+ ["numeric_legend"])
63
+
64
+ def test_empty_action_skips_missing_vars(self):
65
+ out = preset.assemble({}, history_len=3)
66
+ self.assertEqual(out["before_history"], [])
67
+ self.assertEqual(out["depth"], [])
68
+ self.assertEqual(out["after_history"], [])
69
+
70
+ def test_empty_content_is_skipped(self):
71
+ out = preset.assemble({"situation": " ", "gap": "\n"}, 3)
72
+ self.assertEqual(out["before_history"], [])
73
+
74
+ def test_variables_are_substituted(self):
75
+ out = preset.assemble({"situation": "他在夸你。"}, 3)
76
+ self.assertEqual(out["before_history"][0]["content"],
77
+ "[局面摘要]\n他在夸你。")
78
+
79
+ def test_condition_false_hides_block(self):
80
+ out = preset.assemble({"situation": "x", "layered_memory": ""}, 3)
81
+ self.assertEqual([b["id"] for b in out["before_history"]],
82
+ ["situation"])
83
+
84
+ def test_threshold_condition(self):
85
+ record = _rec(condition="trust > 0.5", content="trusted")
86
+ self.assertEqual(len(preset._blocks([record], {"trust": 0.8}, 0)), 1)
87
+ self.assertEqual(len(preset._blocks([record], {"trust": 0.2}, 0)), 0)
88
+
89
+ def test_invalid_condition_fails_closed(self):
90
+ record = _rec(condition="1 +", content="never")
91
+ self.assertEqual(preset._blocks([record], {}, 0), [])
92
+
93
+ def test_order_sorting(self):
94
+ low = _rec(id="b", order=200, content="B")
95
+ high = _rec(id="a", order=100, content="A")
96
+ out = preset._blocks([low, high], {}, 0)
97
+ self.assertEqual([b["id"] for b in out], ["a", "b"])
98
+
99
+ def test_depth_clamped_by_history_len(self):
100
+ out = preset.assemble(FULL, history_len=1)
101
+ self.assertEqual(out["depth"][0]["depth"], 1)
102
+ out2 = preset.assemble(FULL, history_len=0)
103
+ self.assertEqual(out2["depth"][0]["depth"], 0)
104
+
105
+ def test_render_texts_flattens_positions(self):
106
+ rows = preset.render_texts(preset.assemble(FULL, 10))
107
+ positions = [row[0] for row in rows]
108
+ self.assertEqual(positions.count("before_history"), 6)
109
+ self.assertEqual(positions.count("depth"), 1)
110
+ self.assertEqual(positions.count("after_history"), 1)
111
+
112
+ def test_module_is_pure_and_does_not_import_main(self):
113
+ source = (PKG / "preset.py").read_text(encoding="utf-8")
114
+ self.assertNotIn("import main", source)
115
+ self.assertNotIn("main.py", source)
116
+ # Check the module's own namespace instead of the global sys.modules:
117
+ # discovery imports root-level test modules that import main, which made
118
+ # the old process-wide assertion order-dependent.
119
+ module = sys.modules.get("biocore_model.preset")
120
+ self.assertIsNotNone(module)
121
+ self.assertNotIn("main", vars(module))
122
+
123
+ def test_depth_index_after_nth_newest(self):
124
+ self.assertEqual(preset.depth_index(10, 2), 9)
125
+ self.assertEqual(preset.depth_index(3, 5), 0)
126
+ self.assertEqual(preset.depth_index(0, 2), 0)
127
+
128
+ def test_ui_settings_preset_default_is_on(self):
129
+ from biocore_model import ui_settings
130
+ self.assertIs(ui_settings.DEFAULTS["preset"]["enabled"], True)
131
+
132
+ def test_main_has_gated_fallback_path(self):
133
+ from pathlib import Path
134
+ repo = Path(__file__).resolve().parents[2]
135
+ main = (repo / "main.py").read_text(encoding="utf-8")
136
+ self.assertIn("if self._preset_enabled():", main)
137
+ self.assertIn("self._inject_preset(req, event, core, _user_data,", main)
138
+ self.assertIn("self._legacy_context_blocks(req, event, core, _user_data,",
139
+ main)
140
+
141
+ if __name__ == "__main__":
142
+ unittest.main()
@@ -0,0 +1,70 @@
1
+ # -*- coding: utf-8 -*-
2
+ import time, unittest
3
+ from biocore_model import proactive_context as pc
4
+
5
+ NOW = 1_700_000_000
6
+
7
+ class FakeCore:
8
+ labels = {'emotion': 'joy', 'intensity': 0.62}
9
+ spectrum_probs = {'joy': 0.62, 'contentment': 0.23, 'amusement': 0.15}
10
+ v = 0.42; a = 0.66; d = 0.52; social = 0.61; stress = 0.24; energy = 0.81
11
+ homeostasis = {'drives': {'belong': 0.26, 'curiosity': 0.1}}
12
+ interoception = {'label': 'steady'}
13
+
14
+ class ContextTest(unittest.TestCase):
15
+ def test_state_block_fields(self):
16
+ text = pc.render_state_block(FakeCore())
17
+ self.assertIn('主导情绪:joy', text); self.assertIn('情绪谱', text); self.assertIn('维度', text)
18
+ def test_state_block_missing_fields_no_crash(self):
19
+ class Empty: pass
20
+ self.assertIn('主导情绪', pc.render_state_block(Empty()))
21
+ def test_relationship(self):
22
+ text = pc.render_relationship({'state': {'closeness': 0.61, 'trust': 0.57, 'conflict': 0.22}, 'attachment': {'safe': 0.5}})
23
+ self.assertIn('亲近 0.61', text); self.assertIn('依恋安全感 0.50', text)
24
+ def test_gap_bands(self):
25
+ self.assertIn('刚刚', pc.gap_text(0.5))
26
+ self.assertIn('几小时前', pc.gap_text(3))
27
+ self.assertIn('大半天', pc.gap_text(10))
28
+ self.assertIn('两天多', pc.gap_text(38))
29
+ self.assertIn('好几天', pc.gap_text(80))
30
+ def test_gap_style_hint(self):
31
+ self.assertIn('抗议', pc.attachment_gap_hint('anxious'))
32
+ self.assertIn('不要提间隔', pc.attachment_gap_hint('avoidant'))
33
+ self.assertIn('温和问候', pc.attachment_gap_hint('secure'))
34
+ def test_event_next_date(self):
35
+ hints = pc.render_event_hints([{'title': '体检', 'next_date_ts': NOW + 86400, 'importance': 0.8}], NOW)
36
+ self.assertTrue(any('体检' in h and '明天' in h for h in hints))
37
+ def test_event_unresolved(self):
38
+ hints = pc.render_event_hints([{'title': '旧话题', 'unresolved': True, 'ts': NOW - 2 * 86400, 'importance': 0.8}], NOW)
39
+ self.assertTrue(any('没收尾' in h for h in hints))
40
+ def test_event_low_importance_filtered(self):
41
+ self.assertEqual(pc.render_event_hints([{'title': 'x', 'next_date_ts': NOW + 3600, 'importance': 0.1}], NOW), [])
42
+ def test_event_no_title_skipped(self):
43
+ self.assertEqual(pc.render_event_hints([{'next_date_ts': NOW + 3600, 'importance': 0.9}], NOW), [])
44
+ def test_event_max_two(self):
45
+ events = [{'title': 'a', 'next_date_ts': NOW + 3600, 'importance': 0.9}, {'title': 'b', 'unresolved': True, 'ts': NOW - 2 * 86400, 'importance': 0.9}, {'title': 'c', 'unresolved': True, 'ts': NOW - 2 * 86400, 'importance': 0.9}]
46
+ self.assertLessEqual(len(pc.render_event_hints(events, NOW)), 2)
47
+ def test_l1_extra_lines(self):
48
+ lines = pc.l1_extra_lines(38, [{'title': '体检', 'next_date_ts': NOW + 86400, 'importance': 0.9}], NOW)
49
+ self.assertTrue(any(line.startswith('【间隔】') for line in lines))
50
+ self.assertTrue(any(line.startswith('【你记得】') for line in lines))
51
+ def test_prompt_order(self):
52
+ prompt = pc.build_proactive_prompt('人设', '说话方式', '情绪块', '关系块', '间隔块', ['提示'], {'code': 'lonely', 'text': '有点闷', 'weight': 0.3}, 1.34)
53
+ user = prompt['user']
54
+ self.assertLess(user.index('【当前情绪】'), user.index('【关系】'))
55
+ self.assertLess(user.index('【关系】'), user.index('【间隔】'))
56
+ self.assertLess(user.index('【间隔】'), user.index('【动机】'))
57
+ self.assertLess(user.index('【动机】'), user.index('【要求】'))
58
+ self.assertIn('动机', user)
59
+ self.assertNotIn('<bc>', prompt['system'])
60
+ self.assertNotIn('【输出格式', prompt['system'])
61
+ def test_prompt_contains_event_hints(self):
62
+ prompt = pc.build_proactive_prompt("人设", "说话", "x", "y", "z", ["他提过「体检」(明天)"], {"code": "memory_due", "text": "记得体检", "weight": 0.4}, 1.2)
63
+ self.assertIn("你记得", prompt["user"]); self.assertIn("体检", prompt["user"])
64
+
65
+ def test_system_contains_persona_and_speak(self):
66
+ prompt = pc.build_proactive_prompt('人设', '说话方式', 'x', 'y', 'z', [], {})
67
+ self.assertIn('人设', prompt['system']); self.assertIn('说话方式', prompt['system'])
68
+
69
+ if __name__ == '__main__':
70
+ unittest.main()