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,209 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-161 沉默/退出单测:四种动作、概率分布、抽样、关闭、输入健壮性。
|
|
3
|
+
|
|
4
|
+
只依赖 biocore_model.exit_or_silence 的纯函数;不导入 main.py,不新增
|
|
5
|
+
LLM 调用。
|
|
6
|
+
"""
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
from biocore_model import exit_or_silence as es
|
|
10
|
+
|
|
11
|
+
CALM = {"closeness": 0.5, "trust": 0.5, "conflict": 0.0}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def decide(**kwargs):
|
|
15
|
+
return es.exit_or_silence(**kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ContractTest(unittest.TestCase):
|
|
19
|
+
def test_actions(self):
|
|
20
|
+
self.assertEqual(es.ACTIONS,
|
|
21
|
+
("respond", "delay", "silent", "end_topic"))
|
|
22
|
+
|
|
23
|
+
def test_result_keys(self):
|
|
24
|
+
out = decide()
|
|
25
|
+
for key in ("enabled", "action", "probability", "distribution",
|
|
26
|
+
"delay_seconds", "reason", "roll"):
|
|
27
|
+
self.assertIn(key, out)
|
|
28
|
+
|
|
29
|
+
def test_distribution_is_probability(self):
|
|
30
|
+
for state in (None, {"energy": 0.1, "stress": 0.9},
|
|
31
|
+
{"energy": 0.9, "stress": 0.1, "valence": -0.8}):
|
|
32
|
+
out = decide(state=state)
|
|
33
|
+
self.assertAlmostEqual(sum(out["distribution"].values()), 1.0,
|
|
34
|
+
places=2)
|
|
35
|
+
for action, value in out["distribution"].items():
|
|
36
|
+
self.assertIn(action, es.ACTIONS)
|
|
37
|
+
self.assertGreaterEqual(value, 0.0)
|
|
38
|
+
self.assertLessEqual(value, 1.0)
|
|
39
|
+
self.assertEqual(out["probability"],
|
|
40
|
+
out["distribution"][out["action"]])
|
|
41
|
+
|
|
42
|
+
def test_reason_present(self):
|
|
43
|
+
for state in (None, {"energy": 0.1, "stress": 0.9},
|
|
44
|
+
{"energy": 0.9, "stress": 0.1}):
|
|
45
|
+
self.assertTrue(decide(state=state)["reason"])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ActionTest(unittest.TestCase):
|
|
49
|
+
def test_rested_and_comfortable_responds(self):
|
|
50
|
+
out = decide(state={"energy": 0.7, "stress": 0.2, "valence": 0.2},
|
|
51
|
+
relationship=CALM, topic={"intensity": 0.35})
|
|
52
|
+
self.assertEqual(out["action"], "respond")
|
|
53
|
+
self.assertEqual(out["delay_seconds"], 0)
|
|
54
|
+
|
|
55
|
+
def test_exhausted_and_tense_goes_silent(self):
|
|
56
|
+
out = decide(state={"energy": 0.0, "stress": 1.0, "valence": -1.0},
|
|
57
|
+
relationship=CALM)
|
|
58
|
+
self.assertEqual(out["action"], "silent")
|
|
59
|
+
self.assertEqual(out["delay_seconds"], 0)
|
|
60
|
+
|
|
61
|
+
def test_heavy_topic_ends_topic(self):
|
|
62
|
+
out = decide(state={"energy": 0.5, "stress": 0.5, "valence": -0.4},
|
|
63
|
+
relationship={"closeness": 0.3, "trust": 0.2,
|
|
64
|
+
"conflict": 0.5},
|
|
65
|
+
topic={"intensity": 1.0})
|
|
66
|
+
self.assertEqual(out["action"], "end_topic")
|
|
67
|
+
|
|
68
|
+
def test_one_foot_out_the_door_delays(self):
|
|
69
|
+
out = decide(state={"energy": 0.4, "stress": 0.9, "valence": 0.0},
|
|
70
|
+
relationship={"closeness": 0.6, "trust": 0.6},
|
|
71
|
+
topic={"intensity": 0.5})
|
|
72
|
+
self.assertEqual(out["action"], "delay")
|
|
73
|
+
self.assertGreaterEqual(out["delay_seconds"], es.DELAY_MIN_S)
|
|
74
|
+
self.assertLessEqual(out["delay_seconds"], es.DELAY_MAX_S)
|
|
75
|
+
|
|
76
|
+
def test_conflict_does_not_force_respond(self):
|
|
77
|
+
out = decide(state={"energy": 0.6, "stress": 0.4, "valence": -0.3},
|
|
78
|
+
relationship={"conflict": 0.8},
|
|
79
|
+
topic={"intensity": 0.5})
|
|
80
|
+
self.assertIn(out["action"], ("delay", "silent", "end_topic"))
|
|
81
|
+
self.assertLess(out["distribution"]["respond"], 0.30)
|
|
82
|
+
|
|
83
|
+
def test_close_relationship_raises_respond_probability(self):
|
|
84
|
+
near = decide(state={"energy": 0.6, "stress": 0.3},
|
|
85
|
+
relationship={"closeness": 0.95, "trust": 0.9},
|
|
86
|
+
topic={"intensity": 0.3})
|
|
87
|
+
far = decide(state={"energy": 0.6, "stress": 0.3},
|
|
88
|
+
relationship={"closeness": 0.1, "trust": 0.1},
|
|
89
|
+
topic={"intensity": 0.3})
|
|
90
|
+
self.assertGreater(near["distribution"]["respond"],
|
|
91
|
+
far["distribution"]["respond"])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TopicIntensityTest(unittest.TestCase):
|
|
95
|
+
def test_higher_intensity_raises_end_topic(self):
|
|
96
|
+
low = decide(state={"energy": 0.6, "stress": 0.3},
|
|
97
|
+
topic={"intensity": 0.2})
|
|
98
|
+
high = decide(state={"energy": 0.6, "stress": 0.3},
|
|
99
|
+
topic={"intensity": 1.0})
|
|
100
|
+
self.assertGreater(high["distribution"]["end_topic"],
|
|
101
|
+
low["distribution"]["end_topic"])
|
|
102
|
+
|
|
103
|
+
def test_heavy_kind_counts_as_heavy_topic(self):
|
|
104
|
+
plain = decide(topic={"intensity": 0.1})
|
|
105
|
+
heavy = decide(topic={"kind": "conflict", "intensity": 0.1})
|
|
106
|
+
self.assertGreater(heavy["distribution"]["end_topic"],
|
|
107
|
+
plain["distribution"]["end_topic"])
|
|
108
|
+
|
|
109
|
+
def test_numeric_topic_is_accepted(self):
|
|
110
|
+
out = decide(topic=0.9)
|
|
111
|
+
self.assertIn(out["action"], es.ACTIONS)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class SamplingTest(unittest.TestCase):
|
|
115
|
+
def test_roll_zero_is_respond(self):
|
|
116
|
+
out = decide(state={"energy": 0.1, "stress": 0.9}, roll=0.0)
|
|
117
|
+
self.assertEqual(out["action"], "respond")
|
|
118
|
+
|
|
119
|
+
def test_high_roll_reaches_last_action(self):
|
|
120
|
+
out = decide(state={"energy": 0.1, "stress": 0.9}, roll=0.9999)
|
|
121
|
+
self.assertEqual(out["action"], "end_topic")
|
|
122
|
+
|
|
123
|
+
def test_roll_reports_action_probability(self):
|
|
124
|
+
out = decide(state={"energy": 0.1, "stress": 0.9}, roll=0.9)
|
|
125
|
+
self.assertEqual(out["probability"],
|
|
126
|
+
out["distribution"][out["action"]])
|
|
127
|
+
self.assertIsNotNone(out["roll"])
|
|
128
|
+
|
|
129
|
+
def test_roll_is_clamped(self):
|
|
130
|
+
for roll in (-1.0, 2.0):
|
|
131
|
+
out = decide(roll=roll)
|
|
132
|
+
self.assertIn(out["action"], es.ACTIONS)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SwitchTest(unittest.TestCase):
|
|
136
|
+
def test_disabled_always_responds(self):
|
|
137
|
+
out = decide(state={"energy": 0.0, "stress": 1.0},
|
|
138
|
+
flags={"enabled": False})
|
|
139
|
+
self.assertFalse(out["enabled"])
|
|
140
|
+
self.assertEqual(out["action"], "respond")
|
|
141
|
+
self.assertEqual(out["probability"], 1.0)
|
|
142
|
+
self.assertEqual(out["distribution"]["respond"], 1.0)
|
|
143
|
+
self.assertIn("关闭", out["reason"])
|
|
144
|
+
|
|
145
|
+
def test_disabled_directives_send(self):
|
|
146
|
+
out = es.directives(decide(state={"energy": 0.0},
|
|
147
|
+
flags={"enabled": False}))
|
|
148
|
+
self.assertTrue(out["send"])
|
|
149
|
+
self.assertEqual(out["action"], "respond")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class HelpersTest(unittest.TestCase):
|
|
153
|
+
def test_should_reply_matches_action(self):
|
|
154
|
+
silent = decide(state={"energy": 0.0, "stress": 1.0,
|
|
155
|
+
"valence": -1.0})
|
|
156
|
+
self.assertEqual(silent["action"], "silent")
|
|
157
|
+
self.assertFalse(es.should_reply(silent))
|
|
158
|
+
self.assertTrue(es.should_reply(decide()))
|
|
159
|
+
self.assertTrue(es.should_reply(None))
|
|
160
|
+
|
|
161
|
+
def test_directives_map_actions(self):
|
|
162
|
+
self.assertFalse(es.directives(decide(
|
|
163
|
+
state={"energy": 0.0, "stress": 1.0, "valence": -1.0}))["send"])
|
|
164
|
+
delay = decide(state={"energy": 0.4, "stress": 0.9},
|
|
165
|
+
relationship={"closeness": 0.6, "trust": 0.6},
|
|
166
|
+
topic={"intensity": 0.5})
|
|
167
|
+
self.assertEqual(delay["action"], "delay")
|
|
168
|
+
directive = es.directives(delay)
|
|
169
|
+
self.assertTrue(directive["send"])
|
|
170
|
+
self.assertEqual(directive["delay_seconds"], delay["delay_seconds"])
|
|
171
|
+
self.assertGreater(directive["delay_seconds"], 0)
|
|
172
|
+
|
|
173
|
+
def test_directives_bounded(self):
|
|
174
|
+
for state in (None, {"energy": 0.1, "stress": 0.9},
|
|
175
|
+
{"energy": 0.9, "valence": -0.9}):
|
|
176
|
+
out = es.directives(decide(state=state))
|
|
177
|
+
self.assertGreaterEqual(out["length_scale"], 0.0)
|
|
178
|
+
self.assertLessEqual(out["length_scale"], 1.0)
|
|
179
|
+
self.assertIn("tone", out)
|
|
180
|
+
|
|
181
|
+
def test_delay_seconds_only_for_delay(self):
|
|
182
|
+
out = decide(state={"energy": 0.0, "stress": 1.0,
|
|
183
|
+
"valence": -1.0})
|
|
184
|
+
self.assertEqual(out["action"], "silent")
|
|
185
|
+
self.assertEqual(out["delay_seconds"], 0)
|
|
186
|
+
self.assertEqual(decide()["delay_seconds"], 0)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class RobustnessTest(unittest.TestCase):
|
|
190
|
+
def test_none_and_odd_inputs(self):
|
|
191
|
+
for state in (None, {}, "x", 3, []):
|
|
192
|
+
out = decide(state=state)
|
|
193
|
+
self.assertIn(out["action"], es.ACTIONS)
|
|
194
|
+
self.assertEqual(decide()["action"], "respond")
|
|
195
|
+
|
|
196
|
+
def test_nan_values_are_safe(self):
|
|
197
|
+
out = decide(state={"energy": float("nan"),
|
|
198
|
+
"stress": None, "valence": "x"})
|
|
199
|
+
self.assertIn(out["action"], es.ACTIONS)
|
|
200
|
+
self.assertTrue(out["reason"])
|
|
201
|
+
|
|
202
|
+
def test_relationship_defaults_from_state(self):
|
|
203
|
+
out = decide(state={"closeness": 0.1, "trust": 0.1},
|
|
204
|
+
relationship={})
|
|
205
|
+
self.assertEqual(out["action"], "respond")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
unittest.main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from biocore_model.tests.test_expr_template import GuardTest # noqa: F401
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import random, unittest
|
|
3
|
+
from biocore_model import expr_intent as ei
|
|
4
|
+
|
|
5
|
+
NOW = 1_700_000_000
|
|
6
|
+
|
|
7
|
+
def cand(kind, **kw):
|
|
8
|
+
data = {'kind': kind, 'text_hint': kind + ' hint', 'novelty': 1.0}
|
|
9
|
+
data.update(kw)
|
|
10
|
+
return data
|
|
11
|
+
|
|
12
|
+
class IntentTest(unittest.TestCase):
|
|
13
|
+
def test_eight_kinds_available(self):
|
|
14
|
+
for kind in ei.BASE:
|
|
15
|
+
self.assertIn(kind, ei.BASE)
|
|
16
|
+
def test_picks_some_kind(self):
|
|
17
|
+
out = ei.pick_intent([cand(k) for k in ei.BASE], {}, random.Random(1))
|
|
18
|
+
self.assertIn(out['kind'], ei.BASE)
|
|
19
|
+
def test_text_hint_returned(self):
|
|
20
|
+
out = ei.pick_intent([cand('repair')], {}, random.Random(1))
|
|
21
|
+
self.assertEqual(out['text_hint'], 'repair hint')
|
|
22
|
+
def test_novelty_excludes_low(self):
|
|
23
|
+
out = ei.pick_intent([cand('repair', novelty=0.2), cand('question', novelty=1.0)], {}, random.Random(1))
|
|
24
|
+
self.assertEqual(out['kind'], 'question')
|
|
25
|
+
def test_gratitude_requires_event(self):
|
|
26
|
+
out = ei.pick_intent([cand('gratitude'), cand('comfort')], {}, random.Random(1))
|
|
27
|
+
self.assertEqual(out['kind'], 'comfort')
|
|
28
|
+
def test_memory_due_requires_event(self):
|
|
29
|
+
self.assertNotEqual(ei.pick_intent([cand('memory_due')], {}, random.Random(1))['kind'], 'memory_due')
|
|
30
|
+
def test_memory_due_requires_window(self):
|
|
31
|
+
out = ei.pick_intent([cand('memory_due', event_ref='e1', due_ts=NOW + 10 * 86400)], {'now': NOW}, random.Random(1))
|
|
32
|
+
self.assertEqual(out['kind'], '')
|
|
33
|
+
def test_bid_frequency_modulates(self):
|
|
34
|
+
low = ei.pick_intent([cand('question')], {'bid_frequency': 0.5}, random.Random(0))
|
|
35
|
+
high = ei.pick_intent([cand('question')], {'bid_frequency': 1.3}, random.Random(0))
|
|
36
|
+
self.assertGreater(high['weight'], low['weight'])
|
|
37
|
+
def test_responsiveness_modulates_self_life(self):
|
|
38
|
+
low = ei.pick_intent([cand('self_life')], {'responsiveness': 0.1}, random.Random(0))
|
|
39
|
+
high = ei.pick_intent([cand('self_life')], {'responsiveness': 0.9}, random.Random(0))
|
|
40
|
+
self.assertGreater(high['weight'], low['weight'])
|
|
41
|
+
def test_deactivating_halves_self_life(self):
|
|
42
|
+
normal = ei.pick_intent([cand('self_life')], {}, random.Random(0))
|
|
43
|
+
avoidant = ei.pick_intent([cand('self_life')], {'attachment_mode': 'deactivating'}, random.Random(0))
|
|
44
|
+
self.assertAlmostEqual(avoidant['weight'], normal['weight'] * 0.5, places=3)
|
|
45
|
+
def test_consecutive_same_kind_excluded(self):
|
|
46
|
+
out = ei.pick_intent([cand('repair'), cand('comfort')], {'recent_kinds': ['repair', 'repair']}, random.Random(1))
|
|
47
|
+
self.assertEqual(out['kind'], 'comfort')
|
|
48
|
+
def test_single_recent_kind_allowed(self):
|
|
49
|
+
out = ei.pick_intent([cand('repair')], {'recent_kinds': ['repair']}, random.Random(1))
|
|
50
|
+
self.assertEqual(out['kind'], 'repair')
|
|
51
|
+
def test_deterministic_with_seed(self):
|
|
52
|
+
args = ([cand(k) for k in ei.BASE], {}, random.Random(7))
|
|
53
|
+
first = ei.pick_intent(*args)
|
|
54
|
+
second = ei.pick_intent([cand(k) for k in ei.BASE], {}, random.Random(7))
|
|
55
|
+
self.assertEqual(first, second)
|
|
56
|
+
def test_alternatives_listed(self):
|
|
57
|
+
out = ei.pick_intent([cand('repair'), cand('comfort'), cand('question')], {}, random.Random(1))
|
|
58
|
+
self.assertNotIn(out['kind'], out['alternatives']); self.assertEqual(len(out['alternatives']), 2)
|
|
59
|
+
def test_empty_candidates(self):
|
|
60
|
+
self.assertEqual(ei.pick_intent([], {}, random.Random(1))['kind'], '')
|
|
61
|
+
def test_no_field_name_leak_in_hint(self):
|
|
62
|
+
out = ei.pick_intent([cand('self_life')], {}, random.Random(1))
|
|
63
|
+
self.assertNotIn('valence=', out['text_hint'])
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
unittest.main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import expr_state as es
|
|
4
|
+
|
|
5
|
+
FULL = {
|
|
6
|
+
'labels': {'emotion': 'joy', 'intensity': 0.62},
|
|
7
|
+
'spectrum_probs': {'joy': 0.62, 'contentment': 0.23, 'amusement': 0.15},
|
|
8
|
+
'v': 0.42, 'a': 0.66, 'd': 0.52, 'social': 0.61, 'stress': 0.24, 'energy': 0.81,
|
|
9
|
+
'mood': {'valence': -0.15, 'arousal': 0.4, 'label': '偏低'},
|
|
10
|
+
'needs': {'belonging': 0.42, 'control': 0.58, 'meaningful_existence': 0.60},
|
|
11
|
+
'self_esteem': 0.51, 'social_pain': 0.35,
|
|
12
|
+
'attachment': {'activation': 0.42, 'mode': 'hyperactivating'},
|
|
13
|
+
'regulation': {'strategy': 'suppress', 'effort': 0.30},
|
|
14
|
+
'sleep_debt': 1.2,
|
|
15
|
+
'attribution': {'locus': 'external', 'stability': 'unstable'},
|
|
16
|
+
}
|
|
17
|
+
REL = {'state': {'closeness': 0.61, 'trust': 0.57, 'conflict': 0.22}}
|
|
18
|
+
|
|
19
|
+
class ExprStateTest(unittest.TestCase):
|
|
20
|
+
def test_full_render_has_sections(self):
|
|
21
|
+
out = es.render_state_block(FULL, REL)
|
|
22
|
+
for key in ('joy', '情绪还挺杂', '心情', '自尊', '被冷落', '依恋', '在忍着', '关系', '觉得是'):
|
|
23
|
+
self.assertIn(key, out['text'])
|
|
24
|
+
def test_empty_returns_empty(self):
|
|
25
|
+
out = es.render_state_block({}, {})
|
|
26
|
+
self.assertEqual(out['text'], ''); self.assertEqual(out['parts_used'], [])
|
|
27
|
+
def test_missing_fields_skip(self):
|
|
28
|
+
out = es.render_state_block({'labels': {'emotion': 'neutral'}}, {})
|
|
29
|
+
self.assertIn('neutral', out['text'])
|
|
30
|
+
def test_budget_truncates_optional(self):
|
|
31
|
+
out = es.render_state_block(FULL, REL, budget_chars=80)
|
|
32
|
+
self.assertTrue(out['truncated']); self.assertLessEqual(len(out['text']), 80)
|
|
33
|
+
def test_mandatory_kept_under_budget(self):
|
|
34
|
+
out = es.render_state_block(FULL, REL, budget_chars=200)
|
|
35
|
+
for key in ('joy', '情绪还挺杂', '心情'):
|
|
36
|
+
self.assertIn(key, out['text'])
|
|
37
|
+
def test_parts_used_lists_present(self):
|
|
38
|
+
out = es.render_state_block(FULL, REL)
|
|
39
|
+
self.assertIn('emotion', out['parts_used']); self.assertIn('relationship', out['parts_used'])
|
|
40
|
+
def test_no_field_names(self):
|
|
41
|
+
out = es.render_state_block(FULL, REL)
|
|
42
|
+
for name in es.FIELD_NAMES:
|
|
43
|
+
self.assertNotIn(name + '=', out['text'])
|
|
44
|
+
def test_adverb_mapping(self):
|
|
45
|
+
self.assertEqual(es.adverb(0.85), '很')
|
|
46
|
+
self.assertEqual(es.adverb(0.65), '有点')
|
|
47
|
+
self.assertEqual(es.adverb(0.45), '略')
|
|
48
|
+
self.assertEqual(es.adverb(0.1), '几乎不')
|
|
49
|
+
def test_self_esteem_note(self):
|
|
50
|
+
self.assertIn('犹豫', es.render_state_block({'self_esteem': 0.3}, {})['text'])
|
|
51
|
+
def test_attachment_modes(self):
|
|
52
|
+
text = es.render_state_block({'attachment': {'activation': 0.5, 'mode': 'deactivating'}}, {})['text']
|
|
53
|
+
self.assertIn('想退开', text)
|
|
54
|
+
def test_sleep_debt_threshold(self):
|
|
55
|
+
self.assertEqual(es.render_state_block({'sleep_debt': 0.1}, {})['text'], '')
|
|
56
|
+
self.assertIn('有点累', es.render_state_block({'sleep_debt': 1.2}, {})['text'])
|
|
57
|
+
def test_relationship_values(self):
|
|
58
|
+
text = es.render_state_block({}, REL)['text']
|
|
59
|
+
self.assertIn('亲近', text); self.assertIn('信任', text)
|
|
60
|
+
def test_attribution_neutral_skipped(self):
|
|
61
|
+
self.assertEqual(es.render_state_block({'attribution': {'locus': 'neutral'}}, {})['text'], '')
|
|
62
|
+
def test_render_is_deterministic(self):
|
|
63
|
+
self.assertEqual(es.render_state_block(FULL, REL), es.render_state_block(FULL, REL))
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
unittest.main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import re, unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from biocore_model import expr_template as et, expr_guard as eg
|
|
5
|
+
|
|
6
|
+
OPTS = {'persona': '你是小灰', 'speak_style': '像在群里回消息', 'output_protocol': '只输出正文 + <bc>'}
|
|
7
|
+
CTX = {'state_block': '她现在有点低落', 'relationship': '亲近', 'gap': '几小时前', 'event_hints': ['他提过体检']}
|
|
8
|
+
|
|
9
|
+
class TemplateTest(unittest.TestCase):
|
|
10
|
+
def test_system_persona_then_speak(self):
|
|
11
|
+
out = et.build_proactive(state_ctx=CTX, opts=OPTS)
|
|
12
|
+
self.assertLess(out['system'].index('小灰'), out['system'].index('群里'))
|
|
13
|
+
def test_user_order(self):
|
|
14
|
+
out = et.build_proactive(state_ctx=CTX, intent={'kind': 'comfort', 'text_hint': '想关心'}, tone='语气自然', guard=eg.guard_block(), opts=OPTS)
|
|
15
|
+
self.assertEqual(out['order'], ['状态', '关系', '间隔', '你记得', '意图', '语气'])
|
|
16
|
+
def test_user_text_order(self):
|
|
17
|
+
out = et.build_proactive(state_ctx=CTX, intent={'text_hint': '想关心'}, tone='语气自然', guard=eg.guard_block(), opts=OPTS)
|
|
18
|
+
user = out['user']
|
|
19
|
+
self.assertLess(user.index('低落'), user.index('亲近'))
|
|
20
|
+
self.assertLess(user.index('亲近'), user.index('几小时前'))
|
|
21
|
+
self.assertLess(user.index('想关心'), user.index('语气自然'))
|
|
22
|
+
self.assertNotIn('【输出格式', user)
|
|
23
|
+
self.assertNotIn('<bc>', out['system'])
|
|
24
|
+
def test_missing_optional_skipped(self):
|
|
25
|
+
out = et.build_proactive(state_ctx={'state_block': '平静'}, opts=OPTS)
|
|
26
|
+
self.assertEqual(out['order'], ['状态'])
|
|
27
|
+
def test_intent_kind_fallback(self):
|
|
28
|
+
out = et.build_proactive(intent={'kind': 'repair'}, opts=OPTS)
|
|
29
|
+
self.assertIn('repair', out['user'])
|
|
30
|
+
def test_deterministic(self):
|
|
31
|
+
a = et.build_proactive(state_ctx=CTX, intent={'text_hint': 'x'}, tone='t', guard='g', opts=OPTS)
|
|
32
|
+
b = et.build_proactive(state_ctx=CTX, intent={'text_hint': 'x'}, tone='t', guard='g', opts=OPTS)
|
|
33
|
+
self.assertEqual(a, b)
|
|
34
|
+
def test_no_core_assignment(self):
|
|
35
|
+
for name in ('expr_template.py', 'expr_guard.py'):
|
|
36
|
+
source = Path(__file__).resolve().parent.parent.joinpath(name).read_text(encoding='utf-8')
|
|
37
|
+
self.assertIsNone(re.search(r'core\.[A-Za-z_]+\s*=', source), name)
|
|
38
|
+
def test_build_reply_has_no_intent(self):
|
|
39
|
+
out = et.build_reply(state_ctx=CTX, tone='语气自然', guard='g', opts=OPTS)
|
|
40
|
+
self.assertNotIn('意图', out['order'])
|
|
41
|
+
def test_hints_list_joined(self):
|
|
42
|
+
out = et.build_reply(state_ctx={'event_hints': ['a', 'b']}, opts=OPTS)
|
|
43
|
+
self.assertIn('a;b', out['user'])
|
|
44
|
+
def test_protocol_default_present_in_system(self):
|
|
45
|
+
out = et.build_reply(opts={})
|
|
46
|
+
self.assertNotIn('【输出格式', out['system'])
|
|
47
|
+
self.assertNotIn('<bc>', out['system'])
|
|
48
|
+
self.assertNotIn('【输出格式', out['user'])
|
|
49
|
+
|
|
50
|
+
class GuardTest(unittest.TestCase):
|
|
51
|
+
def test_block_mentions_four(self):
|
|
52
|
+
block = eg.guard_block()
|
|
53
|
+
for token in ('编造', '数值', '内疚', '系统'):
|
|
54
|
+
self.assertIn(token, block)
|
|
55
|
+
def test_forbidden_cases(self):
|
|
56
|
+
for text in ('你不理我我就难过', '我这么难受你也不管', '系统说', '模型觉得', '触发了一次', '权限不足'):
|
|
57
|
+
ok, reason = eg.assert_clean(text)
|
|
58
|
+
self.assertFalse(ok, text); self.assertTrue(reason)
|
|
59
|
+
def test_clean_positive(self):
|
|
60
|
+
ok, _reason = eg.assert_clean('今天吃了苹果派,想跟你说一句。')
|
|
61
|
+
self.assertTrue(ok)
|
|
62
|
+
def test_assert_clean_deterministic(self):
|
|
63
|
+
self.assertEqual(eg.assert_clean('系统'), eg.assert_clean('系统'))
|
|
64
|
+
def test_guard_block_deterministic(self):
|
|
65
|
+
self.assertEqual(eg.guard_block(), eg.guard_block())
|
|
66
|
+
def test_no_assignment_in_guard(self):
|
|
67
|
+
source = Path(__file__).resolve().parent.parent.joinpath('expr_guard.py').read_text(encoding='utf-8')
|
|
68
|
+
self.assertIsNone(re.search(r'core\.[A-Za-z_]+\s*=', source))
|
|
69
|
+
|
|
70
|
+
if __name__ == '__main__':
|
|
71
|
+
unittest.main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import expr_tone as et
|
|
4
|
+
|
|
5
|
+
class ToneTest(unittest.TestCase):
|
|
6
|
+
def test_anxiety(self):
|
|
7
|
+
out = et.tone_block('anxiety')
|
|
8
|
+
self.assertAlmostEqual(out['chars_mult'], 0.8); self.assertIn('想问清楚', out['text'])
|
|
9
|
+
def test_sadness(self):
|
|
10
|
+
out = et.tone_block('sadness'); self.assertAlmostEqual(out['chars_mult'], 0.7)
|
|
11
|
+
def test_anger(self):
|
|
12
|
+
self.assertAlmostEqual(et.tone_block('anger')['chars_mult'], 0.9)
|
|
13
|
+
def test_joy(self):
|
|
14
|
+
out = et.tone_block('joy'); self.assertAlmostEqual(out['chars_mult'], 1.2); self.assertIn('分享', out['text'])
|
|
15
|
+
def test_guilt(self):
|
|
16
|
+
self.assertAlmostEqual(et.tone_block('guilt')['chars_mult'], 0.75)
|
|
17
|
+
def test_calm(self):
|
|
18
|
+
self.assertEqual(et.tone_block('calm')['chars_mult'], 1.0)
|
|
19
|
+
def test_suppress_shrinks(self):
|
|
20
|
+
out = et.tone_block('joy', regulation='suppress')
|
|
21
|
+
self.assertTrue(out['suppressed']); self.assertLess(out['chars_mult'], 1.2); self.assertIn('收一点', out['text'])
|
|
22
|
+
def test_reappraise_steady(self):
|
|
23
|
+
self.assertIn('讲道理', et.tone_block('sadness', regulation='reappraise')['text'])
|
|
24
|
+
def test_low_esteem_uncertain(self):
|
|
25
|
+
out = et.tone_block('calm', self_esteem=0.3)
|
|
26
|
+
self.assertIn('也许', out['text']); self.assertEqual(out['text'].count('也许'), 1)
|
|
27
|
+
def test_high_esteem_no_uncertain(self):
|
|
28
|
+
self.assertNotIn('也许', et.tone_block('calm', self_esteem=0.8)['text'])
|
|
29
|
+
def test_deterministic(self):
|
|
30
|
+
self.assertEqual(et.tone_block('anger', 'joy', 0.4, 0.5, 'suppress'), et.tone_block('anger', 'joy', 0.4, 0.5, 'suppress'))
|
|
31
|
+
def test_unknown_falls_back_calm(self):
|
|
32
|
+
self.assertEqual(et.tone_block('bogus')['tendency'], 'calm')
|
|
33
|
+
def test_emotion_fallback(self):
|
|
34
|
+
self.assertEqual(et.tone_block(None, 'sadness')['tendency'], 'sadness')
|
|
35
|
+
def test_no_psych_terms(self):
|
|
36
|
+
for tendency in et.BASE:
|
|
37
|
+
text = et.tone_block(tendency)['text']
|
|
38
|
+
self.assertNotIn('行动倾向', text); self.assertNotIn('焦虑值', text)
|
|
39
|
+
|
|
40
|
+
if __name__ == '__main__':
|
|
41
|
+
unittest.main()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""P2-2 eight extreme-scenario tests (extreme_scenarios.py)."""
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from biocore_model import extreme_scenarios as es
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
9
|
+
SOURCE = (ROOT / "biocore_model" / "extreme_scenarios.py").read_text(
|
|
10
|
+
encoding="utf-8")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _in_bounds(state):
|
|
14
|
+
return all(-1.0 <= state[key] <= 1.0 if key in ("v", "d")
|
|
15
|
+
else 0.0 <= state[key] <= 1.0 for key in es.CHANNELS)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ScenarioMatrixTest(unittest.TestCase):
|
|
19
|
+
def test_all_eight_scenarios_pass(self):
|
|
20
|
+
report = es.run_all()
|
|
21
|
+
self.assertTrue(report["ok"])
|
|
22
|
+
self.assertEqual(len(report["scenarios"]), 8)
|
|
23
|
+
names = [item["scenario"] for item in report["scenarios"]]
|
|
24
|
+
self.assertEqual(sorted(names), sorted(es.SCENARIOS))
|
|
25
|
+
for item in report["scenarios"]:
|
|
26
|
+
self.assertTrue(item["ok"], item)
|
|
27
|
+
self.assertIsInstance(item["details"], dict)
|
|
28
|
+
|
|
29
|
+
def test_declared_scenarios_are_eight(self):
|
|
30
|
+
self.assertEqual(len(es.SCENARIOS), 8)
|
|
31
|
+
self.assertEqual(len(set(es.SCENARIOS)), 8)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LongSilenceTest(unittest.TestCase):
|
|
35
|
+
def test_recovery_toward_baseline(self):
|
|
36
|
+
report = es.long_silence({"v": 0.9, "stress": 0.95, "trust": 0.9},
|
|
37
|
+
gap_hours=24 * 30 * 4)
|
|
38
|
+
self.assertTrue(report["ok"])
|
|
39
|
+
details = report["details"]
|
|
40
|
+
self.assertLess(details["state"]["stress"], 0.95)
|
|
41
|
+
self.assertLess(details["state"]["v"], 0.9)
|
|
42
|
+
self.assertGreater(details["recovered"], 0.9)
|
|
43
|
+
self.assertTrue(_in_bounds(details["state"]))
|
|
44
|
+
|
|
45
|
+
def test_zero_gap_keeps_state(self):
|
|
46
|
+
report = es.long_silence({"v": 0.5, "stress": 0.7}, gap_hours=0)
|
|
47
|
+
self.assertEqual(report["details"]["state"]["v"], 0.5)
|
|
48
|
+
self.assertEqual(report["details"]["state"]["stress"], 0.7)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AbuseTest(unittest.TestCase):
|
|
52
|
+
def test_abuse_stays_bounded(self):
|
|
53
|
+
report = es.repeated_abuse(200)
|
|
54
|
+
details = report["details"]
|
|
55
|
+
self.assertTrue(report["ok"])
|
|
56
|
+
self.assertEqual(details["stress"], 1.0)
|
|
57
|
+
self.assertEqual(details["trust"], 0.0)
|
|
58
|
+
self.assertEqual(details["conflict"], 1.0)
|
|
59
|
+
self.assertGreaterEqual(details["v"], -1.0)
|
|
60
|
+
self.assertTrue(0.0 <= details["refusal"] <= 1.0)
|
|
61
|
+
|
|
62
|
+
def test_zero_hits_is_neutral(self):
|
|
63
|
+
details = es.repeated_abuse(0)["details"]
|
|
64
|
+
self.assertEqual(details["stress"], es.BASELINE["stress"])
|
|
65
|
+
self.assertEqual(details["trust"], es.BASELINE["trust"])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PraiseTest(unittest.TestCase):
|
|
69
|
+
def test_praise_saturates(self):
|
|
70
|
+
report = es.repeated_praise(500)
|
|
71
|
+
details = report["details"]
|
|
72
|
+
self.assertTrue(report["ok"])
|
|
73
|
+
self.assertEqual(details["v"], 1.0)
|
|
74
|
+
self.assertEqual(details["self_esteem"], 1.0)
|
|
75
|
+
self.assertLessEqual(details["trust"], 1.0)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BreakdownTest(unittest.TestCase):
|
|
79
|
+
def test_breakdown_recovery_is_bounded(self):
|
|
80
|
+
report = es.relationship_breakdown({"trust": 0.9, "social": 0.8})
|
|
81
|
+
details = report["details"]
|
|
82
|
+
self.assertTrue(report["ok"])
|
|
83
|
+
self.assertEqual(details["trust"], 0.0)
|
|
84
|
+
self.assertGreater(details["repair_urge"], 0.0)
|
|
85
|
+
self.assertLessEqual(details["repair_urge"], 1.0)
|
|
86
|
+
self.assertGreaterEqual(details["recover_step"], 0.0)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AmnesiaTest(unittest.TestCase):
|
|
90
|
+
def test_missing_state_uses_baseline(self):
|
|
91
|
+
details = es.amnesia({})["details"]
|
|
92
|
+
self.assertEqual(details["state"]["stress"], es.BASELINE["stress"])
|
|
93
|
+
self.assertEqual(details["state"]["trust"], es.BASELINE["trust"])
|
|
94
|
+
self.assertEqual(set(details["needs"]), set(es.NEEDS))
|
|
95
|
+
|
|
96
|
+
def test_dirty_state_is_clamped(self):
|
|
97
|
+
details = es.amnesia({"v": 9.0, "needs": {"belonging": 5.0},
|
|
98
|
+
"junk": 1})["details"]
|
|
99
|
+
self.assertEqual(details["state"]["v"], 1.0)
|
|
100
|
+
self.assertEqual(details["needs"]["belonging"], 1.0)
|
|
101
|
+
self.assertIn("junk", details["dropped"])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConcurrentUsersTest(unittest.TestCase):
|
|
105
|
+
def test_states_do_not_leak(self):
|
|
106
|
+
report = es.concurrent_users({"u1": {"v": 0.5, "trust": 0.9},
|
|
107
|
+
"u2": {"v": -0.5, "trust": 0.1}})
|
|
108
|
+
self.assertTrue(report["ok"])
|
|
109
|
+
self.assertEqual(report["details"]["leaked"], [])
|
|
110
|
+
rows = report["details"]["rows"]
|
|
111
|
+
self.assertEqual(rows["u1"]["v"], 0.5)
|
|
112
|
+
self.assertEqual(rows["u2"]["v"], -0.5)
|
|
113
|
+
self.assertNotEqual(rows["u1"]["trust"], rows["u2"]["trust"])
|
|
114
|
+
|
|
115
|
+
def test_empty_users_is_safe(self):
|
|
116
|
+
self.assertTrue(es.concurrent_users(None)["ok"])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LateNightTest(unittest.TestCase):
|
|
120
|
+
def test_quiet_hours(self):
|
|
121
|
+
for hour in (0, 3, 7):
|
|
122
|
+
report = es.late_night(hour)
|
|
123
|
+
self.assertTrue(report["ok"])
|
|
124
|
+
self.assertFalse(report["details"]["send_allowed"])
|
|
125
|
+
for hour in (8, 14, 23):
|
|
126
|
+
self.assertTrue(es.late_night(hour)["details"]["send_allowed"])
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class CrashRecoveryTest(unittest.TestCase):
|
|
130
|
+
def test_dirty_values_recover(self):
|
|
131
|
+
details = es.crash_recovery({"stress": 0.4, "v": 0.9})["details"]
|
|
132
|
+
restored = details["restored"]
|
|
133
|
+
self.assertEqual(restored["v"], 0.0) # NaN
|
|
134
|
+
self.assertEqual(restored["a"], es.BASELINE["a"]) # inf -> baseline
|
|
135
|
+
self.assertEqual(restored["stress"], es.BASELINE["stress"]) # string
|
|
136
|
+
self.assertEqual(restored["trust"], 1.0) # 99 clamps
|
|
137
|
+
self.assertEqual(restored["energy"], 0.0) # -5 clamps
|
|
138
|
+
self.assertTrue(_in_bounds(restored))
|
|
139
|
+
|
|
140
|
+
def test_module_is_pure(self):
|
|
141
|
+
self.assertNotIn("import main", SOURCE)
|
|
142
|
+
self.assertNotIn("time.", SOURCE)
|
|
143
|
+
self.assertNotIn("open(", SOURCE)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
unittest.main()
|