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,249 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-155 修复流程单测:四阶段、道歉质量、超时回落、关闭、与 boundary 打通。
|
|
3
|
+
|
|
4
|
+
只依赖 biocore_model 的纯函数;不导入 main.py,不新增 LLM 调用。
|
|
5
|
+
"""
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from biocore_model import boundary as bd
|
|
9
|
+
from biocore_model import repair_after_boundary as rp
|
|
10
|
+
|
|
11
|
+
CALM = {"closeness": 0.5, "trust": 0.5, "conflict": 0.0}
|
|
12
|
+
EVENT = bd.judge("你就是个工具", CALM)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def gates(quality=0.0, count=0, severity=None):
|
|
16
|
+
return rp.thresholds(EVENT["severity"] if severity is None else severity,
|
|
17
|
+
quality, count, CALM)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ContractTest(unittest.TestCase):
|
|
21
|
+
def test_result_keys(self):
|
|
22
|
+
out = rp.evaluate(EVENT, relationship=CALM)
|
|
23
|
+
for key in ("enabled", "active", "kind", "severity", "stage",
|
|
24
|
+
"label", "elapsed", "apology_quality", "apology_count",
|
|
25
|
+
"forgiveness", "debt", "hold", "resolved",
|
|
26
|
+
"fallback", "thresholds", "directive", "reason"):
|
|
27
|
+
self.assertIn(key, out)
|
|
28
|
+
|
|
29
|
+
def test_stages_and_labels(self):
|
|
30
|
+
self.assertEqual(rp.STAGES, ("cold", "state", "record", "turn_page"))
|
|
31
|
+
self.assertEqual(set(rp.LABELS), set(rp.STAGES))
|
|
32
|
+
self.assertEqual(rp.LABELS["cold"], "冷处理")
|
|
33
|
+
self.assertEqual(rp.LABELS["turn_page"], "翻页")
|
|
34
|
+
|
|
35
|
+
def test_values_bounded(self):
|
|
36
|
+
for elapsed in (0, 500, 5000, 20000, 999999):
|
|
37
|
+
out = rp.evaluate(EVENT, elapsed_seconds=elapsed, relationship=CALM)
|
|
38
|
+
for key in ("severity", "forgiveness", "debt"):
|
|
39
|
+
self.assertGreaterEqual(out[key], 0.0)
|
|
40
|
+
self.assertLessEqual(out[key], 1.0)
|
|
41
|
+
self.assertGreaterEqual(out["elapsed"], 0.0)
|
|
42
|
+
|
|
43
|
+
def test_gates_are_ordered(self):
|
|
44
|
+
g = gates()
|
|
45
|
+
self.assertLessEqual(g["state"], g["record"])
|
|
46
|
+
self.assertLessEqual(g["record"], g["page"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StageProgressionTest(unittest.TestCase):
|
|
50
|
+
def test_cold_right_after_crossing(self):
|
|
51
|
+
out = rp.evaluate(EVENT, relationship=CALM)
|
|
52
|
+
self.assertTrue(out["active"])
|
|
53
|
+
self.assertEqual(out["stage"], "cold")
|
|
54
|
+
self.assertTrue(out["hold"])
|
|
55
|
+
self.assertFalse(out["resolved"])
|
|
56
|
+
|
|
57
|
+
def test_state_after_cooling(self):
|
|
58
|
+
out = rp.evaluate(EVENT, elapsed_seconds=gates()["state"],
|
|
59
|
+
relationship=CALM)
|
|
60
|
+
self.assertEqual(out["stage"], "state")
|
|
61
|
+
self.assertTrue(out["hold"])
|
|
62
|
+
|
|
63
|
+
def test_record_after_a_while(self):
|
|
64
|
+
out = rp.evaluate(EVENT, elapsed_seconds=gates()["record"],
|
|
65
|
+
relationship=CALM)
|
|
66
|
+
self.assertEqual(out["stage"], "record")
|
|
67
|
+
self.assertFalse(out["hold"])
|
|
68
|
+
|
|
69
|
+
def test_turn_page_by_time(self):
|
|
70
|
+
out = rp.evaluate(EVENT, elapsed_seconds=gates()["page"] + 1,
|
|
71
|
+
relationship=CALM)
|
|
72
|
+
self.assertEqual(out["stage"], "turn_page")
|
|
73
|
+
self.assertFalse(out["fallback"])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ApologyTest(unittest.TestCase):
|
|
77
|
+
def test_apology_speeds_stage_progress(self):
|
|
78
|
+
elapsed = gates(quality=1.0, count=1)["page"] + 10
|
|
79
|
+
slow = rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
80
|
+
apology_quality=0.0, relationship=CALM)
|
|
81
|
+
fast = rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
82
|
+
apology_quality=1.0, relationship=CALM)
|
|
83
|
+
self.assertGreater(rp.STAGES.index(fast["stage"]),
|
|
84
|
+
rp.STAGES.index(slow["stage"]))
|
|
85
|
+
|
|
86
|
+
def test_apology_raises_forgiveness(self):
|
|
87
|
+
elapsed = gates()["record"]
|
|
88
|
+
low = rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
89
|
+
apology_quality=0.0, relationship=CALM)
|
|
90
|
+
high = rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
91
|
+
apology_quality=1.0, relationship=CALM)
|
|
92
|
+
self.assertGreater(high["forgiveness"], low["forgiveness"])
|
|
93
|
+
|
|
94
|
+
def test_good_apology_resolves_page(self):
|
|
95
|
+
out = rp.evaluate(EVENT,
|
|
96
|
+
elapsed_seconds=gates(1.0, 1)["page"] + 10,
|
|
97
|
+
apology_quality=1.0, relationship=CALM)
|
|
98
|
+
self.assertEqual(out["stage"], "turn_page")
|
|
99
|
+
self.assertTrue(out["resolved"])
|
|
100
|
+
self.assertIn("过去", out["reason"])
|
|
101
|
+
|
|
102
|
+
def test_good_apology_lowers_debt(self):
|
|
103
|
+
elapsed = gates()["record"]
|
|
104
|
+
low = rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
105
|
+
apology_quality=0.0, relationship=CALM)
|
|
106
|
+
high = rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
107
|
+
apology_quality=0.9, relationship=CALM)
|
|
108
|
+
self.assertLess(high["debt"], low["debt"])
|
|
109
|
+
|
|
110
|
+
def test_repeated_apology_counts_less(self):
|
|
111
|
+
self.assertLess(rp.apology_quality(0.9, 3),
|
|
112
|
+
rp.apology_quality(0.9, 1))
|
|
113
|
+
self.assertLess(rp.thresholds(0.6, 0.9, 1)["state"],
|
|
114
|
+
rp.thresholds(0.6, 0.9, 3)["state"])
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TimeoutFallbackTest(unittest.TestCase):
|
|
118
|
+
def test_timeout_falls_back_to_page(self):
|
|
119
|
+
out = rp.evaluate(EVENT, elapsed_seconds=600, relationship=CALM,
|
|
120
|
+
flags={"timeout_seconds": 500})
|
|
121
|
+
self.assertEqual(out["stage"], "turn_page")
|
|
122
|
+
self.assertTrue(out["fallback"])
|
|
123
|
+
self.assertFalse(out["resolved"])
|
|
124
|
+
self.assertIn("账", out["reason"])
|
|
125
|
+
|
|
126
|
+
def test_timeout_keeps_debt(self):
|
|
127
|
+
out = rp.evaluate(EVENT, elapsed_seconds=600, relationship=CALM,
|
|
128
|
+
flags={"timeout_seconds": 500})
|
|
129
|
+
self.assertGreater(out["debt"], 0.0)
|
|
130
|
+
|
|
131
|
+
def test_good_apology_survives_timeout(self):
|
|
132
|
+
out = rp.evaluate(EVENT, elapsed_seconds=2500,
|
|
133
|
+
apology_quality=0.95, relationship=CALM,
|
|
134
|
+
flags={"timeout_seconds": 2000})
|
|
135
|
+
self.assertEqual(out["stage"], "turn_page")
|
|
136
|
+
self.assertFalse(out["fallback"])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AdvanceTest(unittest.TestCase):
|
|
140
|
+
def test_stage_never_goes_backwards(self):
|
|
141
|
+
state = rp.start(EVENT, CALM)
|
|
142
|
+
first = rp.advance(state, 400)
|
|
143
|
+
second = rp.advance(first, 3000)
|
|
144
|
+
back = rp.advance(second, -999)
|
|
145
|
+
order = [rp.STAGES.index(row["stage"])
|
|
146
|
+
for row in (state, first, second, back)]
|
|
147
|
+
self.assertEqual(order, sorted(order))
|
|
148
|
+
self.assertEqual(back["stage"], second["stage"])
|
|
149
|
+
|
|
150
|
+
def test_advance_does_not_mutate_input(self):
|
|
151
|
+
state = rp.start(EVENT, CALM)
|
|
152
|
+
before = dict(state)
|
|
153
|
+
rp.advance(state, 5000, apology_quality=0.5)
|
|
154
|
+
self.assertEqual(state, before)
|
|
155
|
+
|
|
156
|
+
def test_advance_records_apology(self):
|
|
157
|
+
state = rp.advance(rp.start(EVENT, CALM), 100,
|
|
158
|
+
apology_quality=0.8)
|
|
159
|
+
self.assertAlmostEqual(state["apology_quality"], 0.8)
|
|
160
|
+
self.assertGreaterEqual(state["apology_count"], 1)
|
|
161
|
+
self.assertGreater(state["forgiveness"], 0.0)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class BoundaryBridgeTest(unittest.TestCase):
|
|
165
|
+
def test_crossed_boundary_opens_cold_flow(self):
|
|
166
|
+
out = rp.from_boundary(EVENT, CALM)
|
|
167
|
+
self.assertTrue(out["active"])
|
|
168
|
+
self.assertEqual(out["stage"], "cold")
|
|
169
|
+
self.assertEqual(out["kind"], EVENT["kind"])
|
|
170
|
+
self.assertEqual(out["severity"], EVENT["severity"])
|
|
171
|
+
|
|
172
|
+
def test_clean_boundary_does_not_open_flow(self):
|
|
173
|
+
out = rp.from_boundary(bd.judge("晚安", CALM), CALM)
|
|
174
|
+
self.assertFalse(out["active"])
|
|
175
|
+
self.assertFalse(out["hold"])
|
|
176
|
+
|
|
177
|
+
def test_accepts_plain_event_dict(self):
|
|
178
|
+
out = rp.from_boundary({"crossed": True, "kind": "lie",
|
|
179
|
+
"severity": 0.8})
|
|
180
|
+
self.assertTrue(out["active"])
|
|
181
|
+
self.assertEqual(out["kind"], "lie")
|
|
182
|
+
self.assertAlmostEqual(out["severity"], 0.8)
|
|
183
|
+
|
|
184
|
+
def test_handoff_field_opens_flow(self):
|
|
185
|
+
out = rp.from_boundary({"handoff": {"to": "repair_after_boundary"},
|
|
186
|
+
"kind": "contempt", "severity": 0.7})
|
|
187
|
+
self.assertTrue(out["active"])
|
|
188
|
+
self.assertEqual(out["kind"], "contempt")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class SwitchAndPolicyTest(unittest.TestCase):
|
|
192
|
+
def test_disabled_returns_old_behavior(self):
|
|
193
|
+
out = rp.evaluate(EVENT, relationship=CALM,
|
|
194
|
+
flags={"enabled": False})
|
|
195
|
+
self.assertFalse(out["enabled"])
|
|
196
|
+
self.assertFalse(out["active"])
|
|
197
|
+
self.assertFalse(out["hold"])
|
|
198
|
+
self.assertIn("关闭", out["reason"])
|
|
199
|
+
|
|
200
|
+
def test_reply_policy_cold_suppresses_warmth(self):
|
|
201
|
+
out = rp.reply_policy(rp.evaluate(EVENT, relationship=CALM))
|
|
202
|
+
self.assertTrue(out["suppress_affection"])
|
|
203
|
+
self.assertLessEqual(out["max_chars"], 25)
|
|
204
|
+
self.assertEqual(out["mode"], "cold_short")
|
|
205
|
+
|
|
206
|
+
def test_reply_policy_page_is_normal(self):
|
|
207
|
+
state = rp.evaluate(EVENT, elapsed_seconds=gates()["page"] + 1,
|
|
208
|
+
relationship=CALM)
|
|
209
|
+
out = rp.reply_policy(state)
|
|
210
|
+
self.assertFalse(out["suppress_affection"])
|
|
211
|
+
self.assertGreaterEqual(out["max_chars"], 100)
|
|
212
|
+
|
|
213
|
+
def test_directives_bounded_every_stage(self):
|
|
214
|
+
for elapsed in (0, gates()["state"], gates()["record"],
|
|
215
|
+
gates()["page"] + 1):
|
|
216
|
+
out = rp.directives(rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
217
|
+
relationship=CALM))
|
|
218
|
+
self.assertGreaterEqual(out["length_scale"], 0.3)
|
|
219
|
+
self.assertLessEqual(out["length_scale"], 1.1)
|
|
220
|
+
for key in ("warmth", "initiative"):
|
|
221
|
+
self.assertGreaterEqual(out[key], 0.0)
|
|
222
|
+
self.assertLessEqual(out[key], 1.0)
|
|
223
|
+
self.assertTrue(out["tone"])
|
|
224
|
+
|
|
225
|
+
def test_inactive_result_has_reason(self):
|
|
226
|
+
out = rp.evaluate(None)
|
|
227
|
+
self.assertFalse(out["active"])
|
|
228
|
+
self.assertTrue(out["reason"])
|
|
229
|
+
|
|
230
|
+
def test_negative_elapsed_is_clamped(self):
|
|
231
|
+
out = rp.evaluate(EVENT, elapsed_seconds=-500, relationship=CALM)
|
|
232
|
+
self.assertEqual(out["elapsed"], 0.0)
|
|
233
|
+
self.assertEqual(out["stage"], "cold")
|
|
234
|
+
|
|
235
|
+
def test_reason_present_for_all_stages(self):
|
|
236
|
+
for elapsed in (0, gates()["state"], gates()["record"],
|
|
237
|
+
gates()["page"] + 1):
|
|
238
|
+
out = rp.evaluate(EVENT, elapsed_seconds=elapsed,
|
|
239
|
+
relationship=CALM)
|
|
240
|
+
self.assertTrue(out["reason"])
|
|
241
|
+
|
|
242
|
+
def test_evaluate_accepts_non_dict(self):
|
|
243
|
+
for value in (None, "x", 3, []):
|
|
244
|
+
out = rp.evaluate(value)
|
|
245
|
+
self.assertFalse(out["active"])
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
unittest.main()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import reply_limit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeContext:
|
|
7
|
+
def __init__(self, config, fail=False):
|
|
8
|
+
self._config = config
|
|
9
|
+
self.fail = fail
|
|
10
|
+
self.saved = 0
|
|
11
|
+
def get_config(self):
|
|
12
|
+
return self._config
|
|
13
|
+
async def save_config_async(self, config):
|
|
14
|
+
self.saved += 1
|
|
15
|
+
if self.fail:
|
|
16
|
+
raise RuntimeError("save failed")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FakePlugin:
|
|
20
|
+
def __init__(self, config, fail=False):
|
|
21
|
+
self.context = FakeContext(config, fail)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cfg(max_tokens=200, enable=True):
|
|
25
|
+
return {"provider": {"default": {"id": "a", "name": "p1", "max_tokens": max_tokens, "enable": enable}}}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReplyLimitTest(unittest.IsolatedAsyncioTestCase):
|
|
29
|
+
async def test_read_default_provider(self):
|
|
30
|
+
data = reply_limit.read(FakePlugin(cfg(320)))
|
|
31
|
+
self.assertEqual(data["max_tokens"], 320)
|
|
32
|
+
self.assertEqual(data["provider_id"], "a")
|
|
33
|
+
self.assertEqual(data["source"], "astrbot")
|
|
34
|
+
|
|
35
|
+
async def test_apply_ok(self):
|
|
36
|
+
plugin = FakePlugin(cfg(200))
|
|
37
|
+
result = await reply_limit.apply(plugin, {"max_tokens": 512})
|
|
38
|
+
self.assertTrue(result["ok"])
|
|
39
|
+
self.assertEqual(result["applied"]["max_tokens"], 512)
|
|
40
|
+
# restart_required depends on whether an active AstrBot config file exists
|
|
41
|
+
self.assertEqual(plugin.context.saved, 1)
|
|
42
|
+
|
|
43
|
+
async def test_apply_writes_file_when_active_config_found(self):
|
|
44
|
+
import json, tempfile
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from biocore_model import reply_limit as rl
|
|
47
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
48
|
+
path = Path(tmp) / "cmd_config.json"
|
|
49
|
+
path.write_text(json.dumps({"provider": [{"id": "a", "max_tokens": 200}]}), encoding="utf-8")
|
|
50
|
+
old = rl.find_active_config
|
|
51
|
+
rl.find_active_config = lambda plugin=None: path
|
|
52
|
+
try:
|
|
53
|
+
result = await rl.apply(FakePlugin(cfg(200)), {"max_tokens": 800})
|
|
54
|
+
finally:
|
|
55
|
+
rl.find_active_config = old
|
|
56
|
+
self.assertEqual(result["file"], str(path))
|
|
57
|
+
self.assertFalse(result["restart_required"])
|
|
58
|
+
self.assertEqual(json.loads(path.read_text(encoding="utf-8"))["provider"][0]["max_tokens"], 800)
|
|
59
|
+
|
|
60
|
+
async def test_apply_rejects_low(self):
|
|
61
|
+
with self.assertRaises(reply_limit.ReplyLimitError) as caught:
|
|
62
|
+
await reply_limit.apply(FakePlugin(cfg()), {"max_tokens": 10})
|
|
63
|
+
self.assertEqual(caught.exception.status, 400)
|
|
64
|
+
|
|
65
|
+
async def test_apply_rejects_high(self):
|
|
66
|
+
with self.assertRaises(reply_limit.ReplyLimitError):
|
|
67
|
+
await reply_limit.apply(FakePlugin(cfg()), {"max_tokens": 5000})
|
|
68
|
+
|
|
69
|
+
async def test_save_failure_sets_restart_required(self):
|
|
70
|
+
result = await reply_limit.apply(FakePlugin(cfg(), fail=True), {"max_tokens": 300})
|
|
71
|
+
self.assertTrue(result["restart_required"])
|
|
72
|
+
self.assertTrue(result["ok"])
|
|
73
|
+
|
|
74
|
+
async def test_missing_config_no_crash(self):
|
|
75
|
+
data = reply_limit.read(FakePlugin({}))
|
|
76
|
+
self.assertEqual(data["max_tokens"], 800)
|
|
77
|
+
result = await reply_limit.apply(FakePlugin({}), {"max_tokens": 128})
|
|
78
|
+
self.assertTrue(result["restart_required"])
|
|
79
|
+
|
|
80
|
+
async def test_bad_type_rejected(self):
|
|
81
|
+
with self.assertRaises(reply_limit.ReplyLimitError):
|
|
82
|
+
await reply_limit.apply(FakePlugin(cfg()), {"max_tokens": "abc"})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
unittest.main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from biocore_model import reply_limit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PersistTest(unittest.TestCase):
|
|
10
|
+
def _config(self, tmp, tokens=200):
|
|
11
|
+
path = Path(tmp) / "cmd_config.json"
|
|
12
|
+
path.write_text(json.dumps({"provider": [{"id": "a", "max_tokens": tokens}]}), encoding="utf-8")
|
|
13
|
+
return path
|
|
14
|
+
|
|
15
|
+
def test_persist_updates_file(self):
|
|
16
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
17
|
+
path = self._config(tmp)
|
|
18
|
+
self.assertTrue(reply_limit.persist_max_tokens(path, 800))
|
|
19
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
20
|
+
self.assertEqual(data["provider"][0]["max_tokens"], 800)
|
|
21
|
+
|
|
22
|
+
def test_backup_created(self):
|
|
23
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
24
|
+
path = self._config(tmp)
|
|
25
|
+
reply_limit.persist_max_tokens(path, 512)
|
|
26
|
+
backups = list(Path(tmp).glob("cmd_config.json.bak_replylimit_*"))
|
|
27
|
+
self.assertEqual(len(backups), 1)
|
|
28
|
+
|
|
29
|
+
def test_backup_rotation_keeps_three(self):
|
|
30
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
31
|
+
path = self._config(tmp)
|
|
32
|
+
for index in range(5):
|
|
33
|
+
reply_limit.persist_max_tokens(path, 300 + index)
|
|
34
|
+
backups = list(Path(tmp).glob("cmd_config.json.bak_replylimit_*"))
|
|
35
|
+
self.assertLessEqual(len(backups), 3)
|
|
36
|
+
|
|
37
|
+
def test_missing_provider_fails(self):
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
39
|
+
path = Path(tmp) / "cmd_config.json"
|
|
40
|
+
path.write_text(json.dumps({"provider": []}), encoding="utf-8")
|
|
41
|
+
self.assertFalse(reply_limit.persist_max_tokens(path, 800))
|
|
42
|
+
|
|
43
|
+
def test_find_active_config_none(self):
|
|
44
|
+
self.assertTrue(reply_limit.find_active_config(None) is None or reply_limit.find_active_config(None).is_file())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
unittest.main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import reply_split
|
|
4
|
+
|
|
5
|
+
CFG = {'enabled': True, 'mode': 'punct', 'max_chars': 400, 'min_chars': 12}
|
|
6
|
+
|
|
7
|
+
class ReplySplitTest(unittest.TestCase):
|
|
8
|
+
def test_punct_keeps_punctuation(self):
|
|
9
|
+
parts = reply_split.split_reply('今天真的挺不错的。你那边怎么样呢?我这边一切顺利。', dict(CFG, min_chars=1))
|
|
10
|
+
self.assertGreaterEqual(len(parts), 2)
|
|
11
|
+
self.assertTrue(parts[0].endswith('。')); self.assertTrue(parts[1].endswith('?'))
|
|
12
|
+
|
|
13
|
+
def test_newline_mode(self):
|
|
14
|
+
cfg = {'enabled': True, 'mode': 'newline', 'min_chars': 1}
|
|
15
|
+
self.assertEqual(reply_split.split_reply('一行\n二行\n三行', cfg), ['一行', '二行', '三行'])
|
|
16
|
+
|
|
17
|
+
def test_words_mode(self):
|
|
18
|
+
cfg = {'enabled': True, 'mode': 'words', 'words': ['好的', '结束'], 'min_chars': 1}
|
|
19
|
+
parts = reply_split.split_reply('好的开始结束', cfg)
|
|
20
|
+
self.assertEqual(len(parts), 2)
|
|
21
|
+
|
|
22
|
+
def test_short_piece_merged(self):
|
|
23
|
+
parts = reply_split.split_reply('这是第一句比较长的话。嗯。后面还有一句比较长的话。', CFG)
|
|
24
|
+
self.assertFalse(any(len(p) < 12 for p in parts[:-1]))
|
|
25
|
+
|
|
26
|
+
def test_over_max_chars_not_split(self):
|
|
27
|
+
cfg = dict(CFG, max_chars=10)
|
|
28
|
+
text = '很长很长很长很长很长。很长很长很长很长很长。'
|
|
29
|
+
self.assertEqual(reply_split.split_reply(text, cfg), [text])
|
|
30
|
+
|
|
31
|
+
def test_at_most_six_segments(self):
|
|
32
|
+
text = ''.join('第%d句比较长的话说三遍。' % i for i in range(10))
|
|
33
|
+
self.assertLessEqual(len(reply_split.split_reply(text, CFG)), 6)
|
|
34
|
+
|
|
35
|
+
def test_disabled_returns_single(self):
|
|
36
|
+
text = '一句。两句。'
|
|
37
|
+
self.assertEqual(reply_split.split_reply(text, dict(CFG, enabled=False)), [text])
|
|
38
|
+
|
|
39
|
+
def test_empty_text(self):
|
|
40
|
+
self.assertEqual(reply_split.split_reply('', CFG), [])
|
|
41
|
+
|
|
42
|
+
def test_single_sentence_not_split(self):
|
|
43
|
+
self.assertEqual(reply_split.split_reply('只有一句话。', CFG), ['只有一句话。'])
|
|
44
|
+
|
|
45
|
+
def test_plain_text_without_punctuation(self):
|
|
46
|
+
self.assertEqual(reply_split.split_reply('没有标点的句子', CFG), ['没有标点的句子'])
|
|
47
|
+
|
|
48
|
+
def test_invalid_mode_falls_back(self):
|
|
49
|
+
self.assertIn(reply_split.split_reply('甲。乙。丙。', {'enabled': True, 'mode': 'bogus', 'min_chars': 1}), [['甲。乙。丙。'], ['甲。', '乙。', '丙。']])
|
|
50
|
+
|
|
51
|
+
if __name__ == '__main__':
|
|
52
|
+
unittest.main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import re, unittest
|
|
3
|
+
from biocore_model import reply_split
|
|
4
|
+
|
|
5
|
+
BASE = {'enable': True, 'words_count_threshold': 150, 'split_mode': 'words',
|
|
6
|
+
'split_words': ['。', '?', '!'], 'content_cleanup_rule': ''}
|
|
7
|
+
|
|
8
|
+
def astrbot_reference(text, cfg):
|
|
9
|
+
if not cfg.get('enable'): return [text]
|
|
10
|
+
if len(text) > cfg.get('words_count_threshold', 150): return [text]
|
|
11
|
+
if cfg.get('split_mode') == 'words':
|
|
12
|
+
out, buf = [], ''
|
|
13
|
+
for ch in text:
|
|
14
|
+
buf += ch
|
|
15
|
+
if any(buf.endswith(w) for w in cfg.get('split_words') or []):
|
|
16
|
+
out.append(buf); buf = ''
|
|
17
|
+
if buf.strip(): out.append(buf)
|
|
18
|
+
else:
|
|
19
|
+
out = re.findall(cfg.get('regex') or '[^\\n]+', text, re.DOTALL | re.MULTILINE)
|
|
20
|
+
cleanup = cfg.get('content_cleanup_rule') or ''
|
|
21
|
+
out = [re.sub(cleanup, '', p) if cleanup else p for p in out]
|
|
22
|
+
out = [p for p in out if str(p).strip()]
|
|
23
|
+
return out if len(out) >= 2 else [text]
|
|
24
|
+
|
|
25
|
+
class AstrbotModeTest(unittest.TestCase):
|
|
26
|
+
def test_default_mode_is_astrbot(self):
|
|
27
|
+
self.assertEqual(reply_split.DEFAULTS['mode'], 'astrbot')
|
|
28
|
+
def test_disabled_returns_single(self):
|
|
29
|
+
cfg = dict(BASE, enable=False)
|
|
30
|
+
self.assertEqual(reply_split.split_reply('一句。两句。', {'enabled': True, 'astrbot': cfg, 'inject_newlines': False}), ['一句。两句。'])
|
|
31
|
+
def test_threshold_exceeded_not_split(self):
|
|
32
|
+
cfg = dict(BASE, words_count_threshold=5)
|
|
33
|
+
self.assertEqual(reply_split.split_reply('一句。两句。', {'enabled': True, 'astrbot': cfg, 'inject_newlines': False}), ['一句。两句。'])
|
|
34
|
+
def test_words_matches_astrbot_reference(self):
|
|
35
|
+
text = '第一句话。第二句话?第三句话!'
|
|
36
|
+
cfg = dict(BASE)
|
|
37
|
+
ours = reply_split.split_reply(text, {'enabled': True, 'astrbot': cfg, 'inject_newlines': False})
|
|
38
|
+
self.assertEqual(ours, astrbot_reference(text, cfg))
|
|
39
|
+
def test_regex_mode_newline(self):
|
|
40
|
+
text = '第一行\n第二行\n第三行'
|
|
41
|
+
cfg = {'enable': True, 'words_count_threshold': 150, 'split_mode': 'regex', 'regex': '[^\\n]+', 'content_cleanup_rule': ''}
|
|
42
|
+
self.assertEqual(reply_split.split_reply(text, {'enabled': True, 'astrbot': cfg, 'inject_newlines': False}), ['第一行', '第二行', '第三行'])
|
|
43
|
+
def test_regex_matches_astrbot_reference(self):
|
|
44
|
+
text = '甲\n乙\n丙'
|
|
45
|
+
cfg = {'enable': True, 'words_count_threshold': 150, 'split_mode': 'regex', 'regex': '[^\\n]+', 'content_cleanup_rule': ''}
|
|
46
|
+
self.assertEqual(reply_split.split_reply(text, {'enabled': True, 'astrbot': cfg, 'inject_newlines': False}), astrbot_reference(text, cfg))
|
|
47
|
+
def test_cleanup_rule_removes_content(self):
|
|
48
|
+
text = '第一句[cut]话。第二句[cut]话?'
|
|
49
|
+
cfg = {'enable': True, 'words_count_threshold': 150, 'split_mode': 'words', 'split_words': ['。', '?'], 'content_cleanup_rule': r'\[cut\]'}
|
|
50
|
+
parts = reply_split.split_reply(text, {'enabled': True, 'astrbot': cfg, 'inject_newlines': False})
|
|
51
|
+
self.assertEqual(parts, ['第一句话。', '第二句话?'])
|
|
52
|
+
def test_below_two_segments_returns_whole(self):
|
|
53
|
+
cfg = dict(BASE)
|
|
54
|
+
self.assertEqual(reply_split.split_reply('只有一句。', {'enabled': True, 'astrbot': cfg, 'inject_newlines': False}), ['只有一句。'])
|
|
55
|
+
def test_empty_text(self):
|
|
56
|
+
self.assertEqual(reply_split.split_reply('', {'enabled': True, 'astrbot': BASE, 'inject_newlines': False}), [])
|
|
57
|
+
def test_effective_shape(self):
|
|
58
|
+
eff = reply_split.effective(None)
|
|
59
|
+
for key in ('enable', 'threshold', 'mode', 'regex_or_words'):
|
|
60
|
+
self.assertIn(key, eff)
|
|
61
|
+
def test_read_astrbot_settings_defaults_no_crash(self):
|
|
62
|
+
settings = reply_split.read_astrbot_settings(None)
|
|
63
|
+
self.assertIn('split_mode', settings)
|
|
64
|
+
|
|
65
|
+
if __name__ == '__main__':
|
|
66
|
+
unittest.main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import re, unittest
|
|
3
|
+
from biocore_model import reply_split
|
|
4
|
+
|
|
5
|
+
ENABLED = {'enable': True, 'words_count_threshold': 150, 'split_mode': 'regex', 'regex': '[^\\n]+'}
|
|
6
|
+
|
|
7
|
+
class InjectNewlinesTest(unittest.TestCase):
|
|
8
|
+
def _inject(self, text, settings=None):
|
|
9
|
+
return reply_split.inject_sentence_newlines(text, settings or ENABLED)
|
|
10
|
+
|
|
11
|
+
def test_multi_sentence_one_per_line(self):
|
|
12
|
+
self.assertEqual(self._inject('第一句。第二句?第三句!'), '第一句。\n第二句?\n第三句!')
|
|
13
|
+
|
|
14
|
+
def test_punctuation_kept(self):
|
|
15
|
+
out = self._inject('甲。乙!')
|
|
16
|
+
self.assertTrue(out.endswith('甲。\n乙!'))
|
|
17
|
+
|
|
18
|
+
def test_existing_newline_not_duplicated(self):
|
|
19
|
+
self.assertEqual(self._inject('第一句。\n第二句。'), '第一句。\n第二句。')
|
|
20
|
+
|
|
21
|
+
def test_no_punctuation_unchanged(self):
|
|
22
|
+
self.assertEqual(self._inject('没有标点的句子'), '没有标点的句子')
|
|
23
|
+
|
|
24
|
+
def test_over_threshold_no_injection(self):
|
|
25
|
+
text = '一句。两句。'
|
|
26
|
+
self.assertEqual(self._inject(text, dict(ENABLED, words_count_threshold=5)), text)
|
|
27
|
+
|
|
28
|
+
def test_disabled_no_injection(self):
|
|
29
|
+
text = '一句。两句。'
|
|
30
|
+
self.assertEqual(self._inject(text, dict(ENABLED, enable=False)), text)
|
|
31
|
+
|
|
32
|
+
def test_single_sentence_unchanged(self):
|
|
33
|
+
self.assertEqual(self._inject('只有一句。'), '只有一句。')
|
|
34
|
+
|
|
35
|
+
def test_blank_lines_collapsed(self):
|
|
36
|
+
self.assertEqual(self._inject('甲。\n\n乙。'), '甲。\n乙。')
|
|
37
|
+
|
|
38
|
+
def test_astrbot_regex_splits_into_sentences(self):
|
|
39
|
+
text = self._inject('第一句。第二句?第三句!')
|
|
40
|
+
self.assertEqual(len(re.findall(r'[^\n]+', text, re.DOTALL | re.MULTILINE)), 3)
|
|
41
|
+
|
|
42
|
+
def test_split_reply_returns_single_text_with_newlines(self):
|
|
43
|
+
cfg = {'enabled': True, 'astrbot': ENABLED, 'inject_newlines': True}
|
|
44
|
+
parts = reply_split.split_reply('甲。乙。', cfg)
|
|
45
|
+
self.assertEqual(parts, ['甲。\n乙。'])
|
|
46
|
+
|
|
47
|
+
def test_status_has_inject_flag(self):
|
|
48
|
+
self.assertTrue(reply_split.status.__doc__ is not None)
|
|
49
|
+
class P: config = {}
|
|
50
|
+
self.assertTrue(reply_split.status(P())['inject_newlines'])
|
|
51
|
+
|
|
52
|
+
if __name__ == '__main__':
|
|
53
|
+
unittest.main()
|