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,90 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Proactive message -> AstrBot conversation history + 3-item [BC] context."""
|
|
3
|
+
import ast
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import types
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
MAIN = Path(__file__).resolve().parents[2] / "main.py"
|
|
11
|
+
SOURCE = MAIN.read_text(encoding="utf-8")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load(name):
|
|
15
|
+
tree = ast.parse(SOURCE)
|
|
16
|
+
cls = next(n for n in tree.body
|
|
17
|
+
if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
|
|
18
|
+
node = next(f for f in cls.body
|
|
19
|
+
if isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
20
|
+
and f.name == name)
|
|
21
|
+
kind = (ast.AsyncFunctionDef if isinstance(node, ast.AsyncFunctionDef)
|
|
22
|
+
else ast.FunctionDef)
|
|
23
|
+
clone = kind(name=node.name, args=node.args, body=node.body,
|
|
24
|
+
decorator_list=[], returns=node.returns, type_comment=None,
|
|
25
|
+
lineno=node.lineno, col_offset=node.col_offset,
|
|
26
|
+
end_lineno=node.end_lineno, end_col_offset=node.end_col_offset)
|
|
27
|
+
ns = {"json": json, "logger": types.SimpleNamespace(
|
|
28
|
+
info=lambda *a, **k: None, warning=lambda *a, **k: None)}
|
|
29
|
+
exec(compile(ast.Module(body=[clone], type_ignores=[]), "<h>", "exec"), ns)
|
|
30
|
+
return ns[name]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FakeCM:
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.history = [{"role": "user", "content": "old"}]
|
|
36
|
+
self.updated = None
|
|
37
|
+
|
|
38
|
+
async def get_curr_conversation_id(self, umo):
|
|
39
|
+
return "cid-1"
|
|
40
|
+
|
|
41
|
+
async def get_conversation(self, umo, cid):
|
|
42
|
+
return types.SimpleNamespace(history=json.dumps(self.history))
|
|
43
|
+
|
|
44
|
+
async def update_conversation(self, umo, cid, history=None, **kwargs):
|
|
45
|
+
self.updated = list(history or [])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HistoryTest(unittest.IsolatedAsyncioTestCase):
|
|
49
|
+
async def test_appends_assistant_message(self):
|
|
50
|
+
cm = FakeCM()
|
|
51
|
+
plugin = types.SimpleNamespace(
|
|
52
|
+
context=types.SimpleNamespace(conversation_manager=cm))
|
|
53
|
+
fn = types.MethodType(_load("_persist_proactive_history"), plugin)
|
|
54
|
+
ok = await fn("umo-1", "哟,老板!今晚好")
|
|
55
|
+
self.assertTrue(ok)
|
|
56
|
+
self.assertEqual(cm.updated[-1]["role"], "assistant")
|
|
57
|
+
self.assertEqual(cm.updated[-1]["content"], "哟,老板!今晚好")
|
|
58
|
+
self.assertEqual(cm.updated[0]["content"], "old")
|
|
59
|
+
|
|
60
|
+
async def test_no_curr_conversation_is_safe(self):
|
|
61
|
+
class NoConv:
|
|
62
|
+
async def get_curr_conversation_id(self, umo):
|
|
63
|
+
return None
|
|
64
|
+
plugin = types.SimpleNamespace(
|
|
65
|
+
context=types.SimpleNamespace(conversation_manager=NoConv()))
|
|
66
|
+
fn = types.MethodType(_load("_persist_proactive_history"), plugin)
|
|
67
|
+
self.assertFalse(await fn("umo", "x"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ContextTest(unittest.TestCase):
|
|
71
|
+
def test_three_items_in_context(self):
|
|
72
|
+
plugin = types.SimpleNamespace(
|
|
73
|
+
_recent_proactive=["甲", "乙", "丙", "丁"],
|
|
74
|
+
_load_recent_proactive=lambda: ["甲", "乙", "丙", "丁"])
|
|
75
|
+
fn = _load("_recent_proactive_context")
|
|
76
|
+
fn = types.MethodType(fn, plugin)
|
|
77
|
+
text = fn()
|
|
78
|
+
self.assertIn("乙", text)
|
|
79
|
+
self.assertIn("丙", text)
|
|
80
|
+
self.assertIn("丁", text)
|
|
81
|
+
self.assertNotIn("甲", text)
|
|
82
|
+
|
|
83
|
+
def test_ctx_log_and_bc_fallback_wired(self):
|
|
84
|
+
self.assertIn("[biocore] ctx contexts=%s prompt_len=%s sys_len=%s", SOURCE)
|
|
85
|
+
self.assertIn("_recent_ctx = self._recent_proactive_context()", SOURCE)
|
|
86
|
+
self.assertIn("await self._persist_proactive_history(umo, final)", SOURCE)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
unittest.main()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""B/C: proactive opening de-dup + self-memory (events/prompt/persistence)."""
|
|
3
|
+
import ast
|
|
4
|
+
import tempfile
|
|
5
|
+
import types
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from biocore_model import expr_template as et
|
|
10
|
+
from biocore_model import runtime as rt
|
|
11
|
+
from biocore_model import ui_settings
|
|
12
|
+
|
|
13
|
+
MAIN = Path(__file__).resolve().parents[2] / "main.py"
|
|
14
|
+
SOURCE = MAIN.read_text(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_methods(names):
|
|
18
|
+
tree = ast.parse(SOURCE)
|
|
19
|
+
cls = next(n for n in tree.body
|
|
20
|
+
if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
|
|
21
|
+
ns = {"time": __import__("time")}
|
|
22
|
+
fns = {}
|
|
23
|
+
for node in cls.body:
|
|
24
|
+
if isinstance(node, ast.FunctionDef) and node.name in names:
|
|
25
|
+
clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
|
|
26
|
+
decorator_list=[], returns=node.returns,
|
|
27
|
+
type_comment=None, lineno=node.lineno,
|
|
28
|
+
col_offset=node.col_offset,
|
|
29
|
+
end_lineno=node.end_lineno,
|
|
30
|
+
end_col_offset=node.end_col_offset)
|
|
31
|
+
exec(compile(ast.Module(body=[clone], type_ignores=[]), "<pm>", "exec"), ns)
|
|
32
|
+
fns[node.name] = ns[node.name]
|
|
33
|
+
return fns
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
FNS = _load_methods(("_load_recent_proactive", "_recent_proactive_prompt",
|
|
37
|
+
"_recent_proactive_context", "_opening_key",
|
|
38
|
+
"_opening_repeats", "_rewrite_proactive_opening",
|
|
39
|
+
"_record_proactive_sent"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FakePlugin:
|
|
43
|
+
def __init__(self, tmp):
|
|
44
|
+
self.data_dir = tmp
|
|
45
|
+
self._recent_proactive = []
|
|
46
|
+
self._recent_proactive_loaded = False
|
|
47
|
+
self.events = []
|
|
48
|
+
for name in FNS:
|
|
49
|
+
setattr(self, name, types.MethodType(FNS[name], self))
|
|
50
|
+
|
|
51
|
+
def _append_event_archive(self, record):
|
|
52
|
+
self.events.append(dict(record))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OpeningTest(unittest.TestCase):
|
|
56
|
+
def test_opening_key_and_repeat(self):
|
|
57
|
+
plugin = FakePlugin(tempfile.mkdtemp())
|
|
58
|
+
plugin._recent_proactive = ["哟,老板!今天也来啦"]
|
|
59
|
+
self.assertTrue(plugin._opening_repeats("哟,老板!今天又来了"))
|
|
60
|
+
self.assertFalse(plugin._opening_repeats("晚上好,今天忙吗"))
|
|
61
|
+
|
|
62
|
+
def test_rewrite_once_changes_opening(self):
|
|
63
|
+
plugin = FakePlugin(tempfile.mkdtemp())
|
|
64
|
+
plugin._recent_proactive = ["哟,老板!今天也来啦"]
|
|
65
|
+
out = plugin._rewrite_proactive_opening("哟,老板!今天又来了")
|
|
66
|
+
self.assertNotEqual(plugin._opening_key(out),
|
|
67
|
+
plugin._opening_key("哟,老板!今天又来了"))
|
|
68
|
+
|
|
69
|
+
def test_recent_prompt(self):
|
|
70
|
+
plugin = FakePlugin(tempfile.mkdtemp())
|
|
71
|
+
plugin._recent_proactive = ["第一句", "第二句", "第三句", "第四句"]
|
|
72
|
+
text = plugin._recent_proactive_prompt()
|
|
73
|
+
self.assertIn("你最近说过", text)
|
|
74
|
+
self.assertIn("第二句", text)
|
|
75
|
+
self.assertIn("不要重复以上任何一句的前 6 个字", text)
|
|
76
|
+
|
|
77
|
+
def test_no_recent_prompt(self):
|
|
78
|
+
plugin = FakePlugin(tempfile.mkdtemp())
|
|
79
|
+
self.assertEqual(plugin._recent_proactive_prompt(), "")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class RecordTest(unittest.TestCase):
|
|
83
|
+
def test_record_event_memory_and_persistence(self):
|
|
84
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
85
|
+
plugin = FakePlugin(tmp)
|
|
86
|
+
plugin._record_proactive_sent("umo-1", "哟,老板!今晚好",
|
|
87
|
+
reason="lonely")
|
|
88
|
+
self.assertEqual(plugin.events[0]["type"], "proactive_sent")
|
|
89
|
+
self.assertEqual(plugin.events[0]["text"], "哟,老板!今晚好")
|
|
90
|
+
self.assertEqual(plugin.events[0]["umo"], "umo-1")
|
|
91
|
+
self.assertEqual(len(plugin._recent_proactive), 1)
|
|
92
|
+
saved = ((ui_settings.load(plugin).get("autonomy") or {})
|
|
93
|
+
.get("recent_proactive") or [])
|
|
94
|
+
self.assertEqual(saved, ["哟,老板!今晚好"])
|
|
95
|
+
|
|
96
|
+
def test_recent_capped_five(self):
|
|
97
|
+
plugin = FakePlugin(tempfile.gettempdir())
|
|
98
|
+
for i in range(7):
|
|
99
|
+
plugin._record_proactive_sent("u", "第%d条主动消息" % i)
|
|
100
|
+
self.assertEqual(len(plugin._recent_proactive), 5)
|
|
101
|
+
self.assertEqual(plugin._recent_proactive[-1], "第6条主动消息")
|
|
102
|
+
|
|
103
|
+
def test_context_last_two(self):
|
|
104
|
+
plugin = FakePlugin(tempfile.gettempdir())
|
|
105
|
+
plugin._recent_proactive = ["甲", "乙", "丙", "丁"]
|
|
106
|
+
ctx = plugin._recent_proactive_context()
|
|
107
|
+
self.assertIn("我刚说过", ctx)
|
|
108
|
+
self.assertIn("乙", ctx)
|
|
109
|
+
self.assertIn("丙", ctx)
|
|
110
|
+
self.assertIn("丁", ctx)
|
|
111
|
+
self.assertNotIn("甲", ctx)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PromptInjectionTest(unittest.TestCase):
|
|
115
|
+
def test_expr_template_recent_block(self):
|
|
116
|
+
out = et.build_reply(state_ctx={"recent_proactive": "我刚说过:吃了没"})
|
|
117
|
+
self.assertIn("我刚说过:吃了没", out["user"])
|
|
118
|
+
|
|
119
|
+
def test_runtime_enrich_sets_recent(self):
|
|
120
|
+
plugin = types.SimpleNamespace(_recent_proactive=["甲", "乙", "丙"])
|
|
121
|
+
core = types.SimpleNamespace(snapshot=lambda: {"v": 0.0})
|
|
122
|
+
ctx = rt.enrich_ctx({"plugin": plugin, "core": core,
|
|
123
|
+
"user": {}, "meta": {}})
|
|
124
|
+
self.assertIn("我刚说过", ctx.get("recent_proactive", ""))
|
|
125
|
+
self.assertIn("乙", ctx["recent_proactive"])
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class UntouchedCooldownTest(unittest.TestCase):
|
|
129
|
+
def test_apply_params_force_writes_cooldown(self):
|
|
130
|
+
self.assertIn('("cooldown_seconds", "cooldown_s", 1800)', SOURCE)
|
|
131
|
+
self.assertNotIn("if key not in out:", SOURCE)
|
|
132
|
+
|
|
133
|
+
def test_wiring_static(self):
|
|
134
|
+
self.assertIn("_recent_proactive_prompt()", SOURCE)
|
|
135
|
+
self.assertIn("_opening_repeats(final)", SOURCE)
|
|
136
|
+
self.assertIn("_record_proactive_sent(", SOURCE)
|
|
137
|
+
self.assertIn("proactive_sent", SOURCE)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
unittest.main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Regression: proactive _send must record with event=None and never NameError."""
|
|
3
|
+
import ast
|
|
4
|
+
import asyncio
|
|
5
|
+
import types
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
MAIN = Path(__file__).resolve().parents[2] / "main.py"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _load_send():
|
|
13
|
+
tree = ast.parse(MAIN.read_text(encoding="utf-8"))
|
|
14
|
+
node = next((n for n in ast.walk(tree)
|
|
15
|
+
if isinstance(n, ast.AsyncFunctionDef) and n.name == "_send"), None)
|
|
16
|
+
assert node is not None, "_send closure not found"
|
|
17
|
+
return node
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FakeTarget:
|
|
21
|
+
sent = []
|
|
22
|
+
|
|
23
|
+
def __init__(self, plugin, umo, private=True):
|
|
24
|
+
self.plugin = plugin
|
|
25
|
+
self.umo = umo
|
|
26
|
+
|
|
27
|
+
async def send(self, chain):
|
|
28
|
+
FakeTarget.sent.append((self.umo, chain))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SendRecordTest(unittest.IsolatedAsyncioTestCase):
|
|
32
|
+
def _run_send(self, recorder):
|
|
33
|
+
FakeTarget.sent = []
|
|
34
|
+
warnings = []
|
|
35
|
+
logger = types.SimpleNamespace(
|
|
36
|
+
warning=lambda *a, **k: warnings.append(a),
|
|
37
|
+
info=lambda *a, **k: None)
|
|
38
|
+
async def _persist_history(umo, text):
|
|
39
|
+
return True
|
|
40
|
+
plugin = types.SimpleNamespace(
|
|
41
|
+
proactive_llm_enabled=False,
|
|
42
|
+
_persist_proactive_history=_persist_history,
|
|
43
|
+
_sanitize_proactive_text=lambda text, cap=0: str(text or ""),
|
|
44
|
+
_opening_repeats=lambda text: False,
|
|
45
|
+
_record_proactive_sent=recorder)
|
|
46
|
+
ns = {
|
|
47
|
+
"self": plugin,
|
|
48
|
+
"text": "哟,老板!今晚好",
|
|
49
|
+
"umo": "umo-1",
|
|
50
|
+
"_built": {"prompt": "", "system": "", "max_chars": 0,
|
|
51
|
+
"reason": "lonely"},
|
|
52
|
+
"_AutonomySendTarget": FakeTarget,
|
|
53
|
+
"MessageChain": lambda parts: list(parts),
|
|
54
|
+
"Plain": lambda text: ("plain", text),
|
|
55
|
+
"logger": logger,
|
|
56
|
+
}
|
|
57
|
+
node = _load_send()
|
|
58
|
+
exec(compile(ast.Module(body=[node], type_ignores=[]), "<send>", "exec"), ns)
|
|
59
|
+
return ns["_send"], warnings
|
|
60
|
+
|
|
61
|
+
async def test_record_called_once_with_event_none(self):
|
|
62
|
+
calls = []
|
|
63
|
+
|
|
64
|
+
def recorder(umo, text, reason="", event=None):
|
|
65
|
+
calls.append({"umo": umo, "text": text, "reason": reason,
|
|
66
|
+
"event": event})
|
|
67
|
+
send, warnings = self._run_send(recorder)
|
|
68
|
+
await send()
|
|
69
|
+
self.assertEqual(len(FakeTarget.sent), 1)
|
|
70
|
+
self.assertEqual(len(calls), 1)
|
|
71
|
+
self.assertEqual(calls[0]["umo"], "umo-1")
|
|
72
|
+
self.assertEqual(calls[0]["text"], "哟,老板!今晚好")
|
|
73
|
+
self.assertEqual(calls[0]["reason"], "lonely")
|
|
74
|
+
self.assertIsNone(calls[0]["event"])
|
|
75
|
+
self.assertEqual(warnings, [])
|
|
76
|
+
|
|
77
|
+
async def test_record_exception_is_swallowed_after_send(self):
|
|
78
|
+
def recorder(*args, **kwargs):
|
|
79
|
+
raise RuntimeError("record boom")
|
|
80
|
+
send, warnings = self._run_send(recorder)
|
|
81
|
+
await send()
|
|
82
|
+
self.assertEqual(len(FakeTarget.sent), 1)
|
|
83
|
+
self.assertTrue(any("proactive record failed" in str(args)
|
|
84
|
+
for args in warnings))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from biocore_model import appraisal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _raw(**extra):
|
|
9
|
+
data = {"dims": {"goal": 0.2, "blame": 0.1, "control": 0.5,
|
|
10
|
+
"certainty": 0.5, "novelty": 0.2, "involve": 0.6,
|
|
11
|
+
"hit": 0.1},
|
|
12
|
+
"why": "test", "c": 0.8}
|
|
13
|
+
data.update(extra)
|
|
14
|
+
return json.dumps(data, ensure_ascii=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProbeContractTest(unittest.TestCase):
|
|
18
|
+
def test_prompt_declares_situation_and_fact(self):
|
|
19
|
+
prompt = appraisal.build_prompt("在吗", "在的")
|
|
20
|
+
for token in ("situation", "challenge", "praise", "hurt", "fact",
|
|
21
|
+
"≤20 字"):
|
|
22
|
+
self.assertIn(token, prompt)
|
|
23
|
+
self.assertIn("对方:在吗", prompt)
|
|
24
|
+
self.assertIn("你:在的", prompt)
|
|
25
|
+
|
|
26
|
+
def test_parse_situation_numbers(self):
|
|
27
|
+
out = appraisal.parse(_raw(situation={"challenge": 0.6,
|
|
28
|
+
"praise": 0.1, "hurt": 0.0}))
|
|
29
|
+
self.assertEqual(out["situation"]["challenge"], 0.6)
|
|
30
|
+
self.assertEqual(out["situation"]["praise"], 0.1)
|
|
31
|
+
self.assertEqual(out["situation"]["hurt"], 0.0)
|
|
32
|
+
self.assertEqual(out["dims"]["goal"], 0.2)
|
|
33
|
+
|
|
34
|
+
def test_parse_clamps_situation_range(self):
|
|
35
|
+
out = appraisal.parse(_raw(situation={"challenge": 2.0,
|
|
36
|
+
"praise": -1.0, "hurt": 0.4}))
|
|
37
|
+
self.assertEqual(out["situation"]["challenge"], 1.0)
|
|
38
|
+
self.assertEqual(out["situation"]["praise"], 0.0)
|
|
39
|
+
self.assertEqual(out["situation"]["hurt"], 0.4)
|
|
40
|
+
|
|
41
|
+
def test_parse_fact_short_kept(self):
|
|
42
|
+
out = appraisal.parse(_raw(fact="他姓王"))
|
|
43
|
+
self.assertEqual(out["fact"], "他姓王")
|
|
44
|
+
|
|
45
|
+
def test_parse_fact_truncated_to_limit(self):
|
|
46
|
+
out = appraisal.parse(_raw(fact="他" * 40))
|
|
47
|
+
self.assertEqual(len(out["fact"]), appraisal.FACT_MAX_CHARS)
|
|
48
|
+
|
|
49
|
+
def test_parse_missing_optional_fields(self):
|
|
50
|
+
out = appraisal.parse(_raw())
|
|
51
|
+
self.assertEqual(out["situation"], {})
|
|
52
|
+
self.assertEqual(out["fact"], "")
|
|
53
|
+
|
|
54
|
+
def test_parse_low_confidence_drops_everything(self):
|
|
55
|
+
raw = json.dumps({"dims": {"goal": 0.5}, "c": 0.2,
|
|
56
|
+
"situation": {"challenge": 0.9},
|
|
57
|
+
"fact": "他姓王"}, ensure_ascii=False)
|
|
58
|
+
self.assertEqual(appraisal.parse(raw), {})
|
|
59
|
+
|
|
60
|
+
def test_parse_malformed_situation_is_safe(self):
|
|
61
|
+
out = appraisal.parse(_raw(situation={"challenge": "高",
|
|
62
|
+
"praise": None, "hurt": 0.3}))
|
|
63
|
+
self.assertNotIn("challenge", out["situation"])
|
|
64
|
+
self.assertNotIn("praise", out["situation"])
|
|
65
|
+
self.assertEqual(out["situation"]["hurt"], 0.3)
|
|
66
|
+
out2 = appraisal.parse(_raw(situation=["challenge"]))
|
|
67
|
+
self.assertEqual(out2["situation"], {})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
unittest.main()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Prospective memory: registration, due queries, overdue, dedup."""
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from biocore_model import prospective as pm
|
|
6
|
+
|
|
7
|
+
NOW = 1790000000.0
|
|
8
|
+
DAY = 86400.0
|
|
9
|
+
|
|
10
|
+
class NormalizeTest(unittest.TestCase):
|
|
11
|
+
def test_strips_punctuation_and_space(self):
|
|
12
|
+
self.assertEqual(pm.normalize(" 明天 问你,结果! "), "明天问你结果")
|
|
13
|
+
|
|
14
|
+
def test_none_is_empty(self):
|
|
15
|
+
self.assertEqual(pm.normalize(None), "")
|
|
16
|
+
|
|
17
|
+
class IdTest(unittest.TestCase):
|
|
18
|
+
def test_same_text_same_id(self):
|
|
19
|
+
a = pm.make_id("her_promise", "问他体检结果", NOW)
|
|
20
|
+
b = pm.make_id("her_promise", "问他体检结果!", NOW + 3600)
|
|
21
|
+
self.assertEqual(a, b)
|
|
22
|
+
|
|
23
|
+
def test_reschedule_keeps_id(self):
|
|
24
|
+
a = pm.make_id("her_promise", "问他体检结果", NOW)
|
|
25
|
+
b = pm.make_id("her_promise", "问他体检结果", NOW + 7 * DAY)
|
|
26
|
+
self.assertEqual(a, b)
|
|
27
|
+
|
|
28
|
+
def test_kind_changes_id(self):
|
|
29
|
+
a = pm.make_id("her_promise", "体检", NOW)
|
|
30
|
+
b = pm.make_id("his_event", "体检", NOW)
|
|
31
|
+
self.assertNotEqual(a, b)
|
|
32
|
+
|
|
33
|
+
class EntryTest(unittest.TestCase):
|
|
34
|
+
def test_fields(self):
|
|
35
|
+
row = pm.entry("his_event", "周五体检", NOW + 2 * DAY, now=NOW)
|
|
36
|
+
self.assertEqual(row["kind"], "his_event")
|
|
37
|
+
self.assertEqual(row["status"], "pending")
|
|
38
|
+
self.assertEqual(row["due_ts"], NOW + 2 * DAY)
|
|
39
|
+
self.assertEqual(row["created_ts"], NOW)
|
|
40
|
+
self.assertTrue(row["id"])
|
|
41
|
+
|
|
42
|
+
def test_text_truncated(self):
|
|
43
|
+
row = pm.entry("his_event", "x" * 200, NOW)
|
|
44
|
+
self.assertEqual(len(row["text"]), 80)
|
|
45
|
+
|
|
46
|
+
class AddTest(unittest.TestCase):
|
|
47
|
+
def test_add_new(self):
|
|
48
|
+
row = pm.entry("his_event", "周五体检", NOW + DAY, now=NOW)
|
|
49
|
+
store, changed = pm.add([], row)
|
|
50
|
+
self.assertTrue(changed)
|
|
51
|
+
self.assertEqual(len(store), 1)
|
|
52
|
+
|
|
53
|
+
def test_duplicate_refreshes_due(self):
|
|
54
|
+
row = pm.entry("his_event", "周五体检", NOW + DAY, now=NOW)
|
|
55
|
+
store, _ = pm.add([], row)
|
|
56
|
+
later = pm.entry("his_event", "周五体检", NOW + 2 * DAY, now=NOW)
|
|
57
|
+
store, changed = pm.add(store, later)
|
|
58
|
+
self.assertTrue(changed)
|
|
59
|
+
self.assertEqual(len(store), 1)
|
|
60
|
+
self.assertEqual(store[0]["due_ts"], NOW + 2 * DAY)
|
|
61
|
+
|
|
62
|
+
def test_duplicate_done_stays_done(self):
|
|
63
|
+
row = pm.entry("his_event", "周五体检", NOW + DAY, now=NOW)
|
|
64
|
+
store, _ = pm.add([], row)
|
|
65
|
+
store, _ = pm.resolve(store, row["id"], "done", now=NOW)
|
|
66
|
+
again = pm.entry("his_event", "周五体检", NOW + DAY, now=NOW)
|
|
67
|
+
store, changed = pm.add(store, again)
|
|
68
|
+
self.assertFalse(changed)
|
|
69
|
+
self.assertEqual(store[0]["status"], "done")
|
|
70
|
+
|
|
71
|
+
def test_bad_record_ignored(self):
|
|
72
|
+
store, changed = pm.add([], {"text": "没有 id"})
|
|
73
|
+
self.assertFalse(changed)
|
|
74
|
+
self.assertEqual(store, [])
|
|
75
|
+
|
|
76
|
+
class DueTest(unittest.TestCase):
|
|
77
|
+
def _store(self):
|
|
78
|
+
rows = [pm.entry("his_event", "早上的事", NOW - 3 * 3600, now=NOW),
|
|
79
|
+
pm.entry("her_promise", "刚到期", NOW, now=NOW),
|
|
80
|
+
pm.entry("his_event", "明天的事", NOW + DAY, now=NOW)]
|
|
81
|
+
store = []
|
|
82
|
+
for row in rows:
|
|
83
|
+
store, _ = pm.add(store, row)
|
|
84
|
+
return store
|
|
85
|
+
|
|
86
|
+
def test_due_now_with_window(self):
|
|
87
|
+
rows = pm.due_now(self._store(), NOW, window_s=4 * 3600)
|
|
88
|
+
self.assertEqual([row["text"] for row in rows], ["早上的事", "刚到期"])
|
|
89
|
+
|
|
90
|
+
def test_due_now_without_window(self):
|
|
91
|
+
rows = pm.due_now(self._store(), NOW)
|
|
92
|
+
self.assertEqual([row["text"] for row in rows], ["刚到期"])
|
|
93
|
+
|
|
94
|
+
def test_due_soon(self):
|
|
95
|
+
rows = pm.due_soon(self._store(), NOW, ahead_s=2 * DAY)
|
|
96
|
+
self.assertEqual([row["text"] for row in rows], ["明天的事"])
|
|
97
|
+
|
|
98
|
+
class OverdueTest(unittest.TestCase):
|
|
99
|
+
def test_marks_after_grace(self):
|
|
100
|
+
row = pm.entry("his_event", "昨天的事", NOW - DAY, now=NOW - DAY)
|
|
101
|
+
store, _ = pm.add([], row)
|
|
102
|
+
store, changed = pm.mark_overdue(store, NOW)
|
|
103
|
+
self.assertEqual(changed, 1)
|
|
104
|
+
self.assertEqual(store[0]["status"], "overdue")
|
|
105
|
+
|
|
106
|
+
def test_not_early(self):
|
|
107
|
+
row = pm.entry("his_event", "刚到期", NOW, now=NOW)
|
|
108
|
+
store, _ = pm.add([], row)
|
|
109
|
+
store, changed = pm.mark_overdue(store, NOW + 60)
|
|
110
|
+
self.assertEqual(changed, 0)
|
|
111
|
+
|
|
112
|
+
class ResolveTest(unittest.TestCase):
|
|
113
|
+
def test_done(self):
|
|
114
|
+
row = pm.entry("his_event", "周五体检", NOW + DAY, now=NOW)
|
|
115
|
+
store, _ = pm.add([], row)
|
|
116
|
+
store, changed = pm.resolve(store, row["id"], "done", now=NOW)
|
|
117
|
+
self.assertTrue(changed)
|
|
118
|
+
self.assertEqual(store[0]["status"], "done")
|
|
119
|
+
|
|
120
|
+
def test_invalid_status_noop(self):
|
|
121
|
+
row = pm.entry("his_event", "周五体检", NOW + DAY, now=NOW)
|
|
122
|
+
store, _ = pm.add([], row)
|
|
123
|
+
store, changed = pm.resolve(store, row["id"], "weird")
|
|
124
|
+
self.assertFalse(changed)
|
|
125
|
+
self.assertEqual(store[0]["status"], "pending")
|
|
126
|
+
|
|
127
|
+
class HintTest(unittest.TestCase):
|
|
128
|
+
def test_pending_hint(self):
|
|
129
|
+
row = pm.entry("his_event", "周五体检", NOW + DAY, now=NOW)
|
|
130
|
+
self.assertEqual(pm.build_hint(row, NOW), "他提过的事到了:周五体检")
|
|
131
|
+
|
|
132
|
+
def test_overdue_hint(self):
|
|
133
|
+
row = pm.entry("her_promise", "问他结果", NOW - DAY, now=NOW - DAY)
|
|
134
|
+
self.assertIn("还没问", pm.build_hint(row, NOW))
|
|
135
|
+
|
|
136
|
+
def test_empty_text(self):
|
|
137
|
+
self.assertEqual(pm.build_hint({"kind": "his_event", "text": ""}), "")
|
|
138
|
+
|
|
139
|
+
class StatsTest(unittest.TestCase):
|
|
140
|
+
def test_counts(self):
|
|
141
|
+
store = []
|
|
142
|
+
for kind, text, due in (("his_event", "a", NOW), ("her_promise", "b", NOW),
|
|
143
|
+
("his_event", "c", NOW)):
|
|
144
|
+
store, _ = pm.add(store, pm.entry(kind, text, due, now=NOW))
|
|
145
|
+
out = pm.stats(store)
|
|
146
|
+
self.assertEqual(out["total"], 3)
|
|
147
|
+
self.assertEqual(out["by_kind"]["his_event"], 2)
|
|
148
|
+
self.assertEqual(out["by_kind"]["her_promise"], 1)
|
|
149
|
+
|
|
150
|
+
def test_malformed_store(self):
|
|
151
|
+
self.assertEqual(pm.stats([None, "x", 3])["total"], 0)
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
unittest.main()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-127: fallback ordering/cooldown tests."""
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from biocore_model import provider_fallback as pf
|
|
7
|
+
|
|
8
|
+
REPO = Path(__file__).resolve().parents[2]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CandidatesTest(unittest.TestCase):
|
|
12
|
+
def test_primary_only_when_fallback_empty(self):
|
|
13
|
+
self.assertEqual(pf.candidates("main", [], {}, 1000.0), ["main"])
|
|
14
|
+
self.assertEqual(pf.candidates("main", None, {}, 1000.0), ["main"])
|
|
15
|
+
|
|
16
|
+
def test_order_primary_then_fallbacks(self):
|
|
17
|
+
out = pf.candidates("main", ["backup1", "backup2"], {}, 1000.0)
|
|
18
|
+
self.assertEqual(out, ["main", "backup1", "backup2"])
|
|
19
|
+
|
|
20
|
+
def test_dedupes_and_cleans_ids(self):
|
|
21
|
+
out = pf.candidates(" main ", ["main", "backup1", " ", None, "backup1"],
|
|
22
|
+
{}, 1000.0)
|
|
23
|
+
self.assertEqual(out, ["main", "backup1"])
|
|
24
|
+
|
|
25
|
+
def test_primary_none_uses_fallbacks(self):
|
|
26
|
+
self.assertEqual(pf.candidates(None, ["backup1"], {}, 1000.0),
|
|
27
|
+
["backup1"])
|
|
28
|
+
|
|
29
|
+
def test_cooling_provider_skipped(self):
|
|
30
|
+
state = {"main": 995.0}
|
|
31
|
+
self.assertEqual(pf.candidates("main", ["backup1"], state, 1000.0),
|
|
32
|
+
["backup1"])
|
|
33
|
+
|
|
34
|
+
def test_cooldown_expires(self):
|
|
35
|
+
state = {"main": 900.0}
|
|
36
|
+
self.assertEqual(pf.candidates("main", ["backup1"], state, 1000.0),
|
|
37
|
+
["main", "backup1"])
|
|
38
|
+
|
|
39
|
+
def test_all_cooling_returns_empty(self):
|
|
40
|
+
state = {"main": 999.0, "backup1": 998.0}
|
|
41
|
+
self.assertEqual(pf.candidates("main", ["backup1"], state, 1000.0), [],
|
|
42
|
+
msg="every candidate cooling")
|
|
43
|
+
|
|
44
|
+
def test_custom_cooldown(self):
|
|
45
|
+
state = {"main": 800.0}
|
|
46
|
+
self.assertEqual(pf.candidates("main", [], state, 1000.0, cooldown=300),
|
|
47
|
+
[])
|
|
48
|
+
self.assertEqual(pf.candidates("main", [], state, 1000.0, cooldown=60),
|
|
49
|
+
["main"])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class StateTest(unittest.TestCase):
|
|
53
|
+
def test_mark_failure_copies_state(self):
|
|
54
|
+
state = {}
|
|
55
|
+
out = pf.mark_failure(state, "backup1", 123.0)
|
|
56
|
+
self.assertEqual(out["backup1"], 123.0)
|
|
57
|
+
self.assertEqual(state, {})
|
|
58
|
+
|
|
59
|
+
def test_mark_success_records_current_and_clears_cooldown(self):
|
|
60
|
+
state = {"main": 100.0, "backup1": 101.0}
|
|
61
|
+
out = pf.mark_success(state, "backup1", 200.0)
|
|
62
|
+
self.assertNotIn("backup1", out)
|
|
63
|
+
self.assertEqual(pf.current(out), "backup1")
|
|
64
|
+
self.assertEqual(out["main"], 100.0)
|
|
65
|
+
self.assertEqual(state["backup1"], 101.0)
|
|
66
|
+
|
|
67
|
+
def test_reserved_keys_never_become_candidates(self):
|
|
68
|
+
state = pf.mark_success({}, "backup1", 10.0)
|
|
69
|
+
self.assertEqual(pf.candidates("main", ["current"], state, 20.0),
|
|
70
|
+
["main"])
|
|
71
|
+
self.assertFalse(pf.is_cooling(state, "current", 20.0))
|
|
72
|
+
|
|
73
|
+
def test_describe(self):
|
|
74
|
+
state = {"main": 100.0}
|
|
75
|
+
self.assertEqual(pf.describe(state), "failed=main")
|
|
76
|
+
state = pf.mark_success(state, "backup1", 200.0)
|
|
77
|
+
self.assertIn("current=backup1", pf.describe(state))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class WiringStaticTest(unittest.TestCase):
|
|
81
|
+
def test_main_has_fallback_wiring(self):
|
|
82
|
+
source = (REPO / "main.py").read_text(encoding="utf-8")
|
|
83
|
+
self.assertIn("provider_fallback_order", source)
|
|
84
|
+
self.assertIn("[biocore] provider fallback from=%s to=%s reason=%s", source)
|
|
85
|
+
self.assertIn("provider_fallback as _pf", source)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
unittest.main()
|