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.
Files changed (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,174 @@
1
+ # -*- coding: utf-8 -*-
2
+ """P0-3 本轮记忆可见: turn_memory receipt -> dashboard card projection.
3
+
4
+ The view logic lives in pages/dashboard/turn_memory_view.js (pure functions);
5
+ these tests execute it with node and also pin the dashboard wiring + the
6
+ backend receipt contract.
7
+ """
8
+ import json
9
+ import shutil
10
+ import subprocess
11
+ import tempfile
12
+ import unittest
13
+ from pathlib import Path
14
+
15
+ from biocore_model import turn_memory as tm
16
+
17
+ ROOT = Path(__file__).resolve().parents[2]
18
+ DASH = ROOT / "pages" / "dashboard"
19
+ VIEW = DASH / "turn_memory_view.js"
20
+ APP = DASH / "app.js"
21
+ SRC_HTML = DASH / "index.src.html"
22
+ INDEX_HTML = DASH / "index.html"
23
+ MAIN = ROOT / "main.py"
24
+ ROUTES = ROOT / "biocore_model" / "plugin_routes.py"
25
+
26
+ NODE = shutil.which("node")
27
+
28
+ RECORD = {
29
+ "ts": 1800000000.0,
30
+ "reply": "好,知道了",
31
+ "count": 4,
32
+ "rows": [
33
+ {"rank": 1, "id": "e1", "text": "她周五去体检",
34
+ "source": "lexical", "tier": "hot", "score": 0.8123,
35
+ "reason": "关键词 0.40 / 近期 0.20"},
36
+ {"rank": 2, "id": "e2", "text": "老板张总",
37
+ "source": "graph", "tier": "warm", "score": 0.5,
38
+ "reason": "规则打分"},
39
+ {"rank": 3, "id": "e3", "text": "常驻事实:她在医院工作",
40
+ "source": "resident", "tier": "hot", "score": 0.4},
41
+ {"rank": 4, "id": "e4", "text": "上次聊到奶茶",
42
+ "source": "retrieval", "tier": "cold", "score": 0.2,
43
+ "reason": "留存 0.10"},
44
+ ],
45
+ }
46
+
47
+
48
+ def _node_view(payload):
49
+ """Run turn_memory_view.js against *payload* and return its projections."""
50
+ script = (
51
+ "require(" + json.dumps(str(VIEW)) + ");"
52
+ "const v=globalThis.BioTurnMemoryView;"
53
+ "const p=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));"
54
+ "process.stdout.write(JSON.stringify({"
55
+ "summary:v.summary(p),all:v.rows(p),two:v.rows(p,2),"
56
+ "zero:v.rows(p,0),none:v.summary(null)}));"
57
+ )
58
+ with tempfile.TemporaryDirectory() as tmp:
59
+ path = Path(tmp) / "payload.json"
60
+ path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
61
+ done = subprocess.run([NODE, "-e", script, str(path)],
62
+ capture_output=True, text=True,
63
+ encoding="utf-8", errors="replace", timeout=60)
64
+ if done.returncode != 0:
65
+ raise AssertionError("node view failed: " + done.stderr[-800:])
66
+ return json.loads(done.stdout)
67
+
68
+
69
+ @unittest.skipUnless(NODE, "node is required for the dashboard view tests")
70
+ class ViewProjectionTest(unittest.TestCase):
71
+ def test_four_rows_keep_source_and_reason(self):
72
+ out = _node_view(RECORD)
73
+ rows = out["summary"]["rows"]
74
+ self.assertEqual(len(rows), 4)
75
+ self.assertEqual([row["source"] for row in rows],
76
+ ["lexical", "graph", "resident", "retrieval"])
77
+ self.assertEqual(rows[0]["reason"], "关键词 0.40 / 近期 0.20")
78
+ self.assertEqual(rows[2]["reason"], "规则打分") # default filled in
79
+ self.assertEqual(rows[0]["rank"], 1)
80
+ self.assertEqual(rows[0]["id"], "e1")
81
+ self.assertAlmostEqual(rows[0]["score"], 0.8123)
82
+ self.assertEqual(out["summary"]["count"], 4)
83
+ self.assertEqual(out["summary"]["total"], 4)
84
+
85
+ def test_never_more_than_four_rows(self):
86
+ payload = dict(RECORD)
87
+ payload["rows"] = RECORD["rows"] + [
88
+ {"rank": 5, "id": "e5", "text": "第五条", "source": "lexical"},
89
+ {"rank": 6, "id": "e6", "text": "第六条", "source": "lexical"},
90
+ ]
91
+ out = _node_view(payload)
92
+ self.assertEqual(len(out["all"]), 4)
93
+ self.assertEqual(len(out["summary"]["rows"]), 4)
94
+ self.assertEqual([row["id"] for row in out["all"]],
95
+ ["e1", "e2", "e3", "e4"])
96
+
97
+ def test_empty_or_broken_record_is_safe(self):
98
+ out = _node_view({"rows": "nope"})
99
+ self.assertEqual(out["all"], [])
100
+ self.assertEqual(out["summary"]["count"], 0)
101
+ self.assertEqual(out["none"]["rows"], [])
102
+ self.assertEqual(out["none"]["count"], 0)
103
+ # rows without id and text are dropped
104
+ out = _node_view({"rows": [{"source": "x"}, {}]})
105
+ self.assertEqual(out["all"], [])
106
+
107
+ def test_limit_argument(self):
108
+ out = _node_view(RECORD)
109
+ self.assertEqual(len(out["two"]), 2)
110
+ self.assertEqual(out["zero"], [])
111
+ self.assertEqual(out["two"][0]["id"], "e1")
112
+
113
+
114
+ class ReceiptContractTest(unittest.TestCase):
115
+ def test_build_record_keeps_four_sources(self):
116
+ trace = [
117
+ {"entry_id": "a1", "text": "她周五体检", "source": "lexical",
118
+ "tier": "hot", "score": 0.8,
119
+ "components": {"lexical": 0.4, "recency": 0.2}},
120
+ {"entry_id": "a2", "text": "老板张总", "source": "graph",
121
+ "tier": "warm", "score": 0.5},
122
+ {"entry_id": "a3", "text": "常驻事实", "source": "resident",
123
+ "tier": "hot", "score": 0.4},
124
+ {"entry_id": "a4", "text": "上次聊到奶茶", "source": "retrieval",
125
+ "tier": "cold", "score": 0.2},
126
+ ]
127
+ rows = tm.build(trace, limit=4)
128
+ record = tm.build_record(rows, reply="ok", ts=1.0)
129
+ self.assertEqual(record["count"], 4)
130
+ for row in record["rows"]:
131
+ self.assertTrue(row["source"])
132
+ project = _node_panel(record) if NODE else None
133
+ if project is not None:
134
+ self.assertEqual(project["count"], 4)
135
+ self.assertEqual(len({row["source"] for row in project["rows"]}), 4)
136
+
137
+
138
+ def _node_panel(record):
139
+ out = _node_view(record)
140
+ return out["summary"]
141
+
142
+
143
+ class DashboardWiringTest(unittest.TestCase):
144
+ def test_status_payload_exposes_turn_memory(self):
145
+ for path in (MAIN, ROUTES):
146
+ text = path.read_text(encoding="utf-8")
147
+ self.assertIn('"turn_memory"', text)
148
+ self.assertIn("_last_turn_memory", text)
149
+
150
+ def test_source_markup_and_renderer_present(self):
151
+ src = SRC_HTML.read_text(encoding="utf-8")
152
+ self.assertIn('id="turnMemoryList"', src)
153
+ self.assertIn('id="turnMemoryNote"', src)
154
+ self.assertIn("turn_memory_view.js", src)
155
+ app = APP.read_text(encoding="utf-8")
156
+ self.assertIn("function renderTurnMemory()", app)
157
+ self.assertIn("BioTurnMemoryView", app)
158
+ self.assertIn("data&&data.turn_memory", app)
159
+ self.assertIn("renderTurnMemory()", app)
160
+
161
+ def test_inlined_index_contains_view_module(self):
162
+ html = INDEX_HTML.read_text(encoding="utf-8")
163
+ self.assertIn('id="turnMemoryList"', html)
164
+ self.assertIn("BioTurnMemoryView", html)
165
+ self.assertIn("root.BioTurnMemoryView", html)
166
+
167
+ def test_inliner_builds_the_view_module(self):
168
+ inliner = (ROOT / "scripts" / "inline-assets.mjs").read_text(
169
+ encoding="utf-8")
170
+ self.assertIn("turn_memory_view.js", inliner)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ unittest.main()
@@ -0,0 +1,84 @@
1
+ # -*- coding: utf-8 -*-
2
+ import json, tempfile, unittest
3
+ from pathlib import Path
4
+ from biocore_model import ui_settings
5
+
6
+
7
+ class FakePlugin:
8
+ def __init__(self, data_dir):
9
+ self.data_dir = data_dir
10
+ self.reply_soft_limit_enabled = None
11
+ self.reply_soft_target_chars = None
12
+ self.reply_soft_max_chars = None
13
+ self.reply_split_enabled = None
14
+ self.reply_split_mode = None
15
+ self.reply_split_max_chars = None
16
+ self.proactive_autonomy = None
17
+ self.proactive_send = None
18
+
19
+
20
+ class UiSettingsTest(unittest.TestCase):
21
+ def test_defaults_when_missing(self):
22
+ with tempfile.TemporaryDirectory() as tmp:
23
+ data = ui_settings.load(FakePlugin(tmp))
24
+ self.assertTrue(data["reply_soft"]["enabled"]); self.assertEqual(data["reply_split"]["mode"], "astrbot")
25
+
26
+ def test_save_then_load_roundtrip(self):
27
+ with tempfile.TemporaryDirectory() as tmp:
28
+ plugin = FakePlugin(tmp)
29
+ result = ui_settings.save(plugin, {"reply_split": {"enabled": True, "mode": "astrbot"}, "reply_soft": {"target_chars": 10, "max_chars": 30}})
30
+ self.assertTrue(result["ok"])
31
+ data = ui_settings.load(plugin)
32
+ self.assertTrue(data["reply_split"]["enabled"]); self.assertEqual(data["reply_split"]["mode"], "astrbot")
33
+ self.assertEqual(data["reply_soft"]["max_chars"], 30)
34
+
35
+ def test_apply_to_plugin_overrides_runtime(self):
36
+ with tempfile.TemporaryDirectory() as tmp:
37
+ plugin = FakePlugin(tmp)
38
+ ui_settings.save(plugin, {"reply_split": {"enabled": True, "mode": "astrbot"}, "autonomy": {"enabled": True, "send": True}})
39
+ ui_settings.apply_to_plugin(plugin)
40
+ self.assertTrue(plugin.reply_split_enabled); self.assertEqual(plugin.reply_split_mode, "astrbot")
41
+ self.assertTrue(plugin.proactive_autonomy); self.assertTrue(plugin.proactive_send)
42
+
43
+ def test_survives_simulated_restart(self):
44
+ with tempfile.TemporaryDirectory() as tmp:
45
+ first = FakePlugin(tmp)
46
+ ui_settings.save(first, {"reply_split": {"enabled": True, "mode": "astrbot"}, "reply_soft": {"target_chars": 10, "max_chars": 30}})
47
+ second = FakePlugin(tmp) # fresh instance = restart
48
+ ui_settings.apply_to_plugin(second)
49
+ self.assertTrue(second.reply_split_enabled); self.assertEqual(second.reply_split_mode, "astrbot")
50
+ self.assertEqual(second.reply_soft_target_chars, 10); self.assertEqual(second.reply_soft_max_chars, 30)
51
+
52
+ def test_corrupt_file_falls_back(self):
53
+ with tempfile.TemporaryDirectory() as tmp:
54
+ plugin = FakePlugin(tmp)
55
+ ui_settings.settings_path(plugin).write_text("{not json", encoding="utf-8")
56
+ data = ui_settings.load(plugin)
57
+ self.assertTrue(data["reply_soft"]["enabled"])
58
+
59
+ def test_backups_rotated_to_three(self):
60
+ with tempfile.TemporaryDirectory() as tmp:
61
+ plugin = FakePlugin(tmp)
62
+ for index in range(6):
63
+ ui_settings.save(plugin, {"reply_split": {"max_chars": 200 + index}})
64
+ backups = list(Path(tmp).glob("ui_settings.json.bak_*"))
65
+ self.assertLessEqual(len(backups), 3)
66
+
67
+ def test_accumulator_v_survives_restart(self):
68
+ with tempfile.TemporaryDirectory() as tmp:
69
+ first = FakePlugin(tmp)
70
+ ui_settings.save(first, {"autonomy": {"accumulator": {"V": 0.42, "ticks_since_fire": 7}}})
71
+ data = ui_settings.load(FakePlugin(tmp))
72
+ self.assertAlmostEqual(data["autonomy"]["accumulator"]["V"], 0.42)
73
+ self.assertEqual(data["autonomy"]["accumulator"]["ticks_since_fire"], 7)
74
+
75
+ def test_schema_written(self):
76
+ with tempfile.TemporaryDirectory() as tmp:
77
+ plugin = FakePlugin(tmp)
78
+ ui_settings.save(plugin, {"autonomy": {"enabled": True}})
79
+ raw = json.loads(ui_settings.settings_path(plugin).read_text(encoding="utf-8"))
80
+ self.assertEqual(raw["schema"], 1)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ unittest.main()
@@ -0,0 +1,179 @@
1
+ class _Recorder:
2
+ """Logger stub so tests never import the stdlib logging module."""
3
+
4
+ def __init__(self):
5
+ self.lines = []
6
+ self.level = 0
7
+
8
+ def _log(self, msg, *args, **kwargs):
9
+ try:
10
+ self.lines.append(str(msg) % args if args else str(msg))
11
+ except Exception:
12
+ self.lines.append(str(msg))
13
+
14
+ def setLevel(self, *_a, **_k):
15
+ return None
16
+
17
+ def addHandler(self, *_a, **_k):
18
+ return None
19
+
20
+ def removeHandler(self, *_a, **_k):
21
+ return None
22
+
23
+ debug = info = warning = error = exception = critical = _log
24
+
25
+
26
+ # -*- coding: utf-8 -*-
27
+ """ui_settings single-authority tests: path, migration, idempotency, roundtrip."""
28
+ import json
29
+ import os
30
+ import tempfile
31
+ import unittest
32
+ from pathlib import Path
33
+ from unittest import mock
34
+
35
+ from biocore_model import ui_settings as ui
36
+
37
+ ENV_DIR = "BIOCORE_UI_SETTINGS_DIR"
38
+ ENV_LEGACY = "BIOCORE_UI_SETTINGS_LEGACY"
39
+
40
+
41
+ class FakePlugin:
42
+ def __init__(self, data_dir):
43
+ self.data_dir = str(data_dir)
44
+
45
+
46
+ class AuthorityPathTest(unittest.TestCase):
47
+ def setUp(self):
48
+ ui._MIGRATED.clear()
49
+
50
+ def tearDown(self):
51
+ ui._MIGRATED.clear()
52
+
53
+ def test_two_plugin_objects_share_one_path(self):
54
+ with tempfile.TemporaryDirectory() as tmp:
55
+ first = ui.settings_path(FakePlugin(tmp))
56
+ second = ui.settings_path(FakePlugin(tmp))
57
+ self.assertEqual(first, second)
58
+ self.assertEqual(first, Path(tmp) / ui.FILE_NAME)
59
+
60
+ def test_plugins_layout_maps_to_plugin_data(self):
61
+ with tempfile.TemporaryDirectory() as tmp:
62
+ legacy = Path(tmp) / "plugins" / "astrbot_plugin_biocore_standalone" / "data"
63
+ legacy.mkdir(parents=True)
64
+ path = ui.settings_path(FakePlugin(legacy))
65
+ self.assertEqual(path, Path(tmp) / "plugin_data" / "astrbot_plugin_biocore" / ui.FILE_NAME)
66
+
67
+ def test_env_override_wins_for_every_plugin(self):
68
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as other:
69
+ with mock.patch.dict(os.environ, {ENV_DIR: tmp}):
70
+ a = ui.settings_path(FakePlugin(other))
71
+ b = ui.settings_path(FakePlugin("/somewhere/else"))
72
+ self.assertEqual(a, b)
73
+ self.assertEqual(a, Path(tmp) / ui.FILE_NAME)
74
+
75
+ def test_canonical_plugin_data_dir_is_kept(self):
76
+ with tempfile.TemporaryDirectory() as tmp:
77
+ canonical = Path(tmp) / "plugin_data" / "astrbot_plugin_biocore_standalone"
78
+ canonical.mkdir(parents=True)
79
+ path = ui.settings_path(FakePlugin(canonical))
80
+ self.assertEqual(path, Path(tmp) / "plugin_data" / "astrbot_plugin_biocore" / ui.FILE_NAME)
81
+
82
+
83
+ class MigrationTest(unittest.TestCase):
84
+ def setUp(self):
85
+ self.tmp = tempfile.TemporaryDirectory()
86
+ self.authority_dir = Path(self.tmp.name) / "plugin_data"
87
+ self.legacy = Path(self.tmp.name) / "legacy_ui_settings.json"
88
+ self.env = mock.patch.dict(os.environ, {
89
+ ENV_DIR: str(self.authority_dir),
90
+ ENV_LEGACY: str(self.legacy),
91
+ })
92
+ self.env.start()
93
+ ui._MIGRATED.clear()
94
+
95
+ def tearDown(self):
96
+ ui._MIGRATED.clear()
97
+ self.env.stop()
98
+ self.tmp.cleanup()
99
+
100
+ def _authority_file(self):
101
+ return self.authority_dir / ui.FILE_NAME
102
+
103
+ def test_a_to_b_migration_merges_keys(self):
104
+ self.legacy.write_text(json.dumps({"reply_soft": {"max_chars": 77},
105
+ "custom": {"x": 1}}), encoding="utf-8")
106
+ data = ui.load(FakePlugin(self.tmp.name))
107
+ self.assertEqual(data["reply_soft"]["max_chars"], 77)
108
+ raw = json.loads(self._authority_file().read_text(encoding="utf-8"))
109
+ self.assertEqual(raw["custom"]["x"], 1)
110
+ self.assertEqual(raw["schema"], 1)
111
+
112
+ def test_migration_logged_once(self):
113
+ self.legacy.write_text(json.dumps({"reply_soft": {"max_chars": 40}}), encoding="utf-8")
114
+ rec = _Recorder()
115
+ saved = ui.logger
116
+ ui.logger = rec
117
+ try:
118
+ ui.load(FakePlugin(self.tmp.name))
119
+ ui.load(FakePlugin(self.tmp.name))
120
+ finally:
121
+ ui.logger = saved
122
+ self.assertEqual(rec.lines.count(ui.MIGRATION_LOG), 1)
123
+
124
+ def test_migration_idempotent(self):
125
+ self.legacy.write_text(json.dumps({"reply_soft": {"max_chars": 55}}), encoding="utf-8")
126
+ first = ui.load(FakePlugin(self.tmp.name))
127
+ second = ui.load(FakePlugin(self.tmp.name))
128
+ self.assertEqual(first["reply_soft"]["max_chars"], 55)
129
+ self.assertEqual(second["reply_soft"]["max_chars"], 55)
130
+
131
+ def test_newer_authority_is_not_overwritten(self):
132
+ self._authority_file().parent.mkdir(parents=True, exist_ok=True)
133
+ self._authority_file().write_text(json.dumps({"reply_soft": {"max_chars": 30}}), encoding="utf-8")
134
+ self.legacy.write_text(json.dumps({"reply_soft": {"max_chars": 77}}), encoding="utf-8")
135
+ os.utime(self._authority_file(), (2_000, 2_000))
136
+ os.utime(self.legacy, (1_000, 1_000))
137
+ data = ui.load(FakePlugin(self.tmp.name))
138
+ self.assertEqual(data["reply_soft"]["max_chars"], 30)
139
+
140
+ def test_newer_legacy_is_merged(self):
141
+ self._authority_file().parent.mkdir(parents=True, exist_ok=True)
142
+ self._authority_file().write_text(json.dumps({"reply_soft": {"max_chars": 30},
143
+ "keep": {"k": 1}}), encoding="utf-8")
144
+ self.legacy.write_text(json.dumps({"reply_soft": {"max_chars": 77}}), encoding="utf-8")
145
+ os.utime(self._authority_file(), (1_000, 1_000))
146
+ os.utime(self.legacy, (2_000, 2_000))
147
+ data = ui.load(FakePlugin(self.tmp.name))
148
+ self.assertEqual(data["reply_soft"]["max_chars"], 77)
149
+ raw = json.loads(self._authority_file().read_text(encoding="utf-8"))
150
+ self.assertEqual(raw["keep"]["k"], 1)
151
+
152
+ def test_legacy_is_never_read_again(self):
153
+ self.legacy.write_text(json.dumps({"reply_soft": {"max_chars": 44}}), encoding="utf-8")
154
+ ui.load(FakePlugin(self.tmp.name))
155
+ self.legacy.write_text(json.dumps({"reply_soft": {"max_chars": 99}}), encoding="utf-8")
156
+ data = ui.load(FakePlugin(self.tmp.name))
157
+ self.assertEqual(data["reply_soft"]["max_chars"], 44)
158
+
159
+ def test_missing_legacy_does_not_crash(self):
160
+ data = ui.load(FakePlugin(self.tmp.name))
161
+ self.assertTrue(data["reply_soft"]["enabled"])
162
+ self.assertFalse(self._authority_file().exists())
163
+
164
+ def test_corrupt_legacy_does_not_crash(self):
165
+ self.legacy.write_text("{not json", encoding="utf-8")
166
+ data = ui.load(FakePlugin(self.tmp.name))
167
+ self.assertTrue(data["reply_soft"]["enabled"])
168
+
169
+ def test_save_load_roundtrip_two_plugins(self):
170
+ first = FakePlugin(self.tmp.name)
171
+ self.assertTrue(ui.save(first, {"reply_soft": {"max_chars": 25}})["ok"])
172
+ second = FakePlugin("/elsewhere")
173
+ self.assertEqual(ui.load(second)["reply_soft"]["max_chars"], 25)
174
+ self.assertTrue(self._authority_file().is_file())
175
+ self.assertFalse((Path("/elsewhere") / ui.FILE_NAME).exists())
176
+
177
+
178
+ if __name__ == "__main__":
179
+ unittest.main()
@@ -0,0 +1,155 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Async user appraisal: non-blocking schedule, idempotency, fallback."""
3
+ import ast
4
+ import asyncio
5
+ import json
6
+ import time
7
+ import types
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ import interpretation
12
+
13
+ MAIN = Path(__file__).resolve().parents[2] / "main.py"
14
+ SOURCE = MAIN.read_text(encoding="utf-8")
15
+
16
+
17
+ def _load_methods():
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
+ wanted = ("_user_appraisal_key", "_user_appraisal_immediate",
22
+ "_schedule_user_appraisal", "_lexical_user_feedback",
23
+ "_user_appraisal_async")
24
+ log = []
25
+ ns = {"asyncio": asyncio, "time": time, "interpretation": interpretation,
26
+ "logger": types.SimpleNamespace(
27
+ info=lambda *a, **k: log.append(a),
28
+ warning=lambda *a, **k: None)}
29
+ fns = {}
30
+ for node in cls.body:
31
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in wanted:
32
+ _cls = (ast.AsyncFunctionDef if isinstance(node, ast.AsyncFunctionDef)
33
+ else ast.FunctionDef)
34
+ clone = _cls(name=node.name, args=node.args, body=node.body,
35
+ decorator_list=[], returns=node.returns,
36
+ type_comment=None, lineno=node.lineno,
37
+ col_offset=node.col_offset,
38
+ end_lineno=node.end_lineno,
39
+ end_col_offset=node.end_col_offset)
40
+ exec(compile(ast.Module(body=[clone], type_ignores=[]), "<uappr>", "exec"), ns)
41
+ fns[node.name] = ns[node.name]
42
+ return fns, log
43
+
44
+
45
+ FNS, LOG = _load_methods()
46
+ DIMS_RAW = ('{"dims":{"goal":0.7,"blame":0.1,"control":0.6,"certainty":0.6,'
47
+ '"novelty":0.2,"involve":0.8,"hit":0.0},"why":"夸","c":0.9}')
48
+
49
+
50
+ class FakeCore:
51
+ def __init__(self):
52
+ self._snapshot = {"v": 0.0, "a": 0.4, "stress": 0.1, "social": 0.5,
53
+ "trust": 0.5, "self_esteem": 0.5}
54
+ self.needs = {"belonging": 0.6, "self_esteem": 0.6,
55
+ "control": 0.6, "meaningful_existence": 0.6}
56
+ self.loaded = []
57
+
58
+ def snapshot(self):
59
+ return dict(self._snapshot)
60
+
61
+ def load_state(self, state):
62
+ self.loaded.append(dict(state))
63
+ self._snapshot.update(state)
64
+
65
+
66
+ class FakePlugin:
67
+ def __init__(self, raw=DIMS_RAW):
68
+ self.raw = raw
69
+ self.llm_calls = 0
70
+ self.lexical_fallback_enabled = True
71
+ self._last_insult_ts = 0.0
72
+ self._appraisal_seen = {}
73
+ self._user_appraisal_task = None
74
+ self._user_appraisal_async = types.MethodType(
75
+ FNS["_user_appraisal_async"], self)
76
+ for name in ("_user_appraisal_key", "_user_appraisal_immediate",
77
+ "_schedule_user_appraisal", "_lexical_user_feedback"):
78
+ setattr(self, name, types.MethodType(FNS[name], self))
79
+
80
+ async def _llm_v2_call(self, kind, prompt, parser, event=None, timeout_ms=None):
81
+ self.llm_calls += 1
82
+ if self.raw is None:
83
+ return None
84
+ return self.raw
85
+
86
+
87
+ class ScheduleTest(unittest.IsolatedAsyncioTestCase):
88
+ async def test_schedule_does_not_block(self):
89
+ plugin = FakePlugin()
90
+
91
+ async def slow(*args, **kwargs):
92
+ await asyncio.sleep(0.3)
93
+ plugin._user_appraisal_async = slow
94
+ started = time.time()
95
+ task = plugin._schedule_user_appraisal(object(), FakeCore(), "夸她")
96
+ elapsed = time.time() - started
97
+ self.assertLess(elapsed, 0.05)
98
+ self.assertIsNotNone(task)
99
+ await asyncio.wait_for(task, timeout=1)
100
+
101
+ async def test_insult_cooling_off_by_default(self):
102
+ plugin = FakePlugin()
103
+ plugin.lexical_fallback_enabled = False
104
+ plugin._user_appraisal_immediate(object(), "你真废物,闭嘴")
105
+ # item-5: no word-table insult cooling; the probe owns semantics.
106
+ self.assertEqual(plugin._last_insult_ts, 0.0)
107
+
108
+ async def test_async_result_writes_once(self):
109
+ plugin = FakePlugin()
110
+ core = FakeCore()
111
+ event = types.SimpleNamespace(unified_msg_origin="u1", message_id="m1")
112
+ key = plugin._user_appraisal_key(event)
113
+ self.assertTrue(await plugin._user_appraisal_async(event, core, "夸她", key))
114
+ self.assertTrue(core.loaded)
115
+ self.assertIn(key, plugin._appraisal_seen)
116
+
117
+ async def test_idempotent_key(self):
118
+ plugin = FakePlugin()
119
+ core = FakeCore()
120
+ event = types.SimpleNamespace(unified_msg_origin="u1", message_id="m2")
121
+ key = plugin._user_appraisal_key(event)
122
+ self.assertTrue(await plugin._user_appraisal_async(event, core, "夸她", key))
123
+ writes = len(core.loaded)
124
+ self.assertFalse(await plugin._user_appraisal_async(event, core, "夸她", key))
125
+ self.assertEqual(len(core.loaded), writes)
126
+ self.assertEqual(plugin.llm_calls, 1)
127
+
128
+ async def test_dims_failure_falls_back_to_words(self):
129
+ plugin = FakePlugin(raw=None)
130
+ core = FakeCore()
131
+ event = types.SimpleNamespace(unified_msg_origin="u1", message_id="m3")
132
+ key = plugin._user_appraisal_key(event)
133
+ self.assertFalse(await plugin._user_appraisal_async(event, core, "你真废物", key))
134
+ self.assertIsNotNone(core.loaded)
135
+
136
+ async def test_switch_off_disables_fallback(self):
137
+ plugin = FakePlugin(raw=None)
138
+ plugin.lexical_fallback_enabled = False
139
+ core = FakeCore()
140
+ event = types.SimpleNamespace(unified_msg_origin="u1", message_id="m4")
141
+ key = plugin._user_appraisal_key(event)
142
+ self.assertFalse(await plugin._user_appraisal_async(event, core, "你真废物", key))
143
+ self.assertFalse(core.loaded)
144
+
145
+
146
+ class WiringStaticTest(unittest.TestCase):
147
+ def test_async_log_and_schedule(self):
148
+ self.assertIn("async=True", SOURCE)
149
+ self.assertIn("self._user_appraisal_immediate(event, text)", SOURCE)
150
+ self.assertNotIn("self._schedule_user_appraisal(event, core, text)", SOURCE)
151
+ self.assertIn("_user_appraisal_async", SOURCE)
152
+
153
+
154
+ if __name__ == "__main__":
155
+ unittest.main()