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.
- package/README.md +11 -0
- package/lib/core-worker.js +268 -0
- package/lib/index.js +160 -70
- package/lib/modules/autonomy.js +125 -0
- package/lib/modules/cache-stats.js +48 -0
- package/lib/modules/core-turn.js +24 -6
- package/lib/modules/expression-inject.js +0 -10
- package/lib/modules/expression-refresh.js +0 -9
- package/lib/modules/memory-capture.js +2 -1
- package/lib/modules/memory-inject.js +50 -5
- package/lib/modules/memory-tools.js +7 -4
- package/lib/modules/python-env.js +46 -0
- package/lib/modules/safety-gate.js +0 -3
- package/lib/modules/series.js +54 -0
- package/lib/modules/switch.js +16 -0
- package/lib/py-bridge.js +81 -15
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +138 -127
- package/python/biocore_memory/engine.py +2653 -2476
- package/python/biocore_memory/recall/embedding.py +129 -0
- package/python/biocore_memory/recall/feedback.py +202 -0
- package/python/biocore_model/preference_drift.py +255 -0
- package/python/biocore_model/tests/__init__.py +1 -0
- package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
- package/python/biocore_model/tests/core_status_check.mjs +29 -0
- package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
- package/python/biocore_model/tests/leak_samples_run.py +18 -0
- package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
- package/python/biocore_model/tests/panel_html_check.mjs +63 -0
- package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
- package/python/biocore_model/tests/render_guard_check.mjs +19 -0
- package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
- package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
- package/python/biocore_model/tests/test_affective_labor.py +102 -0
- package/python/biocore_model/tests/test_appraisal.py +137 -0
- package/python/biocore_model/tests/test_appraisal_action.py +55 -0
- package/python/biocore_model/tests/test_arch_convergence.py +35 -0
- package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
- package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
- package/python/biocore_model/tests/test_attachment_state.py +29 -0
- package/python/biocore_model/tests/test_attribution.py +56 -0
- package/python/biocore_model/tests/test_attributor.py +108 -0
- package/python/biocore_model/tests/test_autonomic.py +69 -0
- package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
- package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
- package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
- package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
- package/python/biocore_model/tests/test_autonomy_check.py +40 -0
- package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
- package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
- package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
- package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
- package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
- package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
- package/python/biocore_model/tests/test_autonomy_model.py +60 -0
- package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
- package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
- package/python/biocore_model/tests/test_autonomy_params.py +163 -0
- package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
- package/python/biocore_model/tests/test_autonomy_self.py +153 -0
- package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
- package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
- package/python/biocore_model/tests/test_autonomy_status.py +35 -0
- package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
- package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
- package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
- package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
- package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
- package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
- package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
- package/python/biocore_model/tests/test_bio_tick.py +202 -0
- package/python/biocore_model/tests/test_boundary.py +183 -0
- package/python/biocore_model/tests/test_boundary_gate.py +295 -0
- package/python/biocore_model/tests/test_brain_split.py +99 -0
- package/python/biocore_model/tests/test_co_recall.py +350 -0
- package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
- package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
- package/python/biocore_model/tests/test_connection_bids.py +69 -0
- package/python/biocore_model/tests/test_core_bridge.py +88 -0
- package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
- package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
- package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
- package/python/biocore_model/tests/test_core_client.py +156 -0
- package/python/biocore_model/tests/test_core_commands.py +146 -0
- package/python/biocore_model/tests/test_core_mode.py +161 -0
- package/python/biocore_model/tests/test_core_state.py +240 -0
- package/python/biocore_model/tests/test_criticality.py +61 -0
- package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
- package/python/biocore_model/tests/test_debug_values.py +169 -0
- package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
- package/python/biocore_model/tests/test_delta_budget.py +157 -0
- package/python/biocore_model/tests/test_diag_trace.py +201 -0
- package/python/biocore_model/tests/test_directives_compat.py +698 -0
- package/python/biocore_model/tests/test_dissonance.py +97 -0
- package/python/biocore_model/tests/test_drift_sign.py +87 -0
- package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
- package/python/biocore_model/tests/test_embedding_local.py +279 -0
- package/python/biocore_model/tests/test_embedding_mode.py +175 -0
- package/python/biocore_model/tests/test_embedding_modes.py +342 -0
- package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
- package/python/biocore_model/tests/test_emotion_probe.py +214 -0
- package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
- package/python/biocore_model/tests/test_entity_graph.py +198 -0
- package/python/biocore_model/tests/test_entity_resolve.py +446 -0
- package/python/biocore_model/tests/test_envy.py +173 -0
- package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
- package/python/biocore_model/tests/test_expr_guard.py +1 -0
- package/python/biocore_model/tests/test_expr_intent.py +66 -0
- package/python/biocore_model/tests/test_expr_state.py +66 -0
- package/python/biocore_model/tests/test_expr_template.py +71 -0
- package/python/biocore_model/tests/test_expr_tone.py +41 -0
- package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
- package/python/biocore_model/tests/test_feedback_flow.py +185 -0
- package/python/biocore_model/tests/test_feedback_guard.py +65 -0
- package/python/biocore_model/tests/test_feedback_llm.py +59 -0
- package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
- package/python/biocore_model/tests/test_feedback_memory.py +63 -0
- package/python/biocore_model/tests/test_feedback_user.py +61 -0
- package/python/biocore_model/tests/test_frustration_curve.py +66 -0
- package/python/biocore_model/tests/test_global_bio_state.py +116 -0
- package/python/biocore_model/tests/test_governance.py +181 -0
- package/python/biocore_model/tests/test_granularity.py +133 -0
- package/python/biocore_model/tests/test_graph_extract.py +224 -0
- package/python/biocore_model/tests/test_graph_recall.py +226 -0
- package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
- package/python/biocore_model/tests/test_growth.py +301 -0
- package/python/biocore_model/tests/test_intent_render.py +60 -0
- package/python/biocore_model/tests/test_intero_regulation.py +224 -0
- package/python/biocore_model/tests/test_intero_style.py +146 -0
- package/python/biocore_model/tests/test_item3_numeric.py +105 -0
- package/python/biocore_model/tests/test_item6_numeric.py +116 -0
- package/python/biocore_model/tests/test_layered_memory.py +174 -0
- package/python/biocore_model/tests/test_leak_guard.py +75 -0
- package/python/biocore_model/tests/test_learning_probe.py +93 -0
- package/python/biocore_model/tests/test_life_loop.py +222 -0
- package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
- package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
- package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
- package/python/biocore_model/tests/test_memory_backend.py +332 -0
- package/python/biocore_model/tests/test_memory_dedup.py +316 -0
- package/python/biocore_model/tests/test_memory_dual.py +219 -0
- package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
- package/python/biocore_model/tests/test_memory_feedback.py +241 -0
- package/python/biocore_model/tests/test_memory_fixes.py +84 -0
- package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -0
- package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
- package/python/biocore_model/tests/test_memory_store.py +372 -0
- package/python/biocore_model/tests/test_metacog.py +103 -0
- package/python/biocore_model/tests/test_migrate.py +245 -0
- package/python/biocore_model/tests/test_migrate_store.py +194 -0
- package/python/biocore_model/tests/test_mind_wander.py +65 -0
- package/python/biocore_model/tests/test_mood.py +35 -0
- package/python/biocore_model/tests/test_narrative.py +114 -0
- package/python/biocore_model/tests/test_needs_esteem.py +51 -0
- package/python/biocore_model/tests/test_needs_events.py +205 -0
- package/python/biocore_model/tests/test_needs_sleep.py +54 -0
- package/python/biocore_model/tests/test_noise_bank.py +54 -0
- package/python/biocore_model/tests/test_numeric_schema.py +98 -0
- package/python/biocore_model/tests/test_output_protocol.py +65 -0
- package/python/biocore_model/tests/test_page_debug.py +147 -0
- package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
- package/python/biocore_model/tests/test_pipeline.py +345 -0
- package/python/biocore_model/tests/test_pipeline_api.py +192 -0
- package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
- package/python/biocore_model/tests/test_plugin_config.py +85 -0
- package/python/biocore_model/tests/test_policy_learning.py +116 -0
- package/python/biocore_model/tests/test_possible_selves.py +88 -0
- package/python/biocore_model/tests/test_predictor.py +227 -0
- package/python/biocore_model/tests/test_preference_drift.py +142 -0
- package/python/biocore_model/tests/test_preset.py +142 -0
- package/python/biocore_model/tests/test_proactive_context.py +70 -0
- package/python/biocore_model/tests/test_proactive_history.py +90 -0
- package/python/biocore_model/tests/test_proactive_memory.py +141 -0
- package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
- package/python/biocore_model/tests/test_probe_contract.py +71 -0
- package/python/biocore_model/tests/test_prospective.py +154 -0
- package/python/biocore_model/tests/test_provider_fallback.py +89 -0
- package/python/biocore_model/tests/test_reconstruction.py +111 -0
- package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
- package/python/biocore_model/tests/test_relation_stage.py +114 -0
- package/python/biocore_model/tests/test_relationship_stage.py +59 -0
- package/python/biocore_model/tests/test_release_140.py +55 -0
- package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
- package/python/biocore_model/tests/test_reply_limit.py +86 -0
- package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
- package/python/biocore_model/tests/test_reply_split.py +52 -0
- package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
- package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
- package/python/biocore_model/tests/test_rerank.py +229 -0
- package/python/biocore_model/tests/test_rollback.py +175 -0
- package/python/biocore_model/tests/test_runtime.py +175 -0
- package/python/biocore_model/tests/test_runtime_active.py +205 -0
- package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
- package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
- package/python/biocore_model/tests/test_scheduler.py +79 -0
- package/python/biocore_model/tests/test_secrecy.py +105 -0
- package/python/biocore_model/tests/test_self_esteem.py +45 -0
- package/python/biocore_model/tests/test_self_intent.py +347 -0
- package/python/biocore_model/tests/test_self_interest.py +183 -0
- package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
- package/python/biocore_model/tests/test_self_verification.py +113 -0
- package/python/biocore_model/tests/test_situation.py +142 -0
- package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
- package/python/biocore_model/tests/test_social_pain.py +49 -0
- package/python/biocore_model/tests/test_social_repair.py +67 -0
- package/python/biocore_model/tests/test_social_theory.py +78 -0
- package/python/biocore_model/tests/test_source_monitoring.py +204 -0
- package/python/biocore_model/tests/test_speak_like_group.py +36 -0
- package/python/biocore_model/tests/test_stages.py +226 -0
- package/python/biocore_model/tests/test_state_tag.py +86 -0
- package/python/biocore_model/tests/test_template_expr.py +65 -0
- package/python/biocore_model/tests/test_text_inference.py +70 -0
- package/python/biocore_model/tests/test_theta_params.py +115 -0
- package/python/biocore_model/tests/test_topic_control.py +200 -0
- package/python/biocore_model/tests/test_turn_memory.py +105 -0
- package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
- package/python/biocore_model/tests/test_ui_settings.py +84 -0
- package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
- package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
- package/python/biocore_model/tests/test_user_classify.py +122 -0
- package/python/biocore_model/tests/test_value_order.py +117 -0
- package/python/biocore_model/tests/test_voice_bank.py +105 -0
- package/python/biocore_model/tests/test_waiting_sense.py +216 -0
- package/python/biocore_model/tests/test_x_directives.py +101 -0
- package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
- package/python/biocore_model/tests/view_mode_check.mjs +17 -0
- package/python/biocore_runtime/autonomy.py +208 -0
- package/python/biocore_runtime/core_state.py +302 -0
- package/python/biocore_runtime/governance.py +495 -0
- package/python/biocore_runtime/growth.py +528 -0
- package/python/biocore_runtime/pipeline.py +850 -23
- package/python/biocore_runtime/relationship_state.py +214 -0
- package/python/biocore_runtime/self_layer.py +103 -0
- package/python/biocore_runtime/service.py +162 -0
- package/python/biocore_runtime/standby.py +472 -0
- package/ui/panel-layout.js +83 -39
- package/ui/panel.css +885 -743
- package/ui/panel.html +356 -296
- 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()
|