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,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()
|