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,231 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-173 批二接线测试:granularity / affective_labor / self_verification。
3
+
4
+ 同时覆盖 task-173 的开关拆分:boundary_enabled 只表示内容安全闸,
5
+ boundary_lines_enabled 才是「她的底线」。三个模块的 directives 必须走现有
6
+ 唯一 merge_named 入口。
7
+ """
8
+ import ast
9
+ import json
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ from biocore_model import affective_labor as al
14
+ from biocore_model import granularity as gr
15
+ from biocore_model import self_verification as sv
16
+ from biocore_model import x_directives as xd
17
+
18
+ ROOT = Path(__file__).resolve().parents[2]
19
+ MAIN = (ROOT / "main.py").read_text(encoding="utf-8")
20
+ ROUTES = (ROOT / "biocore_model" / "plugin_routes.py").read_text(encoding="utf-8")
21
+ SCHEMA = json.loads((ROOT / "_conf_schema.json").read_text(encoding="utf-8"))
22
+ DASH = ROOT / "pages" / "dashboard"
23
+ SRC_HTML = (DASH / "index.src.html").read_text(encoding="utf-8")
24
+ INDEX_HTML = (DASH / "index.html").read_text(encoding="utf-8")
25
+ APP_JS = (DASH / "app.js").read_text(encoding="utf-8")
26
+
27
+ NOW = 1_700_000_000
28
+
29
+
30
+ class FlagTest(unittest.TestCase):
31
+ def test_schema_defaults(self):
32
+ expected = {"boundary_lines_enabled": False,
33
+ "granularity_enabled": True,
34
+ "affective_labor_enabled": True,
35
+ "self_verification_enabled": True}
36
+ for key, default in expected.items():
37
+ entry = SCHEMA.get(key)
38
+ self.assertIsInstance(entry, dict, key)
39
+ self.assertEqual(entry.get("type"), "bool", key)
40
+ self.assertIs(entry.get("default"), default, key)
41
+
42
+ def test_main_reads_flags(self):
43
+ expected = {"boundary_lines_enabled": "False",
44
+ "granularity_enabled": "True",
45
+ "affective_labor_enabled": "True",
46
+ "self_verification_enabled": "True"}
47
+ for key, default in expected.items():
48
+ self.assertIn('self.config.get("%s", %s)' % (key, default), MAIN)
49
+
50
+
51
+ class BoundarySplitTest(unittest.TestCase):
52
+ def test_hints_are_distinct(self):
53
+ safety = SCHEMA["boundary_enabled"]
54
+ lines = SCHEMA["boundary_lines_enabled"]
55
+ self.assertIn("内容安全", safety.get("description", ""))
56
+ self.assertIn("不诊断", safety.get("hint", ""))
57
+ self.assertNotIn("她的底线——", safety.get("hint", ""))
58
+ self.assertIn("独立", lines.get("hint", ""))
59
+ self.assertIn("boundary_enabled", lines.get("hint", ""))
60
+
61
+ def test_relational_block_uses_lines_switch(self):
62
+ self.assertIn(
63
+ 'if bool(getattr(self, "boundary_lines_enabled", False)):', MAIN)
64
+ block = MAIN[MAIN.index("task-172 batch-1: boundary judgment first"):
65
+ MAIN.index("# A4: capture open loops")]
66
+ self.assertNotIn('getattr(self, "boundary_enabled"', block)
67
+ # content-safety gate module is untouched and still keys off its switch
68
+ gate = (ROOT / "biocore_model" / "boundary_gate.py").read_text(
69
+ encoding="utf-8")
70
+ self.assertIn("boundary_enabled", gate)
71
+
72
+
73
+ class GranularityWiringTest(unittest.TestCase):
74
+ def test_wiring(self):
75
+ self.assertIn("from .biocore_model import granularity as _gr_mod", MAIN)
76
+ self.assertIn("from .biocore_model import emotion_geometry as _eg_mod",
77
+ MAIN)
78
+ self.assertIn("core.emotion_granularity = _gr_view", MAIN)
79
+ self.assertIn("[biocore] emotion granularity primary=%s line=%s", MAIN)
80
+ self.assertIn("if bool(getattr(self, \"granularity_enabled\", True)):", MAIN)
81
+
82
+ def test_read_only_no_directive(self):
83
+ block = MAIN[MAIN.index("task-173 granularity"):
84
+ MAIN.index("# task-173 affective_labor")]
85
+ self.assertNotIn("_x_pairs.append", block)
86
+ self.assertIn("_gr_mod.view(", block)
87
+
88
+ def test_labels(self):
89
+ aggrieved = gr.refine({"sadness": 0.50, "anger": 0.20})
90
+ self.assertEqual(aggrieved["labels"], ["委屈"])
91
+ disappointed = gr.refine({"sadness": 0.50, "trust": 0.20})
92
+ self.assertEqual(disappointed["labels"], ["失望"])
93
+ ignored = gr.refine({"loneliness": 0.60, "sadness": 0.15})
94
+ self.assertEqual(ignored["labels"], ["被忽视"])
95
+ panel = gr.view({"sadness": 0.50, "anger": 0.20})
96
+ self.assertEqual(panel["line"], "委屈(难过)")
97
+
98
+ def test_panel_projection(self):
99
+ self.assertIn('"emotion_granularity": (self._granularity_payload()',
100
+ MAIN)
101
+ self.assertIn('"emotion_granularity": (self._granularity_payload()',
102
+ ROUTES)
103
+ self.assertIn('id="granularityNote"', SRC_HTML)
104
+ self.assertIn('id="granularityNote"', INDEX_HTML)
105
+ self.assertIn('$("granularityNote")', APP_JS)
106
+
107
+ def test_payload_method(self):
108
+ tree = ast.parse(MAIN)
109
+ nodes = [n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)
110
+ and n.name == "_granularity_payload"]
111
+ self.assertEqual(len(nodes), 1)
112
+ module = ast.Module(body=nodes, type_ignores=[])
113
+ ns = {}
114
+ exec(compile(module, "<granularity_panel>", "exec"), ns)
115
+ import types
116
+ core = types.SimpleNamespace(emotion_granularity={
117
+ "enabled": True, "display": "委屈", "line": "委屈(难过)",
118
+ "primary": "sadness", "primary_zh": "难过",
119
+ "secondary": [{"key": "aggrieved", "label": "委屈"}],
120
+ "fallback": False})
121
+ plugin = types.SimpleNamespace(
122
+ cores={"k": core}, granularity_enabled=True)
123
+ out = ns["_granularity_payload"](plugin)
124
+ self.assertTrue(out["enabled"])
125
+ self.assertEqual(out["line"], "委屈(难过)")
126
+ self.assertEqual(out["secondary"][0]["label"], "委屈")
127
+
128
+
129
+ class AffectiveLaborWiringTest(unittest.TestCase):
130
+ def test_wiring(self):
131
+ self.assertIn("from .biocore_model import affective_labor as _al_mod",
132
+ MAIN)
133
+ self.assertIn("from .biocore_model import affective_labor as _al_mod2",
134
+ MAIN)
135
+ self.assertIn("core.affective_labor = _al_mod2.update(", MAIN)
136
+ self.assertIn("core.affective_labor_view = _al_panel", MAIN)
137
+ self.assertIn("self._labor_felt = {", MAIN)
138
+ self.assertIn("/** task-173", MAIN) if False else None
139
+
140
+ def test_high_load_restrains_through_bus(self):
141
+ self.assertIn('self._x_pairs.append((\n "affective_labor",', MAIN) or \
142
+ self.assertIn('"affective_labor",', MAIN)
143
+ self.assertIn('"length_scale": 0.85', MAIN)
144
+ self.assertIn('>= 0.60', MAIN)
145
+
146
+ def test_load_accumulates(self):
147
+ first = al.update(al.initial(NOW - 10),
148
+ {"v": 0.6, "a": 0.4}, {"v": -0.6, "a": 0.4},
149
+ honesty=0.0, now=NOW)
150
+ second = al.update(first, {"v": 0.6}, {"v": -0.6},
151
+ honesty=0.0, now=NOW + 60)
152
+ self.assertGreater(second["load"], first["load"])
153
+ paused = al.update(second, {"v": 0.6}, {"v": -0.6},
154
+ honesty=0.0, now=NOW + 60)
155
+ self.assertEqual(paused["load"], second["load"])
156
+ view = al.view(second)
157
+ self.assertIn("risk", view)
158
+ self.assertGreaterEqual(view["risk"], 0.0)
159
+
160
+
161
+ class SelfVerificationWiringTest(unittest.TestCase):
162
+ def test_wiring(self):
163
+ self.assertIn("from .biocore_model import self_verification as _sv_mod",
164
+ MAIN)
165
+ self.assertIn("core.self_verification = _sv_state", MAIN)
166
+ self.assertIn("core.self_verification_view = _sv_mod.view(_sv_state)",
167
+ MAIN)
168
+ self.assertIn("_sv_mod.observe(", MAIN)
169
+ self.assertIn("_sv_mod.intent(_sv_state)", MAIN)
170
+
171
+ def test_urge_allows_correction(self):
172
+ misread = sv.observe(sv.initial(NOW - 10), True,
173
+ clarity=0.9, relation=0.9, severity=1.0, now=NOW)
174
+ accurate = sv.observe(sv.initial(NOW - 10), False,
175
+ clarity=0.9, relation=0.9, now=NOW)
176
+ self.assertGreater(misread["urge"], accurate["urge"])
177
+ self.assertEqual(accurate["urge"], 0.0)
178
+ decided = sv.intent(misread)
179
+ self.assertTrue(decided["want_correct"])
180
+ self.assertIn("allow_correction",
181
+ str(xd.normalize({"allow_correction": True}, "sv")))
182
+
183
+
184
+ class BusTest(unittest.TestCase):
185
+ def test_still_single_merge(self):
186
+ self.assertEqual(MAIN.count("merge_named("), 1)
187
+ self.assertIn("merge_named(self._x_pairs)", MAIN)
188
+
189
+ def test_batch2_directives_merge(self):
190
+ merged = xd.merge_named([
191
+ ("affective_labor", {"length_scale": 0.85, "tone": "累了,话少一点",
192
+ "initiative": 0.35}),
193
+ ("self_verification", {"allow_correction": True,
194
+ "tone": "可以补一句纠正"}),
195
+ ("attributor", {"length_scale": 0.9, "tone": "语气硬一点"}),
196
+ ])
197
+ self.assertIn("affective_labor", merged["sources"])
198
+ self.assertIn("self_verification", merged["sources"])
199
+ self.assertTrue(merged["allow_correction"])
200
+ self.assertLessEqual(merged["length_scale"], 0.85)
201
+ self.assertLessEqual(merged["initiative"], 0.5)
202
+
203
+
204
+ class RollbackTest(unittest.TestCase):
205
+ def test_each_block_guarded(self):
206
+ for key in ("granularity_enabled", "affective_labor_enabled",
207
+ "self_verification_enabled"):
208
+ self.assertIn('if bool(getattr(self, "%s", True)):' % key, MAIN)
209
+ self.assertIn('if bool(getattr(self, "boundary_lines_enabled", False)):',
210
+ MAIN)
211
+
212
+
213
+ class PurityTest(unittest.TestCase):
214
+ def test_modules_stay_pure(self):
215
+ for name in ("granularity", "affective_labor", "self_verification"):
216
+ tree = ast.parse((ROOT / "biocore_model" / (name + ".py"))
217
+ .read_text(encoding="utf-8"))
218
+ modules = set()
219
+ for node in ast.walk(tree):
220
+ if isinstance(node, ast.Import):
221
+ modules.update(alias.name.split(".")[0]
222
+ for alias in node.names)
223
+ elif isinstance(node, ast.ImportFrom) and node.module:
224
+ modules.add(node.module.split(".")[0])
225
+ self.assertNotIn("main", modules, name)
226
+ self.assertNotIn("asyncio", modules, name)
227
+ self.assertNotIn("random", modules, name)
228
+
229
+
230
+ if __name__ == "__main__":
231
+ unittest.main()
@@ -0,0 +1,223 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-174 批三接线测试:九个模块 + 唯一 x_directives 总线。
3
+
4
+ 源码断言确认接线位置/开关回退;纯函数断言确认每个模块的自然行为。
5
+ 全部开关默认 ON,关掉后该块整段跳过。
6
+ """
7
+ import ast
8
+ import json
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ from biocore_model import attachment_behavior as ab
13
+ from biocore_model import compassion_fatigue as cf
14
+ from biocore_model import dissonance as di
15
+ from biocore_model import narrative as na
16
+ from biocore_model import possible_selves as ps
17
+ from biocore_model import reconstruction as rc
18
+ from biocore_model import relation_stage as rs
19
+ from biocore_model import secrecy as se
20
+ from biocore_model import source_monitoring as sm
21
+ from biocore_model import value_order as vo
22
+ from biocore_model import x_directives as xd
23
+
24
+ ROOT = Path(__file__).resolve().parents[2]
25
+ MAIN = (ROOT / "main.py").read_text(encoding="utf-8")
26
+ ROUTES = (ROOT / "biocore_model" / "plugin_routes.py").read_text(encoding="utf-8")
27
+ SCHEMA = json.loads((ROOT / "_conf_schema.json").read_text(encoding="utf-8"))
28
+ DASH = ROOT / "pages" / "dashboard"
29
+ SRC_HTML = (DASH / "index.src.html").read_text(encoding="utf-8")
30
+ INDEX_HTML = (DASH / "index.html").read_text(encoding="utf-8")
31
+ APP_JS = (DASH / "app.js").read_text(encoding="utf-8")
32
+
33
+ SWITCHES = ("value_order_enabled", "dissonance_enabled",
34
+ "attachment_behavior_enabled", "relation_stage_enabled",
35
+ "narrative_enabled", "reconstruction_enabled",
36
+ "source_monitoring_enabled", "possible_selves_enabled",
37
+ "compassion_fatigue_enabled", "secrecy_enabled")
38
+
39
+
40
+ class FlagTest(unittest.TestCase):
41
+ def test_schema_defaults_true(self):
42
+ for key in SWITCHES:
43
+ entry = SCHEMA.get(key)
44
+ self.assertIsInstance(entry, dict, key)
45
+ self.assertEqual(entry.get("type"), "bool", key)
46
+ self.assertIs(entry.get("default"), True, key)
47
+
48
+ def test_main_reads_flags(self):
49
+ for key in SWITCHES:
50
+ self.assertIn('self.config.get("%s", True)' % key, MAIN)
51
+
52
+
53
+ class PositionTest(unittest.TestCase):
54
+ def test_single_merge_still(self):
55
+ self.assertEqual(MAIN.count("merge_named("), 1)
56
+ self.assertIn("merge_named(self._x_pairs)", MAIN)
57
+
58
+ def test_value_order(self):
59
+ self.assertIn("from .biocore_model import value_order as _vo_mod", MAIN)
60
+ self.assertIn("core.value_order = _vo_mod.rank(", MAIN)
61
+ block = MAIN[MAIN.index("task-174 batch-3 (nine modules"):
62
+ MAIN.index("# task-173 granularity")]
63
+ self.assertNotIn("_x_pairs.append((\"value_order\"", block)
64
+
65
+ def test_relation_and_attachment_bus(self):
66
+ self.assertIn("core.relation_stage = _rs_result", MAIN)
67
+ self.assertIn('self._x_pairs.append(("relation_stage", _rs_dir))', MAIN)
68
+ self.assertIn("core.attachment_behavior = _ab_result", MAIN)
69
+ self.assertIn(
70
+ 'self._x_pairs.append(\n ("attachment_behavior", _ab_mod.directives(_ab_result)))',
71
+ MAIN)
72
+
73
+ def test_dissonance_and_narrative(self):
74
+ self.assertIn("core.dissonance = _di_mod3.strength(", MAIN)
75
+ self.assertIn('self._x_pairs.append(\n ("dissonance", _di_mod2.directives(_di_prev)))',
76
+ MAIN)
77
+ self.assertIn("core.narrative = _na_mod.summarize(", MAIN)
78
+ self.assertIn('self._x_pairs.append(("narrative", _na_dir))', MAIN)
79
+
80
+ def test_reconstruction_meta_only(self):
81
+ self.assertIn("core.recall_meta = {", MAIN)
82
+ self.assertIn("core.recall_meta = {\n \"enabled\": True,",
83
+ MAIN)
84
+ self.assertIn("_rm_mod.drift(", MAIN)
85
+ self.assertIn("_sm_mod.monitor(", MAIN)
86
+ block = MAIN[MAIN.index("task-174 reconstruction"):
87
+ MAIN.index("from .biocore_model import runtime")]
88
+ for forbidden in ("_save_memory", "write_text", "_remember(", "json.dump"):
89
+ self.assertNotIn(forbidden, block)
90
+ self.assertIn("self._load_memory(event)", block)
91
+
92
+ def test_possible_selves_and_secrecy(self):
93
+ self.assertIn("core.possible_selves = _ps_mod.rank(", MAIN)
94
+ self.assertIn("core.secret = _se_state", MAIN)
95
+ self.assertIn("core.secret_view = _se_mod.view(_se_state)", MAIN)
96
+ # neither module contributes to the length/tone bus
97
+ self.assertNotIn('_x_pairs.append(("possible_selves"', MAIN)
98
+ self.assertNotIn('_x_pairs.append(("secrecy"', MAIN)
99
+
100
+ def test_compassion_fatigue(self):
101
+ self.assertIn("core.compassion_fatigue = _cf_result", MAIN)
102
+ self.assertIn('self._x_pairs.append(("compassion_fatigue", _cf_dir))',
103
+ MAIN)
104
+
105
+
106
+ class PanelTest(unittest.TestCase):
107
+ def test_status_keys(self):
108
+ for key in ("narrative", "possible_selves", "secrecy"):
109
+ self.assertIn('"%s": (self._%s_payload()' % (key, key), MAIN)
110
+ self.assertIn('"%s": (self._%s_payload()' % (key, key), ROUTES)
111
+
112
+ def test_dashboard_rows(self):
113
+ for ident in ("narrativeNote", "possibleSelfNote", "secrecyNote"):
114
+ self.assertIn('id="%s"' % ident, SRC_HTML)
115
+ self.assertIn('id="%s"' % ident, INDEX_HTML)
116
+ self.assertIn('$("%s")' % ident, APP_JS)
117
+ self.assertIn("一致 ", SRC_HTML)
118
+ self.assertIn("矛盾", SRC_HTML)
119
+ self.assertIn("修复", SRC_HTML)
120
+
121
+
122
+ class ValueOrderBehaviourTest(unittest.TestCase):
123
+ def test_security_keeps_status_quo(self):
124
+ weights = vo.normalize({"security": 0.95, "tradition": 0.9})
125
+ result = vo.rank([{"name": "改", "gains": {"security": 0.2}},
126
+ {"name": "不变", "gains": {"security": 0.0},
127
+ "keeps_status_quo": True}],
128
+ weights, {"relationship_age_days": 365})
129
+ self.assertEqual(result["chosen"], "不变")
130
+ self.assertIn("ranked", result)
131
+
132
+
133
+ class StageAttachmentTest(unittest.TestCase):
134
+ def test_low_stage_and_avoidant(self):
135
+ low = rs.evaluate({"interactions": 0})
136
+ self.assertIn(low["stage"], ("陌生", "熟悉"))
137
+ self.assertTrue(rs.directives(low)["length_scale"] <= 1.0)
138
+ av = ab.evaluate("avoidant", {"closeness": 0.3, "stress": 0.8})
139
+ self.assertGreater(av["withdrawal"], av["initiative"])
140
+ self.assertLessEqual(ab.directives(av)["proactive"], 0.5)
141
+
142
+
143
+ class DissonanceNarrativeTest(unittest.TestCase):
144
+ def test_dissonance_high_has_tone(self):
145
+ out = di.strength([{"text": "我不熬夜"}],
146
+ {"kind": "stay_up", "contradicts": ["熬夜"],
147
+ "salience": 1.0})
148
+ self.assertGreater(out["dissonance"], 0.0)
149
+ self.assertTrue(di.directives(out)["tone"])
150
+
151
+ def test_narrative_metrics(self):
152
+ events = [
153
+ {"ts": 1, "text": "吵架", "kind": "rupture", "valence": -0.8,
154
+ "importance": 0.9},
155
+ {"ts": 2, "text": "和好", "kind": "repair", "valence": 0.7,
156
+ "importance": 0.8},
157
+ ]
158
+ out = na.summarize(events)
159
+ for key in ("consistency", "contradiction", "repair"):
160
+ self.assertIn(key, out["metrics"])
161
+ self.assertTrue(out["entries"])
162
+
163
+
164
+ class RecallMetaTest(unittest.TestCase):
165
+ def test_important_memory_does_not_drift(self):
166
+ out = rc.drift({"text": "重要的事", "importance": 0.95},
167
+ mood=0.9, recalls=5)
168
+ self.assertEqual(out["delta"], 0.0)
169
+ self.assertIn("重要记忆不漂", out["reason"])
170
+
171
+ def test_source_monitoring_reports(self):
172
+ out = sm.monitor({"source": "told", "age_days": 300,
173
+ "valence": -0.5, "importance": 0.7},
174
+ {"valence": -0.6, "arousal": 0.6})
175
+ self.assertTrue(out["enabled"])
176
+ self.assertIn(out["source"], sm.SOURCES)
177
+ self.assertGreaterEqual(out["misattribution"], 0.0)
178
+
179
+
180
+ class PossibleSelvesSecrecyTest(unittest.TestCase):
181
+ def test_possible_selves_sentence(self):
182
+ out = ps.rank([{"name": "更会照顾人",
183
+ "target": {"benevolence": 0.9}, "importance": 0.8}],
184
+ {"benevolence": 0.3})
185
+ self.assertTrue(out["sentence"].startswith("她还想成为"))
186
+ self.assertEqual(out["top"], "更会照顾人")
187
+
188
+ def test_secrecy_withhold_high_sensitivity(self):
189
+ out = se.decide(0.95, 0.15, 0.2)
190
+ self.assertEqual(out["action"], "withhold")
191
+ self.assertGreater(out["pressure_add"], 0.0)
192
+ state = se.update(se.initial(1), 0.95, 0.15, 0.2, now=10)
193
+ self.assertGreaterEqual(state["withholds"], 1)
194
+ self.assertIn("pressure", se.view(state))
195
+
196
+
197
+ class RollbackTest(unittest.TestCase):
198
+ def test_guard_literals(self):
199
+ for key in SWITCHES:
200
+ self.assertIn('if bool(getattr(self, "%s", True)):' % key, MAIN)
201
+
202
+
203
+ class PurityTest(unittest.TestCase):
204
+ def test_modules_stay_pure(self):
205
+ for name in ("value_order", "dissonance", "attachment_behavior",
206
+ "relation_stage", "narrative", "reconstruction",
207
+ "source_monitoring", "possible_selves",
208
+ "compassion_fatigue", "secrecy"):
209
+ tree = ast.parse((ROOT / "biocore_model" / (name + ".py"))
210
+ .read_text(encoding="utf-8"))
211
+ modules = set()
212
+ for node in ast.walk(tree):
213
+ if isinstance(node, ast.Import):
214
+ modules.update(alias.name.split(".")[0]
215
+ for alias in node.names)
216
+ elif isinstance(node, ast.ImportFrom) and node.module:
217
+ modules.add(node.module.split(".")[0])
218
+ self.assertNotIn("main", modules, name)
219
+ self.assertNotIn("asyncio", modules, name)
220
+
221
+
222
+ if __name__ == "__main__":
223
+ unittest.main()
@@ -0,0 +1,202 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Life-maintenance tick tests: decay, intero, needs, threshold, idempotency."""
3
+ import os
4
+ import time
5
+ import unittest
6
+ from pathlib import Path
7
+ from unittest import mock
8
+
9
+ from biocore_model import bio_tick as bt
10
+ from biocore_model import sleep_circadian as sleep
11
+
12
+
13
+ class FakeCore:
14
+ def __init__(self, **kw):
15
+ self.v = 0.0
16
+ self.a = 0.5
17
+ self.d = 0.55
18
+ self.social = 0.5
19
+ self.stress = 0.2
20
+ self.energy = 0.6
21
+ self.trust = 0.5
22
+ self.labels = {"emotion": "neutral"}
23
+ self.emotion_decay_factors = {"neutral": 1.0}
24
+ self.valence_decay_minutes = 120.0
25
+ self.arousal_decay_minutes = 30.0
26
+ self.social_decay_minutes = 180.0
27
+ self.circadian_alertness = 0.5
28
+ self.allostatic_load = 0.0
29
+ self.needs = None
30
+ self.sleep_debt = 0.0
31
+ self.last_debt_ts = 0.0
32
+ self.last_bio_tick_ts = 0.0
33
+ self._bio_tick_count = 0
34
+ self.relationship_context = {"state": {"conflict": 0.0, "closeness": 0.5}}
35
+ self.pending_intent = None
36
+ self.intero = None
37
+ self._metabolic_energy_offset = 0.0
38
+ for key, value in kw.items():
39
+ setattr(self, key, value)
40
+
41
+ def snapshot(self):
42
+ return {"v": self.v, "a": self.a, "d": self.d, "social": self.social,
43
+ "stress": self.stress, "energy": self.energy,
44
+ "trust": self.trust, "labels": dict(self.labels),
45
+ "sleep_debt": self.sleep_debt,
46
+ "allostatic_load": self.allostatic_load}
47
+
48
+
49
+ def _now(hour=12):
50
+ return time.mktime((2026, 1, 1, hour, 0, 0, 0, 0, -1))
51
+
52
+
53
+ class DecayTest(unittest.TestCase):
54
+ def test_valence_decays_to_zero(self):
55
+ core = FakeCore(v=-0.5)
56
+ out = bt.bio_tick(core, now=1000.0, dt_hours=1.0)
57
+ self.assertAlmostEqual(core.v, -0.5 * 0.5 ** 0.5, places=4)
58
+ self.assertTrue(out["changed"])
59
+
60
+ def test_arousal_regresses_to_baseline(self):
61
+ core = FakeCore(a=0.8)
62
+ bt.bio_tick(core, now=1000.0, dt_hours=1.0)
63
+ self.assertAlmostEqual(core.a, 0.50, places=4)
64
+
65
+ def test_social_regresses_to_baseline(self):
66
+ core = FakeCore(social=0.2)
67
+ bt.bio_tick(core, now=1000.0, dt_hours=1.0)
68
+ expected = 0.55 + (0.2 - 0.55) * (0.5 ** (60.0 / 180.0))
69
+ self.assertAlmostEqual(core.social, expected, places=4)
70
+
71
+ def test_stress_re_derived_from_affect(self):
72
+ core = FakeCore(v=-0.4, a=0.7, d=0.6, social=0.5)
73
+ bt.bio_tick(core, now=1000.0, dt_hours=1.0)
74
+ expected = (0.45 * max(0.0, -core.v) * core.a + 0.35 * (1.0 - core.d)
75
+ + 0.20 * (1.0 - core.social))
76
+ self.assertAlmostEqual(core.stress, expected, places=2)
77
+
78
+ def test_dt_cap_at_six_hours(self):
79
+ core = FakeCore(v=-1.0)
80
+ out = bt.bio_tick(core, now=1000.0, dt_hours=100.0)
81
+ self.assertEqual(out["dt_hours"], 6.0)
82
+ self.assertAlmostEqual(core.v, -1.0 * 0.5 ** (360.0 / 120.0), places=4)
83
+
84
+
85
+ class InteroNeedsTest(unittest.TestCase):
86
+ def test_intero_updated_and_ts_set(self):
87
+ core = FakeCore(stress=0.6, a=0.7)
88
+ out = bt.bio_tick(core, now=1234.0, dt_hours=1.0)
89
+ self.assertEqual(out["intero"]["ts"], 1234.0)
90
+ self.assertIs(core.intero, out["intero"])
91
+ for dim in ("tension", "warmth", "heaviness", "clarity"):
92
+ self.assertGreaterEqual(out["intero"][dim], 0.0)
93
+ self.assertLessEqual(out["intero"][dim], 1.0)
94
+
95
+ def test_needs_step_runs(self):
96
+ core = FakeCore()
97
+ out = bt.bio_tick(core, now=5000.0, dt_hours=1.0)
98
+ self.assertEqual(core.needs["last_tick_ts"], 5000.0)
99
+ bt.bio_tick(core, now=8600.0, dt_hours=1.0) # second tick applies the silence delta
100
+ self.assertLess(core.needs["belonging"], 0.6)
101
+ self.assertIn("needs", out["log"])
102
+
103
+ def test_sleep_debt_accumulates(self):
104
+ core = FakeCore()
105
+ bt.bio_tick(core, now=5000.0, dt_hours=2.0)
106
+ self.assertAlmostEqual(core.sleep_debt, 1.0, places=3)
107
+ self.assertEqual(core.last_debt_ts, 5000.0)
108
+
109
+ def test_circadian_alertness_set_from_now(self):
110
+ core = FakeCore()
111
+ now = _now(3)
112
+ bt.bio_tick(core, now=now, dt_hours=1.0)
113
+ self.assertAlmostEqual(core.circadian_alertness,
114
+ sleep.circadian(time.localtime(now).tm_hour), places=4)
115
+
116
+ def test_allostatic_load_grows_under_stress(self):
117
+ core = FakeCore(stress=0.9)
118
+ bt.bio_tick(core, now=1000.0, dt_hours=0.0) # no affect decay: keep stress high
119
+ self.assertGreater(core.allostatic_load, 0.0)
120
+
121
+
122
+ class IdempotencyTest(unittest.TestCase):
123
+ def test_same_now_only_applies_once(self):
124
+ core = FakeCore(v=-0.6, a=0.8)
125
+ first = bt.bio_tick(core, now=2000.0, dt_hours=1.0)
126
+ v1, a1, count1 = core.v, core.a, core._bio_tick_count
127
+ second = bt.bio_tick(core, now=2000.0, dt_hours=1.0)
128
+ self.assertFalse(first["idempotent"])
129
+ self.assertTrue(second["idempotent"])
130
+ self.assertFalse(second["changed"])
131
+ self.assertEqual((core.v, core.a, core._bio_tick_count), (v1, a1, count1))
132
+
133
+ def test_log_throttled_every_ten_ticks(self):
134
+ core = FakeCore()
135
+ emits = []
136
+ for index in range(10):
137
+ out = bt.bio_tick(core, now=1000.0 + index * 3600.0, dt_hours=1.0)
138
+ emits.append(out["log"]["emit"])
139
+ self.assertTrue(emits[0]) # first tick always emits (first-run visibility)
140
+ self.assertEqual(emits[1:9], [False] * 8)
141
+ self.assertTrue(emits[9])
142
+ self.assertEqual(out["log"]["count"], 10)
143
+
144
+ def test_env_forces_every_tick(self):
145
+ core = FakeCore()
146
+ with mock.patch.dict(os.environ, {"BIOCORE_BIO_TICK_LOG": "1"}):
147
+ emits = [bt.bio_tick(core, now=5000.0 + index * 3600.0,
148
+ dt_hours=1.0)["log"]["emit"] for index in range(5)]
149
+ self.assertEqual(emits, [True] * 5)
150
+
151
+
152
+ class PendingIntentTest(unittest.TestCase):
153
+ def test_high_discomfort_triggers_intent(self):
154
+ core = FakeCore(stress=0.9, a=0.8, sleep_debt=0.8, v=-0.6, social=0.3,
155
+ trust=0.3, d=0.4, energy=0.2, allostatic_load=0.5,
156
+ labels={"emotion": "sadness"})
157
+ core.relationship_context = {"state": {"conflict": 0.6, "closeness": 0.2}}
158
+ core.intero = {"tension": 0.90, "warmth": 0.10, "heaviness": 0.90,
159
+ "clarity": 0.20, "ts": 0.0}
160
+ out = bt.bio_tick(core, now=3000.0, dt_hours=0.1)
161
+ self.assertGreater(out["discomfort"], bt.DISCOMFORT_TRIGGER)
162
+ intent = out["pending_intent"]
163
+ self.assertIsInstance(intent, dict)
164
+ self.assertIn(intent["action"], ("approach", "share", "question",
165
+ "withdraw", "repair", "rest"))
166
+ self.assertEqual(intent["source"], "bio_tick")
167
+ self.assertTrue(intent["reason"])
168
+
169
+ def test_calm_state_keeps_pending_intent_empty(self):
170
+ core = FakeCore()
171
+ out = bt.bio_tick(core, now=3000.0, dt_hours=1.0)
172
+ self.assertLessEqual(out["discomfort"], bt.DISCOMFORT_TRIGGER)
173
+ self.assertIsNone(out["pending_intent"])
174
+ self.assertIsNone(out["action"])
175
+
176
+
177
+ class PurityTest(unittest.TestCase):
178
+ def test_source_has_no_llm_or_main_import(self):
179
+ source = Path(bt.__file__).read_text(encoding="utf-8").lower()
180
+ self.assertNotIn("llm", source)
181
+ self.assertNotIn("import main", source)
182
+ self.assertNotIn("from main", source)
183
+ self.assertNotIn("requests", source)
184
+ self.assertNotIn("httpx", source)
185
+
186
+ def test_memory_maintenance_throttle_and_forget(self):
187
+ core = FakeCore()
188
+ core.memory_entries = [
189
+ {"id": "a", "retrieval_s": 0.8},
190
+ {"id": "b", "retrieval_s": 0.01},
191
+ ]
192
+ first = bt.memory_maintenance(core, now=1000.0, dt_hours=1.0)
193
+ self.assertFalse(first["skipped"])
194
+ self.assertEqual(first["decayed"], 2)
195
+ self.assertGreaterEqual(first["forgotten"], 1)
196
+ second = bt.memory_maintenance(core, now=1200.0, dt_hours=1.0)
197
+ self.assertTrue(second["skipped"])
198
+ self.assertEqual(second["reason"], "throttled")
199
+
200
+
201
+ if __name__ == "__main__":
202
+ unittest.main()