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,245 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Real old-layout migration test: 1.3.4 data -> 1.4.0, then idempotency."""
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
import types
|
|
9
|
+
import unittest
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from biocore_model import migrate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FakePlugin:
|
|
16
|
+
def __init__(self, data_dir):
|
|
17
|
+
self.data_dir = str(data_dir)
|
|
18
|
+
|
|
19
|
+
def _autonomy_params(self):
|
|
20
|
+
return {"theta": 0.5239, "cooldown_s": 1625, "daily_cap": 3,
|
|
21
|
+
"tau_hours": 16.0}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _old_layout(root):
|
|
25
|
+
data = Path(root) / "plugins" / "astrbot_plugin_biocore" / "data"
|
|
26
|
+
(data / "memory").mkdir(parents=True, exist_ok=True)
|
|
27
|
+
(data / "states").mkdir(parents=True, exist_ok=True)
|
|
28
|
+
(data / "ui_settings.json").write_text(json.dumps({
|
|
29
|
+
"autonomy": {"enabled": True, "threshold": 0.7,
|
|
30
|
+
"accumulator": {"V": -0.16, "drift": 0.02,
|
|
31
|
+
"last_tick_ts": 123.0}},
|
|
32
|
+
"proactive_autonomy_threshold": 0.7,
|
|
33
|
+
"proactive_autonomy_max_chars": 120,
|
|
34
|
+
"schema": 1,
|
|
35
|
+
}, ensure_ascii=False), encoding="utf-8")
|
|
36
|
+
mem = data / "memory" / "s1.json"
|
|
37
|
+
mem.write_text(json.dumps({"entries": [
|
|
38
|
+
{"id": "m1", "text": "老记忆一"},
|
|
39
|
+
{"id": "m2", "text": "老记忆二"},
|
|
40
|
+
{"id": "m3", "text": "老记忆三"},
|
|
41
|
+
]}, ensure_ascii=False), encoding="utf-8")
|
|
42
|
+
old = time.time() - 10 * 86400
|
|
43
|
+
os.utime(mem, (old, old))
|
|
44
|
+
(data / "states" / "u1.json").write_text(
|
|
45
|
+
json.dumps({"umo": "user-x"}), encoding="utf-8")
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_methods(names, namespace=None):
|
|
50
|
+
source = (Path(__file__).resolve().parents[2] / "main.py").read_text(
|
|
51
|
+
encoding="utf-8")
|
|
52
|
+
tree = ast.parse(source)
|
|
53
|
+
cls = next(n for n in tree.body
|
|
54
|
+
if isinstance(n, ast.ClassDef) and n.name == "BioCorePlugin")
|
|
55
|
+
ns = dict(namespace or {})
|
|
56
|
+
fns = {}
|
|
57
|
+
for node in cls.body:
|
|
58
|
+
if isinstance(node, ast.FunctionDef) and node.name in names:
|
|
59
|
+
clone = ast.FunctionDef(name=node.name, args=node.args, body=node.body,
|
|
60
|
+
decorator_list=[], returns=node.returns,
|
|
61
|
+
type_comment=None, lineno=node.lineno,
|
|
62
|
+
col_offset=node.col_offset,
|
|
63
|
+
end_lineno=node.end_lineno,
|
|
64
|
+
end_col_offset=node.end_col_offset)
|
|
65
|
+
exec(compile(ast.Module(body=[clone], type_ignores=[]), "<mig>", "exec"), ns)
|
|
66
|
+
fns[node.name] = ns[node.name]
|
|
67
|
+
return fns
|
|
68
|
+
|
|
69
|
+
class _Recorder:
|
|
70
|
+
"""Minimal logger stub; keeps tests free of the stdlib logging import."""
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
self.lines = []
|
|
74
|
+
|
|
75
|
+
def _log(self, msg, *args, **kwargs):
|
|
76
|
+
try:
|
|
77
|
+
self.lines.append(str(msg) % args if args else str(msg))
|
|
78
|
+
except Exception:
|
|
79
|
+
self.lines.append(str(msg))
|
|
80
|
+
|
|
81
|
+
debug = info = warning = error = exception = critical = _log
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MigrationTest(unittest.TestCase):
|
|
85
|
+
def test_old_data_migrates_and_backfills(self):
|
|
86
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
87
|
+
data = _old_layout(tmp)
|
|
88
|
+
plugin = FakePlugin(data)
|
|
89
|
+
rec = _Recorder()
|
|
90
|
+
saved_logger = migrate.logger
|
|
91
|
+
migrate.logger = rec
|
|
92
|
+
try:
|
|
93
|
+
report = migrate.migrate(plugin)
|
|
94
|
+
finally:
|
|
95
|
+
migrate.logger = saved_logger
|
|
96
|
+
self.assertEqual(report["applied"],
|
|
97
|
+
["settings", "accumulator", "memory"])
|
|
98
|
+
auth = (Path(tmp) / "plugin_data" / "astrbot_plugin_biocore"
|
|
99
|
+
/ "ui_settings.json")
|
|
100
|
+
saved = json.loads(auth.read_text(encoding="utf-8"))
|
|
101
|
+
self.assertEqual(saved.get("schema_version"),
|
|
102
|
+
migrate.SCHEMA_VERSION)
|
|
103
|
+
acc = saved["autonomy"]["accumulator"]
|
|
104
|
+
self.assertAlmostEqual(float(acc["theta"]), 0.5239, places=3)
|
|
105
|
+
self.assertGreaterEqual(float(acc["V"]), 0.0)
|
|
106
|
+
self.assertEqual(acc["cooldown_seconds"], 1625)
|
|
107
|
+
self.assertEqual(acc["daily_cap"], 3)
|
|
108
|
+
self.assertEqual(acc["last_umo"], "user-x")
|
|
109
|
+
mem = json.loads((data / "memory" / "s1.json").read_text(
|
|
110
|
+
encoding="utf-8"))
|
|
111
|
+
self.assertEqual(len(mem["entries"]), 3)
|
|
112
|
+
for entry in mem["entries"]:
|
|
113
|
+
self.assertIn("storage_s", entry)
|
|
114
|
+
self.assertIn("retrieval_s", entry)
|
|
115
|
+
self.assertIn("access_count", entry)
|
|
116
|
+
self.assertIn("arousal_at_encoding", entry)
|
|
117
|
+
self.assertFalse(entry["hidden"])
|
|
118
|
+
self.assertGreater(entry["storage_s"], 0.0)
|
|
119
|
+
# Old 10-day file -> power-law retrieval below a fresh file.
|
|
120
|
+
self.assertLess(mem["entries"][0]["retrieval_s"], 0.5)
|
|
121
|
+
joined = "\n".join(rec.lines)
|
|
122
|
+
for token in ("migrate done:", "settings=", "accumulator=",
|
|
123
|
+
"memory=", "warnings="):
|
|
124
|
+
self.assertIn(token, joined)
|
|
125
|
+
|
|
126
|
+
def test_schema_version_survives_ui_settings_save(self):
|
|
127
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
128
|
+
data = _old_layout(tmp)
|
|
129
|
+
plugin = FakePlugin(data)
|
|
130
|
+
migrate.migrate(plugin)
|
|
131
|
+
from biocore_model import ui_settings
|
|
132
|
+
ui_settings.save(plugin, {"autonomy": {"send": True}})
|
|
133
|
+
auth = (Path(tmp) / "plugin_data" / "astrbot_plugin_biocore"
|
|
134
|
+
/ "ui_settings.json")
|
|
135
|
+
saved = json.loads(auth.read_text(encoding="utf-8"))
|
|
136
|
+
self.assertEqual(saved.get("schema_version"),
|
|
137
|
+
migrate.SCHEMA_VERSION)
|
|
138
|
+
self.assertEqual(migrate.migrate(plugin)["applied"], [])
|
|
139
|
+
|
|
140
|
+
def test_second_run_is_idempotent(self):
|
|
141
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
142
|
+
data = _old_layout(tmp)
|
|
143
|
+
plugin = FakePlugin(data)
|
|
144
|
+
migrate.migrate(plugin)
|
|
145
|
+
auth = (Path(tmp) / "plugin_data" / "astrbot_plugin_biocore"
|
|
146
|
+
/ "ui_settings.json")
|
|
147
|
+
mem = data / "memory" / "s1.json"
|
|
148
|
+
before_settings = auth.read_bytes()
|
|
149
|
+
before_mem = mem.read_bytes()
|
|
150
|
+
report = migrate.migrate(plugin)
|
|
151
|
+
self.assertEqual(report["applied"], [])
|
|
152
|
+
self.assertEqual(auth.read_bytes(), before_settings)
|
|
153
|
+
self.assertEqual(mem.read_bytes(), before_mem)
|
|
154
|
+
|
|
155
|
+
def test_fresh_install_is_noop_and_safe(self):
|
|
156
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
157
|
+
fresh = Path(tmp) / "plugin_data" / "astrbot_plugin_biocore"
|
|
158
|
+
fresh.mkdir(parents=True)
|
|
159
|
+
plugin = FakePlugin(fresh)
|
|
160
|
+
report = migrate.migrate(plugin)
|
|
161
|
+
self.assertEqual(report["applied"], [])
|
|
162
|
+
self.assertEqual(report["warnings"], [])
|
|
163
|
+
auth = fresh / "ui_settings.json"
|
|
164
|
+
self.assertTrue(auth.is_file())
|
|
165
|
+
self.assertEqual(json.loads(auth.read_text(encoding="utf-8"))
|
|
166
|
+
.get("schema_version"),
|
|
167
|
+
migrate.SCHEMA_VERSION)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
unittest.main()
|
|
172
|
+
|
|
173
|
+
class HardeningTest(unittest.TestCase):
|
|
174
|
+
def test_atomic_write_failure_leaves_no_tmp_or_partial(self):
|
|
175
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
176
|
+
target = Path(tmp) / "x.json"
|
|
177
|
+
target.write_text('{"old": 1}', encoding="utf-8")
|
|
178
|
+
from unittest import mock
|
|
179
|
+
with mock.patch.object(migrate.os, "replace", side_effect=OSError("boom")):
|
|
180
|
+
with self.assertRaises(OSError):
|
|
181
|
+
migrate._write(target, {"new": 2})
|
|
182
|
+
self.assertEqual(target.read_text(encoding="utf-8"), '{"old": 1}')
|
|
183
|
+
self.assertFalse((Path(tmp) / "x.json.tmp").exists())
|
|
184
|
+
|
|
185
|
+
def test_backup_contains_three_classes_and_migrates(self):
|
|
186
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
187
|
+
data = _old_layout(tmp)
|
|
188
|
+
plugin = FakePlugin(data)
|
|
189
|
+
report = migrate.migrate(plugin)
|
|
190
|
+
backup = Path(report["backup_dir"])
|
|
191
|
+
self.assertTrue(backup.is_dir())
|
|
192
|
+
self.assertTrue((backup / "settings" / "ui_settings.json").is_file())
|
|
193
|
+
self.assertEqual(len(list((backup / "memory").glob("*.json"))), 1)
|
|
194
|
+
self.assertEqual(len(list((backup / "states").glob("*.json"))), 1)
|
|
195
|
+
self.assertIn("memory", report["applied"])
|
|
196
|
+
|
|
197
|
+
def test_failed_migration_keeps_old_files(self):
|
|
198
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
199
|
+
data = _old_layout(tmp)
|
|
200
|
+
old_legacy = (data / "ui_settings.json").read_bytes()
|
|
201
|
+
plugin = FakePlugin(data)
|
|
202
|
+
from unittest import mock
|
|
203
|
+
with mock.patch.object(migrate, "_write", side_effect=OSError("disk")):
|
|
204
|
+
report = migrate.migrate(plugin)
|
|
205
|
+
self.assertTrue(report["warnings"])
|
|
206
|
+
self.assertEqual((data / "ui_settings.json").read_bytes(), old_legacy)
|
|
207
|
+
auth = Path(tmp) / "plugin_data" / "astrbot_plugin_biocore" / "ui_settings.json"
|
|
208
|
+
self.assertFalse(auth.exists())
|
|
209
|
+
|
|
210
|
+
def test_restore_brings_backup_back(self):
|
|
211
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
212
|
+
data = _old_layout(tmp)
|
|
213
|
+
plugin = FakePlugin(data)
|
|
214
|
+
report = migrate.migrate(plugin)
|
|
215
|
+
backup = report["backup_dir"]
|
|
216
|
+
auth = (Path(tmp) / "plugin_data" / "astrbot_plugin_biocore"
|
|
217
|
+
/ "ui_settings.json")
|
|
218
|
+
auth.write_text("{}", encoding="utf-8")
|
|
219
|
+
mem = data / "memory" / "s1.json"
|
|
220
|
+
mem.write_text("{}", encoding="utf-8")
|
|
221
|
+
result = migrate.restore(backup, plugin)
|
|
222
|
+
self.assertTrue(result["ok"])
|
|
223
|
+
restored = json.loads(auth.read_text(encoding="utf-8"))
|
|
224
|
+
self.assertIn("autonomy", restored)
|
|
225
|
+
self.assertIn("entries", json.loads(mem.read_text(encoding="utf-8")))
|
|
226
|
+
self.assertGreaterEqual(result["restored"]["states"], 1)
|
|
227
|
+
|
|
228
|
+
def test_retry_path_runs_once(self):
|
|
229
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
230
|
+
data = _old_layout(tmp)
|
|
231
|
+
plugin = FakePlugin(data)
|
|
232
|
+
ns = {"time": time, "logger": types.SimpleNamespace(
|
|
233
|
+
warning=lambda *a, **k: None, info=lambda *a, **k: None)}
|
|
234
|
+
fns = _load_methods(("_migration_payload", "_ensure_migration_retry_once"),
|
|
235
|
+
namespace=ns)
|
|
236
|
+
for name, fn in fns.items():
|
|
237
|
+
setattr(plugin, name, types.MethodType(fn, plugin))
|
|
238
|
+
plugin._migration_retry_done = False
|
|
239
|
+
plugin._migration_report = {}
|
|
240
|
+
payload = plugin._ensure_migration_retry_once()
|
|
241
|
+
self.assertGreaterEqual(payload["schema_version"], 2)
|
|
242
|
+
first = (data / "ui_settings.json").read_bytes()
|
|
243
|
+
plugin._ensure_migration_retry_once()
|
|
244
|
+
self.assertEqual((data / "ui_settings.json").read_bytes(), first)
|
|
245
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Offline tests for biocore_model.migrate_store (temp dirs only)."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from unittest import mock
|
|
9
|
+
|
|
10
|
+
from biocore_model import migrate_store
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def entry(**overrides):
|
|
14
|
+
value = {"id": "abc123", "text": "hello memory", "ts": 1700000000.0, "role": "user",
|
|
15
|
+
"importance": 0.8, "pin": True, "scope": "default", "tags": ["plan"]}
|
|
16
|
+
value.update(overrides)
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConvertTest(unittest.TestCase):
|
|
21
|
+
def test_normal_mapping(self):
|
|
22
|
+
core, report = migrate_store.convert([entry()])
|
|
23
|
+
self.assertEqual(report["converted"], 1)
|
|
24
|
+
self.assertEqual(core[0]["id"], "abc123")
|
|
25
|
+
self.assertEqual(core[0]["text"], "hello memory")
|
|
26
|
+
self.assertEqual(core[0]["ts"], 1700000000.0)
|
|
27
|
+
self.assertEqual(core[0]["role"], "user")
|
|
28
|
+
self.assertEqual(core[0]["importance"], 0.8)
|
|
29
|
+
self.assertTrue(core[0]["pin"])
|
|
30
|
+
self.assertEqual(core[0]["tags"], ["plan"])
|
|
31
|
+
self.assertEqual(core[0]["client_scope"], "global")
|
|
32
|
+
|
|
33
|
+
def test_defaults_for_missing_optional_fields(self):
|
|
34
|
+
core, _ = migrate_store.convert([{"id": "x", "text": "only text"}])
|
|
35
|
+
self.assertEqual(core[0]["role"], "user")
|
|
36
|
+
self.assertEqual(core[0]["tags"], [])
|
|
37
|
+
self.assertEqual(core[0]["importance"], 0.5)
|
|
38
|
+
self.assertFalse(core[0]["pin"])
|
|
39
|
+
self.assertEqual(core[0]["client_scope"], "global")
|
|
40
|
+
|
|
41
|
+
def test_unknown_fields_are_reported_not_dropped_silently(self):
|
|
42
|
+
core, report = migrate_store.convert([entry(vector=[0.1, 0.2], sender_id="42", memory_tier="hot")])
|
|
43
|
+
self.assertEqual(report.get("unmapped_fields", {}).get("vector", 0), 0)
|
|
44
|
+
self.assertEqual(report["vectors_migrated"], 1)
|
|
45
|
+
self.assertEqual(report["unmapped_fields"]["sender_id"], 1)
|
|
46
|
+
self.assertEqual(report["unmapped_fields"]["memory_tier"], 1)
|
|
47
|
+
self.assertIn("vector", core[0])
|
|
48
|
+
|
|
49
|
+
def test_missing_text_is_skipped_and_counted(self):
|
|
50
|
+
core, report = migrate_store.convert([{"id": "a", "text": ""}, {"id": "b", "text": "ok"}])
|
|
51
|
+
self.assertEqual(report["converted"], 1)
|
|
52
|
+
self.assertEqual(report["skipped"], 1)
|
|
53
|
+
self.assertEqual(report["missing_required"]["text"], 1)
|
|
54
|
+
self.assertEqual(core[0]["id"], "b")
|
|
55
|
+
|
|
56
|
+
def test_empty_and_non_dict_entries(self):
|
|
57
|
+
core, report = migrate_store.convert([])
|
|
58
|
+
self.assertEqual((report["converted"], report["skipped"]), (0, 0))
|
|
59
|
+
core, report = migrate_store.convert([None, "nope", entry()])
|
|
60
|
+
self.assertEqual(report["converted"], 1)
|
|
61
|
+
self.assertEqual(report["skipped"], 2)
|
|
62
|
+
|
|
63
|
+
def test_generated_id_is_deterministic(self):
|
|
64
|
+
row = {"text": "no id here", "ts": 1700000001.0, "role": "user"}
|
|
65
|
+
first, report = migrate_store.convert([row])
|
|
66
|
+
second, _ = migrate_store.convert([row])
|
|
67
|
+
self.assertEqual(first[0]["id"], second[0]["id"])
|
|
68
|
+
self.assertEqual(len(first[0]["id"]), 12)
|
|
69
|
+
self.assertEqual(report["generated_ids"], 1)
|
|
70
|
+
|
|
71
|
+
def test_scope_mapping(self):
|
|
72
|
+
core, _ = migrate_store.convert([entry(scope="session:dsh-1"), dict(entry(), id="b", scope="default")])
|
|
73
|
+
self.assertEqual(core[0]["client_scope"], "session:dsh-1")
|
|
74
|
+
self.assertEqual(core[1]["client_scope"], "global")
|
|
75
|
+
|
|
76
|
+
def test_valid_at_is_carried(self):
|
|
77
|
+
core, _ = migrate_store.convert([entry(valid_at=1700000000.0)])
|
|
78
|
+
self.assertEqual(core[0]["valid_at"], 1700000000.0)
|
|
79
|
+
|
|
80
|
+
def test_convert_is_idempotent(self):
|
|
81
|
+
rows = [entry(), entry(id="b", text="second")]
|
|
82
|
+
first, report_first = migrate_store.convert(rows)
|
|
83
|
+
second, report_second = migrate_store.convert(rows)
|
|
84
|
+
self.assertEqual(first, second)
|
|
85
|
+
self.assertEqual(report_first, report_second)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SourceAndRunTest(unittest.TestCase):
|
|
89
|
+
def setUp(self):
|
|
90
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
91
|
+
self.root = Path(self.tmp.name)
|
|
92
|
+
self.store_file = self.root / "memory.json"
|
|
93
|
+
self.store_file.write_text(json.dumps({"entries": [entry()]}), encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
def tearDown(self):
|
|
96
|
+
self.tmp.cleanup()
|
|
97
|
+
|
|
98
|
+
def test_detect_source_via_env(self):
|
|
99
|
+
with mock.patch.dict(os.environ, {migrate_store.SOURCE_ENV: str(self.store_file)}):
|
|
100
|
+
self.assertEqual(migrate_store.detect_source(), str(self.store_file))
|
|
101
|
+
|
|
102
|
+
def test_detect_source_from_launcher_layout(self):
|
|
103
|
+
plugin = self.root / "instances" / "inst1" / "core" / "data" / "plugin_data" / "astrbot_plugin_biocore_standalone"
|
|
104
|
+
memory = plugin / "memory"
|
|
105
|
+
memory.mkdir(parents=True)
|
|
106
|
+
(memory / "a.json").write_text(json.dumps({"entries": [entry()]}), encoding="utf-8")
|
|
107
|
+
with mock.patch.dict(os.environ, {migrate_store.SOURCE_ENV: ""}):
|
|
108
|
+
found = migrate_store.detect_source(candidates=[self.root / "instances"])
|
|
109
|
+
self.assertEqual(found, str(memory))
|
|
110
|
+
|
|
111
|
+
def test_detect_source_missing_returns_none(self):
|
|
112
|
+
empty = self.root / "nothing"
|
|
113
|
+
empty.mkdir()
|
|
114
|
+
with mock.patch.dict(os.environ, {migrate_store.SOURCE_ENV: ""}):
|
|
115
|
+
self.assertIsNone(migrate_store.detect_source(candidates=[empty]))
|
|
116
|
+
|
|
117
|
+
def test_corrupt_source_reports_and_skips(self):
|
|
118
|
+
corrupt = self.root / "bad.json"
|
|
119
|
+
corrupt.write_text("{ not json", encoding="utf-8")
|
|
120
|
+
result = migrate_store.run(src=str(corrupt))
|
|
121
|
+
self.assertEqual(result["corrupt_files"], 1)
|
|
122
|
+
self.assertEqual(result["error"], "corrupt_source")
|
|
123
|
+
self.assertFalse(result["applied"])
|
|
124
|
+
|
|
125
|
+
def test_dry_run_does_not_write(self):
|
|
126
|
+
dst = self.root / "out" / "memory.json"
|
|
127
|
+
result = migrate_store.run(src=str(self.store_file), dst=str(dst), apply=False)
|
|
128
|
+
self.assertTrue(result["ok"])
|
|
129
|
+
self.assertTrue(result["dry_run"])
|
|
130
|
+
self.assertFalse(result["applied"])
|
|
131
|
+
self.assertFalse(dst.exists())
|
|
132
|
+
self.assertEqual(result["converted"], 1)
|
|
133
|
+
|
|
134
|
+
def test_apply_writes_atomically(self):
|
|
135
|
+
dst = self.root / "out" / "memory.json"
|
|
136
|
+
result = migrate_store.run(src=str(self.store_file), dst=str(dst), apply=True)
|
|
137
|
+
self.assertTrue(result["applied"])
|
|
138
|
+
doc = json.loads(dst.read_text(encoding="utf-8"))
|
|
139
|
+
self.assertEqual(len(doc["entries"]), 1)
|
|
140
|
+
self.assertEqual(doc["schema_version"], 1)
|
|
141
|
+
self.assertEqual(list(dst.parent.glob("*.tmp")), [])
|
|
142
|
+
|
|
143
|
+
def test_target_non_empty_requires_force(self):
|
|
144
|
+
dst = self.root / "out" / "memory.json"
|
|
145
|
+
dst.parent.mkdir(parents=True)
|
|
146
|
+
dst.write_text(json.dumps({"entries": [{"id": "keep"}]}), encoding="utf-8")
|
|
147
|
+
blocked = migrate_store.run(src=str(self.store_file), dst=str(dst), apply=True)
|
|
148
|
+
self.assertEqual(blocked["error"], "target_not_empty")
|
|
149
|
+
self.assertFalse(blocked["applied"])
|
|
150
|
+
self.assertIn("keep", dst.read_text(encoding="utf-8"))
|
|
151
|
+
forced = migrate_store.run(src=str(self.store_file), dst=str(dst), apply=True, force=True)
|
|
152
|
+
self.assertTrue(forced["applied"])
|
|
153
|
+
self.assertEqual(len(json.loads(dst.read_text(encoding="utf-8"))["entries"]), 1)
|
|
154
|
+
|
|
155
|
+
def test_source_not_found_reports_error(self):
|
|
156
|
+
with mock.patch.dict(os.environ, {migrate_store.SOURCE_ENV: ""}):
|
|
157
|
+
result = migrate_store.run(src=str(self.root / "missing" / "memory.json"))
|
|
158
|
+
self.assertEqual(result["error"], "source_not_found")
|
|
159
|
+
self.assertFalse(result["ok"])
|
|
160
|
+
|
|
161
|
+
def test_apply_without_target_errors(self):
|
|
162
|
+
result = migrate_store.run(src=str(self.store_file), apply=True)
|
|
163
|
+
self.assertEqual(result["error"], "target_required")
|
|
164
|
+
|
|
165
|
+
def test_sample_contains_three_entries_max(self):
|
|
166
|
+
rows = [entry(id=str(index), text="memory " + str(index)) for index in range(5)]
|
|
167
|
+
src = self.root / "many.json"
|
|
168
|
+
src.write_text(json.dumps({"entries": rows}), encoding="utf-8")
|
|
169
|
+
result = migrate_store.run(src=str(src))
|
|
170
|
+
self.assertEqual(result["converted"], 5)
|
|
171
|
+
self.assertEqual(len(result["sample"]), 3)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class VectorMigrationTest(unittest.TestCase):
|
|
175
|
+
def test_vector_is_kept_with_model(self):
|
|
176
|
+
entries = [{"id": "v1", "text": "hello", "vector": [0.1, 0.2, 0.3], "embedding_model": "m1"}]
|
|
177
|
+
core, report = migrate_store.convert(entries)
|
|
178
|
+
self.assertEqual(core[0]["vector"], [0.1, 0.2, 0.3])
|
|
179
|
+
self.assertEqual(core[0]["vector_model"], "m1")
|
|
180
|
+
self.assertEqual(report["vectors_migrated"], 1)
|
|
181
|
+
self.assertEqual(report["vectors_dropped"], 0)
|
|
182
|
+
|
|
183
|
+
def test_invalid_vector_is_dropped_and_counted(self):
|
|
184
|
+
entries = [{"id": "v2", "text": "hello", "vector": [0.1, "x", 0.3]}]
|
|
185
|
+
core, report = migrate_store.convert(entries)
|
|
186
|
+
self.assertNotIn("vector", core[0])
|
|
187
|
+
self.assertEqual(report["vectors_dropped"], 1)
|
|
188
|
+
self.assertEqual(report["vectors_migrated"], 0)
|
|
189
|
+
|
|
190
|
+
def test_empty_vector_is_dropped(self):
|
|
191
|
+
entries = [{"id": "v3", "text": "hello", "vector": []}]
|
|
192
|
+
core, report = migrate_store.convert(entries)
|
|
193
|
+
self.assertNotIn("vector", core[0])
|
|
194
|
+
self.assertEqual(report["vectors_dropped"], 0)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""HUMAN-4 mind wandering tests: weights, deterministic sampling, rumination bias."""
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from biocore_model import mind_wander as mw
|
|
6
|
+
|
|
7
|
+
LOOPS = [{"topic": "calm", "tension": 0.0}, {"topic": "tense", "tension": 1.0}]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WeightTest(unittest.TestCase):
|
|
11
|
+
def test_weights_positive(self):
|
|
12
|
+
for weight in mw.weights(LOOPS, rumination=0.5):
|
|
13
|
+
self.assertGreater(weight, 0.0)
|
|
14
|
+
|
|
15
|
+
def test_rumination_boosts_tension(self):
|
|
16
|
+
calm, tense = mw.weights(LOOPS, rumination=1.0)
|
|
17
|
+
self.assertGreater(tense, calm)
|
|
18
|
+
|
|
19
|
+
def test_no_rumination_equal_tension(self):
|
|
20
|
+
loops = [{"topic": "a", "tension": 0.5}, {"topic": "b", "tension": 0.5}]
|
|
21
|
+
a, b = mw.weights(loops, rumination=0.0)
|
|
22
|
+
self.assertEqual(a, b)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChooseTest(unittest.TestCase):
|
|
26
|
+
def test_deterministic(self):
|
|
27
|
+
self.assertEqual(mw.choose(LOOPS, 0.5, seed=4, now=9),
|
|
28
|
+
mw.choose(LOOPS, 0.5, seed=4, now=9))
|
|
29
|
+
|
|
30
|
+
def test_fields(self):
|
|
31
|
+
row = mw.choose(LOOPS, 0.0, seed=1, now=1)
|
|
32
|
+
self.assertIn(row["topic"], ("calm", "tense"))
|
|
33
|
+
self.assertIn("weight", row)
|
|
34
|
+
self.assertIn("total", row)
|
|
35
|
+
|
|
36
|
+
def test_empty_returns_none(self):
|
|
37
|
+
self.assertIsNone(mw.choose([], 0.5, seed=1, now=1))
|
|
38
|
+
|
|
39
|
+
def test_single_item_always(self):
|
|
40
|
+
row = mw.choose([{"topic": "only", "tension": 0.1}], 0.9, seed=7, now=7)
|
|
41
|
+
self.assertEqual(row["topic"], "only")
|
|
42
|
+
|
|
43
|
+
def test_rumination_biases_tense(self):
|
|
44
|
+
picks = [mw.choose(LOOPS, 1.0, seed=index, now=index)["topic"]
|
|
45
|
+
for index in range(30)]
|
|
46
|
+
self.assertGreaterEqual(picks.count("tense"), 18)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WanderTest(unittest.TestCase):
|
|
50
|
+
def test_limit_and_distinct(self):
|
|
51
|
+
rows = mw.wander(LOOPS, 0.2, seed=3, now=3, limit=2)
|
|
52
|
+
self.assertEqual(len(rows), 2)
|
|
53
|
+
self.assertNotEqual(rows[0]["topic"], rows[1]["topic"])
|
|
54
|
+
|
|
55
|
+
def test_does_not_mutate(self):
|
|
56
|
+
snapshot = [dict(row) for row in LOOPS]
|
|
57
|
+
mw.wander(LOOPS, 0.5, seed=2, now=2)
|
|
58
|
+
self.assertEqual(LOOPS, snapshot)
|
|
59
|
+
|
|
60
|
+
def test_seeded_helper(self):
|
|
61
|
+
self.assertIn(mw.seeded(5, now=5)["topic"], ("work", "friend", "rest"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
unittest.main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import unittest
|
|
3
|
+
from biocore_model import mood as mood_bio
|
|
4
|
+
|
|
5
|
+
class MoodTest(unittest.TestCase):
|
|
6
|
+
def test_update_toward_target(self):
|
|
7
|
+
m = mood_bio.mood_update({'valence': 0.0, 'arousal': 0.0}, 1.0, 0.0)
|
|
8
|
+
self.assertGreater(m['valence'], 0); self.assertLess(m['valence'], 1)
|
|
9
|
+
def test_rate(self):
|
|
10
|
+
m = mood_bio.mood_update({'valence': 0.0, 'arousal': 0.0}, 1.0, 0.0, rate=0.05)
|
|
11
|
+
self.assertAlmostEqual(m['valence'], 0.05)
|
|
12
|
+
def test_idempotent_same_now(self):
|
|
13
|
+
m = mood_bio.mood_update({'valence': 0.0, 'arousal': 0.0}, 1.0, 0.0, now=100)
|
|
14
|
+
again = mood_bio.mood_update(m, 1.0, 0.0, now=100)
|
|
15
|
+
self.assertEqual(again['valence'], m['valence'])
|
|
16
|
+
def test_label_quadrants(self):
|
|
17
|
+
self.assertEqual(mood_bio.label({'valence': -0.5, 'arousal': 0.8}), '低落')
|
|
18
|
+
self.assertEqual(mood_bio.label({'valence': 0.5, 'arousal': 0.8}), '愉快')
|
|
19
|
+
self.assertEqual(mood_bio.label({'valence': 0.2, 'arousal': 0.2}), '平静偏正')
|
|
20
|
+
self.assertEqual(mood_bio.label({'valence': -0.2, 'arousal': 0.2}), '平静')
|
|
21
|
+
def test_view_fields(self):
|
|
22
|
+
v = mood_bio.mood_view({'valence': 0.1, 'arousal': 0.2})
|
|
23
|
+
for key in ('valence', 'arousal', 'label'): self.assertIn(key, v)
|
|
24
|
+
def test_half_life_ticks(self):
|
|
25
|
+
ticks = mood_bio.ticks_to_target(0.0, 1.0)
|
|
26
|
+
self.assertGreater(ticks, 40); self.assertLess(ticks, 100)
|
|
27
|
+
def test_clamp(self):
|
|
28
|
+
self.assertLessEqual(mood_bio.mood_update({'valence': 1.0}, 5.0, 0.0)['valence'], 1.0)
|
|
29
|
+
self.assertGreaterEqual(mood_bio.mood_update({'valence': -1.0}, -5.0, 0.0)['valence'], -1.0)
|
|
30
|
+
def test_zero_dt_no_change_without_now(self):
|
|
31
|
+
m = {'valence': 0.3, 'arousal': 0.2}
|
|
32
|
+
self.assertEqual(mood_bio.mood_update(m, 0.3, 0.2)['valence'], 0.3)
|
|
33
|
+
|
|
34
|
+
if __name__ == '__main__':
|
|
35
|
+
unittest.main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Narrative: chapters plus the three coherence metrics."""
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from biocore_model import narrative as nv
|
|
6
|
+
|
|
7
|
+
DAY = 86400.0
|
|
8
|
+
BASE = 1790000000.0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ev(kind, text, valence, day=0.0, importance=0.6):
|
|
12
|
+
return {"kind": kind, "text": text, "valence": valence,
|
|
13
|
+
"ts": BASE + day * DAY, "importance": importance}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EntriesTest(unittest.TestCase):
|
|
17
|
+
def test_empty(self):
|
|
18
|
+
self.assertEqual(nv.entries([]), [])
|
|
19
|
+
|
|
20
|
+
def test_theme_mapping(self):
|
|
21
|
+
rows = nv.entries([ev("rupture", "吵了一架", -0.6)])
|
|
22
|
+
self.assertEqual(rows[0]["theme"], "关系裂缝")
|
|
23
|
+
|
|
24
|
+
def test_unknown_kind_falls_back(self):
|
|
25
|
+
rows = nv.entries([ev("weird", "小事", 0.1)])
|
|
26
|
+
self.assertEqual(rows[0]["theme"], nv.DEFAULT_THEME)
|
|
27
|
+
|
|
28
|
+
def test_limit_and_newest_first(self):
|
|
29
|
+
events = [ev("gift", "第%d次" % i, 0.4, day=i) for i in range(10)]
|
|
30
|
+
rows = nv.entries(events, limit=3)
|
|
31
|
+
self.assertEqual(len(rows), 3)
|
|
32
|
+
self.assertEqual(rows[0]["ts"], BASE + 9 * DAY)
|
|
33
|
+
|
|
34
|
+
def test_dedupes_same_text(self):
|
|
35
|
+
events = [ev("gift", "同一句话", 0.4, day=0),
|
|
36
|
+
ev("gift", "同一句话", 0.4, day=1)]
|
|
37
|
+
self.assertEqual(len(nv.entries(events)), 1)
|
|
38
|
+
|
|
39
|
+
def test_malformed_skipped(self):
|
|
40
|
+
rows = nv.entries([None, "x", {"text": ""}, ev("gift", "好", 0.3)])
|
|
41
|
+
self.assertEqual(len(rows), 1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ConsistencyTest(unittest.TestCase):
|
|
45
|
+
def test_empty_is_zero(self):
|
|
46
|
+
self.assertEqual(nv.consistency([]), 0.0)
|
|
47
|
+
|
|
48
|
+
def test_all_positive(self):
|
|
49
|
+
events = [ev("gift", "a", 0.5), ev("gift", "b", 0.3)]
|
|
50
|
+
self.assertEqual(nv.consistency(events), 1.0)
|
|
51
|
+
|
|
52
|
+
def test_mixed_lowers(self):
|
|
53
|
+
events = [ev("gift", "a", 0.5), ev("gift", "b", -0.5)]
|
|
54
|
+
self.assertLess(nv.consistency(events), 1.0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ContradictionTest(unittest.TestCase):
|
|
58
|
+
def test_none_when_same_sign(self):
|
|
59
|
+
events = [ev("rupture", "a", -0.5), ev("rupture", "b", -0.2)]
|
|
60
|
+
self.assertEqual(nv.contradiction(events), 0.0)
|
|
61
|
+
|
|
62
|
+
def test_detects_opposites(self):
|
|
63
|
+
events = [ev("rupture", "a", -0.5), ev("rupture", "b", 0.4)]
|
|
64
|
+
self.assertEqual(nv.contradiction(events), 1.0)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RepairTest(unittest.TestCase):
|
|
68
|
+
def test_none_without_negative(self):
|
|
69
|
+
self.assertEqual(nv.repair([ev("gift", "a", 0.5)]), 0.0)
|
|
70
|
+
|
|
71
|
+
def test_repair_within_window(self):
|
|
72
|
+
events = [ev("rupture", "吵", -0.6, day=0),
|
|
73
|
+
ev("repair", "和好", 0.5, day=2)]
|
|
74
|
+
self.assertEqual(nv.repair(events), 1.0)
|
|
75
|
+
|
|
76
|
+
def test_no_repair_outside_window(self):
|
|
77
|
+
events = [ev("rupture", "吵", -0.6, day=0),
|
|
78
|
+
ev("repair", "和好", 0.5, day=30)]
|
|
79
|
+
self.assertEqual(nv.repair(events), 0.0)
|
|
80
|
+
|
|
81
|
+
def test_repair_needs_same_theme(self):
|
|
82
|
+
events = [ev("rupture", "吵", -0.6, day=0),
|
|
83
|
+
ev("gift", "收到礼物", 0.5, day=1)]
|
|
84
|
+
self.assertEqual(nv.repair(events), 0.0)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SummaryTest(unittest.TestCase):
|
|
88
|
+
def test_summary_shape(self):
|
|
89
|
+
events = [ev("rupture", "吵", -0.6, day=0), ev("repair", "和好", 0.5, day=1)]
|
|
90
|
+
out = nv.summarize(events)
|
|
91
|
+
self.assertEqual(out["events"], 2)
|
|
92
|
+
self.assertIn("consistency", out["metrics"])
|
|
93
|
+
self.assertIn("contradiction", out["metrics"])
|
|
94
|
+
self.assertIn("repair", out["metrics"])
|
|
95
|
+
self.assertTrue(out["entries"])
|
|
96
|
+
|
|
97
|
+
def test_disabled(self):
|
|
98
|
+
out = nv.summarize([ev("gift", "a", 0.5)], flags={"enabled": False})
|
|
99
|
+
self.assertEqual(out["entries"], [])
|
|
100
|
+
self.assertEqual(out["metrics"]["consistency"], 0.0)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class DirectiveTest(unittest.TestCase):
|
|
104
|
+
def test_contradiction_invites_clarify(self):
|
|
105
|
+
out = nv.directives({"enabled": True, "metrics": {"contradiction": 0.9}})
|
|
106
|
+
self.assertTrue(out["allow_clarify"])
|
|
107
|
+
|
|
108
|
+
def test_clean_story_no_prompt(self):
|
|
109
|
+
out = nv.directives({"enabled": True, "metrics": {"contradiction": 0.0}})
|
|
110
|
+
self.assertFalse(out["allow_clarify"])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
unittest.main()
|