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,93 @@
1
+ # -*- coding: utf-8 -*-
2
+ """P2-1 learning-loop verification tests (learning_probe.py)."""
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ from biocore_model import learning_probe as lp
7
+ from biocore_model import policy_learning as pl
8
+
9
+ ROOT = Path(__file__).resolve().parents[2]
10
+ SOURCE = (ROOT / "biocore_model" / "learning_probe.py").read_text(encoding="utf-8")
11
+
12
+
13
+ class RepeatRewardTest(unittest.TestCase):
14
+ def test_q_rises_and_steps_are_logged(self):
15
+ table, rows = lp.repeat_reward(rewards=(1.0, 1.0, 1.0))
16
+ self.assertEqual(len(rows), 3)
17
+ self.assertEqual(rows[0]["q"], 0.2)
18
+ self.assertGreater(rows[-1]["q"], rows[0]["q"])
19
+ self.assertIsNone(rows[0]["delta"])
20
+ self.assertIsNotNone(rows[-1]["delta"])
21
+ self.assertIn(lp.DEFAULT_CTX, table)
22
+
23
+ def test_repeated_stimulus_decays_response(self):
24
+ _table, rows = lp.repeat_reward(rewards=(1.0, 1.0, 1.0))
25
+ self.assertTrue(lp.declining_deltas(rows))
26
+ self.assertLess(lp.response_ratio(rows), 1.0)
27
+ self.assertGreater(lp.response_ratio(rows), 0.0)
28
+ deltas = [row["delta"] for row in rows if row["delta"] is not None]
29
+ self.assertEqual(len(deltas), 2)
30
+ self.assertLess(deltas[1], deltas[0])
31
+
32
+ def test_no_reward_no_learning_contrast(self):
33
+ _table, rows = lp.repeat_reward(rewards=(0.0, 0.0, 0.0))
34
+ self.assertEqual([row["q"] for row in rows], [0.0, 0.0, 0.0])
35
+ self.assertFalse(lp.declining_deltas(rows))
36
+ self.assertEqual(lp.response_ratio(rows), 1.0)
37
+
38
+ def test_invalid_inputs_are_safe(self):
39
+ table, rows = lp.repeat_reward(rewards="nope", alpha="x")
40
+ self.assertIsInstance(table, dict)
41
+ self.assertEqual(len(rows), 3)
42
+ _table2, rows2 = lp.repeat_reward(rewards=(1.0,), alpha=0.2)
43
+ self.assertEqual(len(rows2), 1)
44
+ self.assertFalse(lp.declining_deltas(None))
45
+ self.assertEqual(lp.response_ratio(None), 1.0)
46
+
47
+
48
+ class AlphaCurveTest(unittest.TestCase):
49
+ def test_alpha_declines_with_error(self):
50
+ curve = lp.alpha_curve([0.8, 0.4, 0.2, 0.1])
51
+ alphas = [row["alpha"] for row in curve]
52
+ self.assertTrue(all(alpha is not None for alpha in alphas))
53
+ self.assertTrue(all(alphas[i] <= alphas[i - 1]
54
+ for i in range(1, len(alphas))))
55
+
56
+
57
+ class BehaviourChangeTest(unittest.TestCase):
58
+ def test_preference_changes_after_learning(self):
59
+ before = lp.preferred({})
60
+ self.assertTrue(before.get("explored"))
61
+ table, _rows = lp.repeat_reward(rewards=(1.0, 1.0, 1.0))
62
+ after = lp.preferred(table)
63
+ self.assertFalse(after.get("explored"))
64
+ self.assertEqual(after["action"], lp.DEFAULT_ACTION)
65
+ self.assertGreater(after["q"], 0.0)
66
+
67
+ def test_other_action_learned_when_rewarded(self):
68
+ table, _rows = lp.repeat_reward(action="repair", rewards=(1.0, 1.0))
69
+ pick = lp.preferred(table)
70
+ self.assertEqual(pick["action"], "repair")
71
+ self.assertGreater(pick["q"], 0.0)
72
+ self.assertLess(float(table[lp.DEFAULT_CTX]["approach"]["q"]), 1e-9)
73
+
74
+
75
+ class SelfCheckTest(unittest.TestCase):
76
+ def test_report_shape_and_ok(self):
77
+ report = lp.self_check()
78
+ self.assertTrue(report["ok"])
79
+ self.assertTrue(report["declining"])
80
+ self.assertLess(report["ratio"], 1.0)
81
+ self.assertTrue(report["behaviour_changed"])
82
+ self.assertTrue(report["alpha_declines"])
83
+ self.assertEqual(len(report["rows"]), 3)
84
+
85
+ def test_module_is_pure(self):
86
+ self.assertNotIn("import main", SOURCE)
87
+ self.assertNotIn("def open(", SOURCE)
88
+ self.assertNotIn("open(", SOURCE)
89
+ self.assertNotIn("time.", SOURCE)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ unittest.main()
@@ -0,0 +1,222 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Life-loop startup tests: pending flag, running-loop start, bio_tick call.
3
+
4
+ main.py cannot be imported without AstrBot, so the three lifecycle methods are
5
+ extracted from the main.py AST and bound to a fake plugin. This keeps the test
6
+ about the real code while staying headless.
7
+ """
8
+ import ast
9
+ import asyncio
10
+ import time
11
+ import types
12
+ import unittest
13
+ from pathlib import Path
14
+
15
+ PLUG = Path(__file__).resolve().parents[2]
16
+ SOURCE = (PLUG / "main.py").read_text(encoding="utf-8")
17
+
18
+
19
+ class _Log:
20
+ def __init__(self):
21
+ self.rows = []
22
+
23
+ def info(self, fmt, *args):
24
+ self.rows.append(("info", fmt % args if args else fmt))
25
+
26
+ def warning(self, fmt, *args):
27
+ self.rows.append(("warn", fmt % args if args else fmt))
28
+
29
+ def debug(self, fmt, *args):
30
+ self.rows.append(("debug", fmt % args if args else fmt))
31
+
32
+
33
+ def load_methods():
34
+ tree = ast.parse(SOURCE)
35
+ cls = next(n for n in tree.body
36
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
37
+ wanted = ("_ensure_life_task", "_stop_life_task", "_life_loop")
38
+ log = _Log()
39
+ calls = {"bio": 0, "memory": 0}
40
+
41
+ def _bio_tick(core, now, dt_hours=None):
42
+ calls["bio"] += 1
43
+ return {"changed": True, "log": {"emit": False, "count": calls["bio"]}}
44
+
45
+ def _memory_maintenance(core, now, **kwargs):
46
+ calls["memory"] += 1
47
+ return {"skipped": False}
48
+
49
+ fake_tick = types.SimpleNamespace(bio_tick=_bio_tick,
50
+ memory_maintenance=_memory_maintenance)
51
+ fast_asyncio = types.SimpleNamespace(
52
+ get_running_loop=asyncio.get_running_loop,
53
+ create_task=asyncio.create_task,
54
+ CancelledError=asyncio.CancelledError,
55
+ sleep=lambda _delay: asyncio.sleep(0.005),
56
+ )
57
+ ns = {"asyncio": fast_asyncio, "time": time, "logger": log,
58
+ "_bio_tick": fake_tick}
59
+ fns = {}
60
+ for node in cls.body:
61
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in wanted:
62
+ kind = ast.AsyncFunctionDef if isinstance(node, ast.AsyncFunctionDef) else ast.FunctionDef
63
+ clone = kind(name=node.name, args=node.args, body=node.body,
64
+ decorator_list=[], returns=node.returns,
65
+ type_comment=None, lineno=node.lineno,
66
+ col_offset=node.col_offset,
67
+ end_lineno=node.end_lineno,
68
+ end_col_offset=node.end_col_offset)
69
+ exec(compile(ast.Module(body=[clone], type_ignores=[]), "<life>", "exec"), ns)
70
+ fns[node.name] = ns[node.name]
71
+ return fns, log, calls
72
+
73
+
74
+ class FakePlugin:
75
+ def __init__(self, enabled=True):
76
+ self.enabled = enabled
77
+ self.cores = {"s1": object()}
78
+ self._life_task = None
79
+ self._life_start_pending = False
80
+ self.proactive_autonomy = False
81
+ self.autonomy_loop_enabled = False
82
+ self.autonomy_calls = 0
83
+
84
+ def _bio_cores_for_tick(self):
85
+ return list(self.cores.values())
86
+
87
+ async def _autonomy_tick(self, now=None):
88
+ self.autonomy_calls += 1
89
+
90
+
91
+ FNS, LOG, CALLS = load_methods()
92
+
93
+
94
+ def make_plugin(enabled=True):
95
+ plugin = FakePlugin(enabled=enabled)
96
+ plugin._ensure_life_task = types.MethodType(FNS["_ensure_life_task"], plugin)
97
+ plugin._stop_life_task = types.MethodType(FNS["_stop_life_task"], plugin)
98
+ plugin._life_loop = types.MethodType(FNS["_life_loop"], plugin)
99
+ return plugin
100
+
101
+
102
+ class NoLoopStartupTest(unittest.TestCase):
103
+ def test_start_without_running_loop_sets_pending(self):
104
+ plugin = make_plugin()
105
+ plugin._ensure_life_task() # called from a sync context: no loop here
106
+ self.assertIsNone(plugin._life_task)
107
+ self.assertTrue(plugin._life_start_pending)
108
+
109
+ def test_disabled_plugin_does_not_start(self):
110
+ plugin = make_plugin(enabled=False)
111
+ plugin._ensure_life_task()
112
+ self.assertIsNone(plugin._life_task)
113
+ self.assertFalse(plugin._life_start_pending)
114
+
115
+
116
+ class RunningLoopTest(unittest.IsolatedAsyncioTestCase):
117
+ def setUp(self):
118
+ CALLS["bio"] = 0
119
+ CALLS["memory"] = 0
120
+ LOG.rows.clear()
121
+
122
+ async def test_start_inside_loop_creates_task(self):
123
+ plugin = make_plugin()
124
+ plugin._ensure_life_task()
125
+ self.assertIsNotNone(plugin._life_task)
126
+ self.assertFalse(plugin._life_task.done())
127
+ self.assertFalse(plugin._life_start_pending)
128
+ self.assertTrue(any("life loop started" in row[1]
129
+ for row in LOG.rows if row[0] == "info"))
130
+ plugin.enabled = False
131
+ await asyncio.wait_for(plugin._life_task, timeout=1)
132
+
133
+ async def test_second_call_is_idempotent(self):
134
+ plugin = make_plugin()
135
+ plugin._ensure_life_task()
136
+ first = plugin._life_task
137
+ plugin._ensure_life_task()
138
+ self.assertIs(plugin._life_task, first)
139
+ plugin.enabled = False
140
+ await asyncio.wait_for(first, timeout=1)
141
+
142
+ async def test_life_loop_calls_bio_tick(self):
143
+ plugin = make_plugin()
144
+ plugin._ensure_life_task()
145
+ await asyncio.sleep(0.03)
146
+ self.assertGreaterEqual(CALLS["bio"], 1)
147
+ plugin.enabled = False
148
+ await asyncio.wait_for(plugin._life_task, timeout=1)
149
+
150
+ async def test_autonomy_tick_called_only_when_switch_on(self):
151
+ plugin = make_plugin()
152
+ plugin._ensure_life_task()
153
+ await asyncio.sleep(0.03)
154
+ self.assertEqual(plugin.autonomy_calls, 0)
155
+ plugin.proactive_autonomy = True
156
+ for _ in range(200):
157
+ await asyncio.sleep(0.002)
158
+ if plugin._life_tick_n >= 6:
159
+ break
160
+ self.assertGreaterEqual(plugin.autonomy_calls, 1)
161
+ plugin.enabled = False
162
+ await asyncio.wait_for(plugin._life_task, timeout=1)
163
+
164
+ async def test_stop_cancels_and_clears(self):
165
+ plugin = make_plugin()
166
+ plugin._ensure_life_task()
167
+ task = plugin._life_task
168
+ plugin._stop_life_task()
169
+ self.assertIsNone(plugin._life_task)
170
+ self.assertFalse(plugin._life_start_pending)
171
+ await asyncio.sleep(0.02)
172
+ self.assertTrue(task.done())
173
+
174
+ async def test_pending_flag_cleared_after_start(self):
175
+ plugin = make_plugin()
176
+ plugin._ensure_life_task() # no loop: pending (but this test runs in a loop)
177
+ self.assertFalse(plugin._life_start_pending)
178
+ plugin.enabled = False
179
+ await asyncio.wait_for(plugin._life_task, timeout=1)
180
+
181
+ async def test_memory_maintenance_wired(self):
182
+ plugin = make_plugin()
183
+ plugin._ensure_life_task()
184
+ await asyncio.sleep(0.05)
185
+ self.assertGreaterEqual(CALLS["memory"], 0) # every 60th tick; smoke only
186
+ plugin.enabled = False
187
+ await asyncio.wait_for(plugin._life_task, timeout=1)
188
+
189
+
190
+ class DeprecatedAutonomyTaskTest(unittest.TestCase):
191
+ def test_source_has_no_legacy_loop_task(self):
192
+ self.assertNotIn("create_task(self._autonomy_loop())", SOURCE)
193
+
194
+ def test_ensure_autonomy_task_is_noop(self):
195
+ tree = ast.parse(SOURCE)
196
+ cls = next(n for n in tree.body
197
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
198
+ node = next(f for f in cls.body
199
+ if isinstance(f, ast.FunctionDef) and f.name == "_ensure_autonomy_task")
200
+ created = []
201
+ fake_asyncio = types.SimpleNamespace(
202
+ create_task=lambda coro: created.append(coro),
203
+ get_running_loop=asyncio.get_running_loop)
204
+ ns = {"asyncio": fake_asyncio, "logger": _Log()}
205
+ clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
206
+ decorator_list=[], returns=node.returns,
207
+ type_comment=None, lineno=node.lineno,
208
+ col_offset=node.col_offset, end_lineno=node.end_lineno,
209
+ end_col_offset=node.end_col_offset)
210
+ exec(compile(ast.Module(body=[clone], type_ignores=[]),
211
+ "<autonomy>", "exec"), ns)
212
+ plugin = FakePlugin()
213
+ plugin._autonomy_task = None
214
+ plugin.proactive_autonomy = True
215
+ plugin.autonomy_loop_enabled = True
216
+ ns["_ensure_autonomy_task"](plugin)
217
+ self.assertIsNone(plugin._autonomy_task)
218
+ self.assertEqual(created, [])
219
+
220
+
221
+ if __name__ == "__main__":
222
+ unittest.main()
@@ -0,0 +1,131 @@
1
+ # -*- coding: utf-8 -*-
2
+ """10s life loop tests: iteration counters, 6x autonomy, 360x memory, logging."""
3
+ import ast
4
+ import asyncio
5
+ import time
6
+ import types
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
11
+ SOURCE = MAIN.read_text(encoding="utf-8")
12
+
13
+
14
+ def _load_life_loop():
15
+ tree = ast.parse(SOURCE)
16
+ cls = next(n for n in tree.body
17
+ if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
18
+ node = next(f for f in cls.body
19
+ if isinstance(f, ast.AsyncFunctionDef) and f.name == "_life_loop")
20
+ log = []
21
+
22
+ class _Log:
23
+ def info(self, fmt, *args):
24
+ log.append(("info", fmt % args if args else fmt))
25
+
26
+ def warning(self, fmt, *args):
27
+ log.append(("warn", fmt % args if args else fmt))
28
+
29
+ calls = {"bio": 0, "memory": 0, "delays": []}
30
+
31
+ def bio_tick(core, now, dt_hours=None):
32
+ calls["bio"] += 1
33
+ return {"discomfort": 0.0, "log": {"emit": False, "count": calls["bio"]}}
34
+
35
+ def memory(core, now, **kwargs):
36
+ calls["memory"] += 1
37
+
38
+ class FastAsyncio:
39
+ @staticmethod
40
+ def sleep(delay):
41
+ calls["delays"].append(delay)
42
+ return asyncio.sleep(0)
43
+
44
+ @staticmethod
45
+ def create_task(coro):
46
+ return asyncio.ensure_future(coro)
47
+
48
+ ns = {"asyncio": FastAsyncio, "time": time, "logger": _Log(),
49
+ "_bio_tick": types.SimpleNamespace(bio_tick=bio_tick,
50
+ memory_maintenance=memory)}
51
+ clone = ast.AsyncFunctionDef(name=node.name, args=node.args, body=node.body,
52
+ decorator_list=[], returns=node.returns,
53
+ type_comment=None, lineno=node.lineno,
54
+ col_offset=node.col_offset, end_lineno=node.end_lineno,
55
+ end_col_offset=node.end_col_offset)
56
+ exec(compile(ast.Module(body=[clone], type_ignores=[]), "<life10>", "exec"), ns)
57
+ return ns[node.name], log, calls
58
+
59
+
60
+ LIFE, LOG, CALLS = _load_life_loop()
61
+
62
+
63
+ class FakePlugin:
64
+ def __init__(self, autonomy=True):
65
+ self.enabled = True
66
+ self.cores = {"s1": object()}
67
+ self._life_tick_n = 0
68
+ self.proactive_autonomy = bool(autonomy)
69
+ self.autonomy_loop_enabled = False
70
+ self.autonomy_calls = 0
71
+
72
+ def _bio_cores_for_tick(self):
73
+ return list(self.cores.values())
74
+
75
+ def _memory_maintenance_pass(self, now):
76
+ CALLS["memory"] += 1
77
+
78
+ async def _autonomy_tick(self, now=None):
79
+ self.autonomy_calls += 1
80
+
81
+
82
+ class TenSecondLoopTest(unittest.IsolatedAsyncioTestCase):
83
+ def setUp(self):
84
+ CALLS["bio"] = 0
85
+ CALLS["memory"] = 0
86
+ CALLS["delays"] = []
87
+ LOG.clear()
88
+
89
+ async def _run_until(self, plugin, target):
90
+ task = asyncio.create_task(LIFE(plugin))
91
+ for _ in range(200000):
92
+ await asyncio.sleep(0)
93
+ if plugin._life_tick_n >= target:
94
+ break
95
+ plugin.enabled = False
96
+ await asyncio.wait_for(task, timeout=2)
97
+
98
+ def test_static_sleep_is_ten_seconds(self):
99
+ self.assertIn("await asyncio.sleep(10)", SOURCE)
100
+
101
+ async def test_bio_tick_every_iteration_and_autonomy_every_six(self):
102
+ plugin = FakePlugin(autonomy=True)
103
+ await self._run_until(plugin, 13)
104
+ n = plugin._life_tick_n
105
+ self.assertEqual(CALLS["bio"], n)
106
+ self.assertEqual(plugin.autonomy_calls, n // 6)
107
+ self.assertTrue(all(delay == 10 for delay in CALLS["delays"]))
108
+
109
+ async def test_memory_every_360(self):
110
+ plugin = FakePlugin(autonomy=False)
111
+ await self._run_until(plugin, 361)
112
+ n = plugin._life_tick_n
113
+ self.assertEqual(CALLS["memory"], n // 360)
114
+ self.assertEqual(plugin.autonomy_calls, 0)
115
+
116
+ async def test_autonomy_switch_off_never_ticks(self):
117
+ plugin = FakePlugin(autonomy=False)
118
+ await self._run_until(plugin, 12)
119
+ self.assertEqual(plugin.autonomy_calls, 0)
120
+
121
+ async def test_first_and_every_sixth_log(self):
122
+ plugin = FakePlugin(autonomy=False)
123
+ await self._run_until(plugin, 12)
124
+ messages = [row[1] for row in LOG if row[0] == "info"]
125
+ self.assertTrue(any("count': 1" in message for message in messages))
126
+ self.assertTrue(any("count': 6" in message for message in messages))
127
+ self.assertTrue(any("count': 12" in message for message in messages))
128
+
129
+
130
+ if __name__ == "__main__":
131
+ unittest.main()