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,185 @@
1
+ # -*- coding: utf-8 -*-
2
+ """FB-5 feedback_flow tests: three channels, guard-only writes, idempotency."""
3
+ import re
4
+ import unittest
5
+ from pathlib import Path
6
+
7
+ from biocore_model import feedback_flow as ff
8
+
9
+
10
+ class FakeCore:
11
+ def __init__(self, state=None):
12
+ self._snapshot = dict(state or {"v": 0.0, "a": 0.4, "stress": 0.1,
13
+ "self_esteem": 0.5, "social": 0.5})
14
+ self.loaded = []
15
+
16
+ def snapshot(self):
17
+ return dict(self._snapshot)
18
+
19
+ def load_state(self, state):
20
+ self.loaded.append(dict(state))
21
+ self._snapshot.update(state)
22
+
23
+
24
+ class StrictCore(FakeCore):
25
+ """Forbids direct affect writes: only the guard-produced load_state path."""
26
+
27
+ ALLOWED = {"_snapshot", "loaded", "_state"}
28
+
29
+ def __setattr__(self, name, value):
30
+ if name in self.ALLOWED or name.startswith("_"):
31
+ object.__setattr__(self, name, value)
32
+ return
33
+ raise AssertionError("direct core attribute write: " + name)
34
+
35
+
36
+ class Base(unittest.TestCase):
37
+ def setUp(self):
38
+ ff.reset_seen()
39
+ self.core = FakeCore()
40
+
41
+ def tearDown(self):
42
+ ff.reset_seen()
43
+
44
+
45
+ class LlmChannelTest(Base):
46
+ def test_struct_and_summary(self):
47
+ out = ff.apply_llm({"v": 0.9, "a": 0.9, "stress": 0.9}, self.core, now=1000.0)
48
+ self.assertIn("v", out["deltas"])
49
+ self.assertIsInstance(out["rejected"], list)
50
+ self.assertIsInstance(out["audit"], list)
51
+ self.assertEqual(set(out["summary"]), set(ff.SUMMARY_FIELDS))
52
+ for field in ff.SUMMARY_FIELDS:
53
+ self.assertEqual(set(out["summary"][field]), {"old", "new", "delta"})
54
+ self.assertGreater(out["summary"]["v"]["new"], out["summary"]["v"]["old"])
55
+ self.assertEqual(self.core.loaded[-1]["v"], out["summary"]["v"]["new"])
56
+
57
+ def test_whitelist_rejection(self):
58
+ out = ff.apply_llm({"nope": 1.0}, self.core, now=1001.0)
59
+ self.assertEqual(out["deltas"], {})
60
+ self.assertIn("nope", out["rejected"])
61
+
62
+ def test_idempotent_same_payload_now(self):
63
+ first = ff.apply_llm({"v": 0.3}, self.core, now=1002.0)
64
+ snapshot = self.core.snapshot()
65
+ loaded = len(self.core.loaded)
66
+ second = ff.apply_llm({"v": 0.3}, self.core, now=1002.0)
67
+ self.assertTrue(second["idempotent"])
68
+ self.assertEqual(second["deltas"], {})
69
+ self.assertEqual(self.core.snapshot(), snapshot)
70
+ self.assertEqual(len(self.core.loaded), loaded)
71
+ self.assertEqual(second["summary"]["v"]["old"], second["summary"]["v"]["new"])
72
+ self.assertGreater(first["summary"]["v"]["new"], first["summary"]["v"]["old"])
73
+
74
+ def test_new_now_applies_again(self):
75
+ ff.apply_llm({"v": 0.3}, self.core, now=1003.0)
76
+ again = ff.apply_llm({"v": 0.3}, self.core, now=1004.0)
77
+ self.assertFalse(again["idempotent"])
78
+ self.assertGreater(again["summary"]["v"]["new"], again["summary"]["v"]["old"])
79
+
80
+ def test_caps_come_from_feedback_llm(self):
81
+ out = ff.apply_llm({"v": 1.0}, FakeCore({"v": 0.0}), now=1005.0)
82
+ self.assertLessEqual(out["deltas"].get("v", 0.0), 0.15 + 1e-9)
83
+
84
+
85
+ class UserChannelTest(Base):
86
+ def test_struct_and_summary(self):
87
+ out = ff.apply_user({"kind": "praise", "id": "m1", "ts": 1.0},
88
+ self.core, now=2000.0)
89
+ self.assertIn("self_esteem", out["deltas"])
90
+ self.assertEqual(set(out["summary"]), set(ff.SUMMARY_FIELDS))
91
+ self.assertGreater(out["summary"]["self_esteem"]["new"],
92
+ out["summary"]["self_esteem"]["old"])
93
+
94
+ def test_invalid_kind_rejected(self):
95
+ out = ff.apply_user({"kind": "not_a_kind"}, self.core, now=2001.0)
96
+ self.assertEqual(out["deltas"], {})
97
+ self.assertIn("not_a_kind", out["rejected"])
98
+ self.assertEqual(self.core.loaded, [])
99
+
100
+ def test_idempotent_same_meta_now(self):
101
+ meta = {"kind": "reply", "id": "m2", "ts": 2.0}
102
+ ff.apply_user(meta, self.core, now=2002.0)
103
+ snapshot = self.core.snapshot()
104
+ again = ff.apply_user(meta, self.core, now=2002.0)
105
+ self.assertTrue(again["idempotent"])
106
+ self.assertEqual(again["deltas"], {})
107
+ self.assertEqual(self.core.snapshot(), snapshot)
108
+
109
+
110
+ class MemoryChannelTest(Base):
111
+ def _entries(self):
112
+ return [
113
+ {"id": "old1", "strength": 0.10, "last_decay_ts": 100.0},
114
+ {"id": "old2", "strength": 0.50, "last_decay_ts": 100.0},
115
+ {"id": "e1", "strength": 0.50},
116
+ ]
117
+
118
+ def test_decay_and_forgotten(self):
119
+ out = ff.apply_memory(self._entries(), now=100.0 + 48 * 3600, kind="decay")
120
+ strengths = {row["id"]: row["strength"] for row in out["entries"]}
121
+ self.assertLess(strengths["old2"], 0.5)
122
+ self.assertIn("old1", out["forgotten"])
123
+ self.assertEqual(out["kind"], "decay")
124
+
125
+ def test_recall_touch_copies(self):
126
+ entries = self._entries()
127
+ out = ff.apply_memory(entries, now=3000.0, kind="recall:e1")
128
+ touched = {row["id"]: row for row in out["entries"]}["e1"]
129
+ self.assertGreater(touched["strength"], 0.5)
130
+ self.assertEqual(entries[2]["strength"], 0.5) # input not mutated
131
+ self.assertEqual(out["forgotten"], [])
132
+
133
+ def test_select_returns_selected(self):
134
+ out = ff.apply_memory(self._entries(), now=3001.0, kind="select:2")
135
+ self.assertEqual(len(out["entries"]), 2)
136
+ self.assertTrue(out["changed"])
137
+ self.assertEqual(out["kind"], "select")
138
+
139
+ def test_unknown_kind_noop(self):
140
+ entries = self._entries()
141
+ out = ff.apply_memory(entries, now=3002.0, kind="whatever")
142
+ self.assertEqual(out["kind"], "noop")
143
+ self.assertFalse(out["changed"])
144
+ self.assertEqual([row["id"] for row in out["entries"]],
145
+ [row["id"] for row in entries])
146
+
147
+
148
+ class GuardOnlyWriteTest(unittest.TestCase):
149
+ def setUp(self):
150
+ ff.reset_seen()
151
+
152
+ def test_no_direct_core_attribute_write(self):
153
+ core = StrictCore()
154
+ out = ff.apply_llm({"v": 0.3}, core, now=4000.0)
155
+ self.assertGreater(out["summary"]["v"]["new"], out["summary"]["v"]["old"])
156
+ self.assertEqual(core.loaded[-1]["v"], out["summary"]["v"]["new"])
157
+
158
+ def test_dict_core_updates_in_place(self):
159
+ core = {"v": 0.0, "a": 0.4, "stress": 0.1, "self_esteem": 0.5}
160
+ out = ff.apply_llm({"v": 0.2}, core, now=4001.0)
161
+ self.assertGreater(core["v"], 0.0)
162
+ self.assertEqual(core["v"], out["summary"]["v"]["new"])
163
+
164
+ def test_source_has_no_main_import_or_core_assignment(self):
165
+ source = Path(ff.__file__).read_text(encoding="utf-8")
166
+ self.assertIsNone(re.search(r"^\s*(?:from\s+main|import\s+main)\b",
167
+ source, re.MULTILINE))
168
+ self.assertIsNone(re.search(r"\bcore\.[A-Za-z_]+\s*=", source))
169
+ self.assertIn("feedback_guard", source)
170
+
171
+
172
+ class ResetSeenTest(unittest.TestCase):
173
+ def test_reset_allows_reapply(self):
174
+ ff.reset_seen()
175
+ core = FakeCore()
176
+ ff.apply_llm({"v": 0.3}, core, now=5000.0)
177
+ first_snapshot = core.snapshot()
178
+ ff.reset_seen()
179
+ again = ff.apply_llm({"v": 0.3}, core, now=5000.0)
180
+ self.assertFalse(again["idempotent"])
181
+ self.assertNotEqual(core.snapshot(), first_snapshot)
182
+
183
+
184
+ if __name__ == "__main__":
185
+ unittest.main()
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import feedback_guard as fg
4
+
5
+ STATE = {'v': 0.2, 'a': 0.5, 'stress': 0.2, 'social': 0.5, 'social_pain': 0.3, 'self_esteem': 0.5, 'trust': 0.5, 'closeness': 0.5, 'confidence': 0.8, 'needs': {'belonging': 0.6}}
6
+ NOW = 1_700_000_000
7
+
8
+ class GuardTest(unittest.TestCase):
9
+ def test_llm_cap(self):
10
+ out = fg.vet({'v': 0.9}, STATE, 'llm', NOW)
11
+ self.assertAlmostEqual(out['accepted']['v'], 0.15)
12
+ def test_user_cap(self):
13
+ out = fg.vet({'v': 0.9}, STATE, 'user', NOW)
14
+ self.assertAlmostEqual(out['accepted']['v'], 0.02)
15
+ def test_other_source_tighter(self):
16
+ out = fg.vet({'v': 0.9}, STATE, 'other', NOW)
17
+ self.assertAlmostEqual(out['accepted']['v'], 0.02)
18
+ def test_stress_cap_llm(self):
19
+ self.assertAlmostEqual(fg.vet({'stress': 0.9}, STATE, 'llm', NOW)['accepted']['stress'], 0.10)
20
+ def test_whitelist_rejects_unknown(self):
21
+ out = fg.vet({'luck': 0.1}, STATE, 'llm', NOW)
22
+ self.assertEqual(out['rejected'][0]['field'], 'luck')
23
+ def test_needs_unknown_rejected(self):
24
+ out = fg.vet({'needs': {'wealth': 0.1}}, STATE, 'llm', NOW)
25
+ self.assertTrue(any('wealth' in row['field'] for row in out['rejected']))
26
+ def test_total_cap_v(self):
27
+ out = fg.vet({'v': 0.15}, STATE, 'llm', NOW)
28
+ self.assertLessEqual(abs(out['accepted']['v']), 0.15)
29
+ def test_range_clamp(self):
30
+ state = dict(STATE, v=0.95)
31
+ out = fg.vet({'v': 0.15}, state, 'llm', NOW)
32
+ self.assertLessEqual(out['accepted']['v'], 0.05)
33
+ def test_idempotent_same_payload(self):
34
+ seen = {}
35
+ fg.vet({'v': 0.1}, STATE, 'llm', NOW, seen=seen)
36
+ second = fg.vet({'v': 0.1}, STATE, 'llm', NOW, seen=seen)
37
+ self.assertTrue(second['idempotent']); self.assertEqual(second['accepted'], {})
38
+ def test_audit_fields(self):
39
+ row = fg.vet({'v': 0.1}, STATE, 'llm', NOW)['audit'][0]
40
+ for key in ('field', 'old', 'new', 'source', 'reason', 'ts'): self.assertIn(key, row)
41
+ def test_audit_cap_100(self):
42
+ seen = {}
43
+ total = 0
44
+ for i in range(110):
45
+ out = fg.vet({'v': 0.01}, STATE, 'llm', NOW + i, seen=seen)
46
+ total += len(out['audit'])
47
+ self.assertLessEqual(total, 110)
48
+ def test_vet_does_not_mutate_state(self):
49
+ state = dict(STATE)
50
+ fg.vet({'v': 0.1}, state, 'llm', NOW)
51
+ self.assertEqual(state, STATE)
52
+ def test_apply_writes(self):
53
+ out = fg.apply(STATE, {'v': 0.1, 'needs': {'belonging': 0.05}})
54
+ self.assertGreater(out['state']['v'], STATE['v']); self.assertIn('v', out['changed'])
55
+ def test_apply_needs(self):
56
+ out = fg.apply(STATE, {'needs': {'belonging': 0.05}})
57
+ self.assertGreater(out['state']['needs']['belonging'], 0.6)
58
+ def test_deterministic(self):
59
+ self.assertEqual(fg.vet({'v': 0.1}, STATE, 'llm', NOW), fg.vet({'v': 0.1}, STATE, 'llm', NOW))
60
+ def test_limits_override(self):
61
+ out = fg.vet({'v': 0.5}, STATE, 'llm', NOW, limits={'v': 0.05})
62
+ self.assertAlmostEqual(out['accepted']['v'], 0.05)
63
+
64
+ if __name__ == '__main__':
65
+ unittest.main()
@@ -0,0 +1,59 @@
1
+ # -*- coding: utf-8 -*-
2
+ import re, unittest
3
+ from pathlib import Path
4
+ from biocore_model import feedback_llm as fb
5
+
6
+ STATE = {'v': 0.0, 'a': 0.5, 'stress': 0.2, 'trust': 0.5, 'closeness': 0.5, 'confidence': 0.8,
7
+ 'needs': {'belonging': 0.6}}
8
+
9
+ class FeedbackTest(unittest.TestCase):
10
+ def test_basic_delta(self):
11
+ out = fb.parse_to_deltas({'v': 0.1}, state=STATE)
12
+ self.assertAlmostEqual(out['deltas']['v'], 0.1)
13
+ def test_step_cap_v(self):
14
+ out = fb.parse_to_deltas({'v': 0.9}, state=STATE)
15
+ self.assertAlmostEqual(out['deltas']['v'], 0.15)
16
+ def test_step_cap_stress(self):
17
+ out = fb.parse_to_deltas({'stress': 0.9}, state=STATE)
18
+ self.assertLessEqual(out['deltas']['stress'], 0.10)
19
+ def test_trust_cap(self):
20
+ out = fb.parse_to_deltas({'trust': 0.9}, state=STATE)
21
+ self.assertLessEqual(out['deltas']['trust'], 0.05)
22
+ def test_range_clamp(self):
23
+ state = dict(STATE, v=0.95)
24
+ out = fb.parse_to_deltas({'v': 1.0}, state=state)
25
+ self.assertLessEqual(out['deltas']['v'], 0.05)
26
+ def test_rejected_unknown(self):
27
+ out = fb.parse_to_deltas({'mystery': 1}, state=STATE)
28
+ self.assertIn('mystery', out['rejected'])
29
+ def test_rejected_bad_type(self):
30
+ out = fb.parse_to_deltas({'v': 'high'}, state=STATE)
31
+ self.assertIn('v', out['rejected'])
32
+ def test_idempotent_same_payload_now(self):
33
+ seen = {}
34
+ first = fb.parse_to_deltas({'v': 0.1}, state=STATE, now=100, seen=seen)
35
+ second = fb.parse_to_deltas({'v': 0.1}, state=STATE, now=100, seen=seen)
36
+ self.assertTrue(second['idempotent']); self.assertEqual(second['deltas'], {})
37
+ self.assertTrue(first['deltas'])
38
+ def test_audit_fields(self):
39
+ out = fb.parse_to_deltas({'v': 0.1}, state=STATE)
40
+ row = out['audit'][0]
41
+ for key in ('field', 'old', 'new', 'source', 'reason'):
42
+ self.assertIn(key, row)
43
+ self.assertEqual(row['source'], 'llm')
44
+ def test_needs_delta(self):
45
+ out = fb.parse_to_deltas({'needs': {'belonging': 0.8}}, state=STATE)
46
+ self.assertGreater(out['deltas']['needs']['belonging'], 0)
47
+ def test_unknown_need_rejected(self):
48
+ out = fb.parse_to_deltas({'needs': {'wealth': 1}}, state=STATE)
49
+ self.assertTrue(any('wealth' in item for item in out['rejected']))
50
+ def test_deterministic(self):
51
+ self.assertEqual(fb.parse_to_deltas({'v': 0.1}, state=STATE), fb.parse_to_deltas({'v': 0.1}, state=STATE))
52
+ def test_hash_stable(self):
53
+ self.assertEqual(fb.payload_hash({'a': 1, 'b': 2}), fb.payload_hash({'b': 2, 'a': 1}))
54
+ def test_no_core_assignment(self):
55
+ source = Path(__file__).resolve().parent.parent.joinpath('feedback_llm.py').read_text(encoding='utf-8')
56
+ self.assertIsNone(re.search(r'core\.[A-Za-z_]+\s*=', source))
57
+
58
+ if __name__ == '__main__':
59
+ unittest.main()
@@ -0,0 +1,140 @@
1
+ # -*- coding: utf-8 -*-
2
+ """FB-1 inferred-meta path tests (interpretation layer -> feedback deltas)."""
3
+ import unittest
4
+
5
+ from biocore_model import feedback_flow as ff
6
+ from biocore_model import feedback_llm as fl
7
+
8
+ INFERRED = {
9
+ "emotion": "joy",
10
+ "intensity": 0.807,
11
+ "valence_delta": 0.145,
12
+ "arousal_delta": 0.048,
13
+ "dominance_delta": 0.032,
14
+ "social_delta": 0.040,
15
+ "trust_delta": 0.016,
16
+ "confidence": 0.677,
17
+ "source": "inferred",
18
+ }
19
+ NOW = 1_700_000_000.0
20
+
21
+
22
+ class InferredParseTest(unittest.TestCase):
23
+ def test_full_meta_maps_to_feedback_fields(self):
24
+ result = fl.parse_to_deltas(INFERRED, now=NOW)
25
+ self.assertEqual(result["source"], "inferred")
26
+ deltas = result["deltas"]
27
+ self.assertAlmostEqual(deltas["v"], 0.0982, places=4)
28
+ self.assertAlmostEqual(deltas["a"], 0.0325, places=4)
29
+ self.assertAlmostEqual(deltas["d"], 0.0217, places=4)
30
+ self.assertAlmostEqual(deltas["social"], 0.0271, places=4)
31
+ self.assertAlmostEqual(deltas["trust"], 0.0108, places=4)
32
+ self.assertEqual(result["rejected"], [])
33
+
34
+ def test_confidence_scales_deltas(self):
35
+ low = dict(INFERRED, confidence=0.5)
36
+ high = dict(INFERRED, confidence=0.9)
37
+ low_deltas = fl.parse_to_deltas(low, now=NOW)["deltas"]
38
+ high_deltas = fl.parse_to_deltas(high, now=NOW)["deltas"]
39
+ for field in ("v", "a", "d", "social", "trust"):
40
+ self.assertGreater(high_deltas[field], low_deltas[field])
41
+ self.assertAlmostEqual(high_deltas["v"] / low_deltas["v"], 0.9 / 0.5, places=3)
42
+
43
+ def test_low_confidence_produces_nothing(self):
44
+ payload = dict(INFERRED, confidence=0.39)
45
+ result = fl.parse_to_deltas(payload, now=NOW)
46
+ self.assertEqual(result["deltas"], {})
47
+ self.assertEqual(result.get("gate"), "low_confidence")
48
+
49
+ def test_confidence_threshold_boundary(self):
50
+ result = fl.parse_to_deltas(dict(INFERRED, confidence=0.4), now=NOW)
51
+ self.assertTrue(result["deltas"])
52
+ self.assertNotIn("gate", result)
53
+
54
+ def test_missing_confidence_produces_nothing(self):
55
+ payload = dict(INFERRED)
56
+ payload.pop("confidence")
57
+ self.assertEqual(fl.parse_to_deltas(payload, now=NOW)["deltas"], {})
58
+
59
+ def test_partial_fields_only_emit_present_ones(self):
60
+ payload = {"valence_delta": 0.1, "confidence": 0.8}
61
+ deltas = fl.parse_to_deltas(payload, now=NOW)["deltas"]
62
+ self.assertEqual(list(deltas), ["v"])
63
+ self.assertAlmostEqual(deltas["v"], 0.08, places=4)
64
+
65
+ def test_invalid_field_is_rejected_not_crashing(self):
66
+ payload = {"valence_delta": "not-a-number", "confidence": 0.9}
67
+ result = fl.parse_to_deltas(payload, now=NOW)
68
+ self.assertEqual(result["deltas"], {})
69
+ self.assertIn("valence_delta", result["rejected"])
70
+
71
+ def test_caps_are_applied(self):
72
+ payload = {"valence_delta": 5.0, "confidence": 1.0}
73
+ self.assertAlmostEqual(fl.parse_to_deltas(payload, now=NOW)["deltas"]["v"],
74
+ 0.15, places=4)
75
+
76
+ def test_idempotent_same_now(self):
77
+ seen = {}
78
+ first = fl.parse_to_deltas(INFERRED, now=NOW, seen=seen)
79
+ second = fl.parse_to_deltas(INFERRED, now=NOW, seen=seen)
80
+ self.assertTrue(first["deltas"])
81
+ self.assertTrue(second["idempotent"])
82
+ self.assertEqual(second["deltas"], {})
83
+
84
+ def test_audit_carries_confidence_and_reason(self):
85
+ audit = fl.parse_to_deltas(INFERRED, now=NOW)["audit"]
86
+ self.assertTrue(audit)
87
+ for row in audit:
88
+ self.assertEqual(row["reason"], "inferred meta")
89
+ self.assertEqual(row["source"], "llm")
90
+ self.assertGreater(row["confidence"], 0)
91
+
92
+
93
+ class LegacyPathUnchangedTest(unittest.TestCase):
94
+ def test_target_value_path_still_works(self):
95
+ result = fl.parse_to_deltas({"v": 0.9}, state={"v": 0.0}, now=NOW)
96
+ self.assertAlmostEqual(result["deltas"]["v"], 0.15, places=4)
97
+
98
+ def test_needs_path_still_works(self):
99
+ result = fl.parse_to_deltas(
100
+ {"needs": {"belonging": 0.9}}, state={"needs": {"belonging": 0.6}},
101
+ now=NOW)
102
+ self.assertAlmostEqual(result["deltas"]["needs"]["belonging"], 0.10,
103
+ places=4)
104
+
105
+
106
+ class ApplyLlmIntegrationTest(unittest.TestCase):
107
+ def setUp(self):
108
+ ff.reset_seen()
109
+
110
+ def tearDown(self):
111
+ ff.reset_seen()
112
+
113
+ def test_apply_llm_consumes_inferred_meta(self):
114
+ core = {"v": 0.0, "a": 0.5, "stress": 0.1, "trust": 0.5, "social": 0.5,
115
+ "self_esteem": 0.5}
116
+ report = ff.apply_llm(dict(INFERRED), core, NOW)
117
+ deltas = report["deltas"]
118
+ self.assertTrue(deltas)
119
+ self.assertAlmostEqual(deltas["v"], 0.0982, places=4)
120
+ self.assertAlmostEqual(deltas["a"], 0.0325, places=4)
121
+ self.assertAlmostEqual(deltas["social"], 0.0271, places=4)
122
+ self.assertAlmostEqual(deltas["trust"], 0.0108, places=4)
123
+ # feedback_guard has no 'd' in its whitelist -> rejected at the gate
124
+ rejected_fields = {row.get("field") if isinstance(row, dict) else row
125
+ for row in report["rejected"]}
126
+ self.assertIn("d", rejected_fields)
127
+ self.assertNotIn("d", deltas)
128
+ self.assertGreater(report["summary"]["v"]["new"],
129
+ report["summary"]["v"]["old"])
130
+ self.assertGreater(core["v"], 0.0)
131
+
132
+ def test_apply_llm_ignores_low_confidence(self):
133
+ core = {"v": 0.0, "a": 0.5, "stress": 0.1, "trust": 0.5, "social": 0.5}
134
+ report = ff.apply_llm(dict(INFERRED, confidence=0.2), core, NOW)
135
+ self.assertEqual(report["deltas"], {})
136
+ self.assertEqual(report["summary"]["v"]["new"], report["summary"]["v"]["old"])
137
+
138
+
139
+ if __name__ == "__main__":
140
+ unittest.main()
@@ -0,0 +1,63 @@
1
+ # -*- coding: utf-8 -*-
2
+ import random, re, unittest
3
+ from pathlib import Path
4
+ from biocore_model import feedback_memory as fm
5
+
6
+ NOW = 1_700_000_000
7
+
8
+ def entry(eid, strength=1.0, last=None):
9
+ return {'id': eid, 'strength': strength, 'last_decay_ts': last if last is not None else NOW}
10
+
11
+ class MemoryFeedbackTest(unittest.TestCase):
12
+ def test_touch_boost(self):
13
+ out = fm.touch([entry('e1', 0.3)], 'e1', NOW + 10)
14
+ self.assertGreater(out['entries'][0]['strength'], 0.3); self.assertEqual(out['entries'][0]['recall_count'], 1)
15
+ def test_touch_cap(self):
16
+ out = fm.touch([entry('e1', 0.95)], 'e1', NOW + 10)
17
+ self.assertLessEqual(out['entries'][0]['strength'], 1.0)
18
+ def test_touch_missing_id_noop(self):
19
+ out = fm.touch([entry('e1')], 'nope', NOW + 10)
20
+ self.assertEqual(out['entries'][0]['strength'], 1.0); self.assertEqual(out['audit'], [])
21
+ def test_touch_idempotent_same_now(self):
22
+ first = fm.touch([entry('e1', 0.3)], 'e1', NOW + 10)
23
+ second = fm.touch(first['entries'], 'e1', NOW + 10)
24
+ self.assertTrue(second['idempotent']); self.assertEqual(second['deltas'], {})
25
+ def test_core_memory_after_six(self):
26
+ entries = [entry('e1', 0.5)]
27
+ for step in range(6):
28
+ entries = fm.touch(entries, 'e1', NOW + 10 + step)['entries']
29
+ self.assertTrue(entries[0]['core_memory'])
30
+ def test_power_law_decay(self):
31
+ out = fm.decay_all([entry('e1', 1.0, NOW - 86400)], NOW)
32
+ self.assertAlmostEqual(out['entries'][0]['strength'], round(2 ** -0.5, 4), places=3)
33
+ def test_core_memory_no_decay(self):
34
+ e = entry('e1', 0.6, NOW - 86400 * 10); e['core_memory'] = True
35
+ self.assertEqual(fm.decay_all([e], NOW)['entries'][0]['strength'], 0.6)
36
+ def test_forgotten_threshold(self):
37
+ out = fm.decay_all([entry('e1', 0.2, NOW - 86400 * 10)], NOW)
38
+ self.assertLess(out['entries'][0]['strength'], 0.15); self.assertIn('e1', out['forgotten_ids'])
39
+ def test_select_skips_weak(self):
40
+ out = fm.select_for_prompt([entry('a', 0.9), entry('b', 0.05)], limit=2)
41
+ self.assertEqual([e['id'] for e in out['selected']], ['a']); self.assertIn('b', out['skipped'])
42
+ def test_select_limit(self):
43
+ entries = [entry('e%d' % i, 0.5 + i / 10) for i in range(6)]
44
+ out = fm.select_for_prompt(entries, limit=3, rng=random.Random(1))
45
+ self.assertEqual(len(out['selected']), 3)
46
+ def test_select_deterministic_seed(self):
47
+ entries = [entry('e%d' % i, 0.3 + i / 10) for i in range(5)]
48
+ a = fm.select_for_prompt(entries, limit=2, rng=random.Random(7))
49
+ b = fm.select_for_prompt(entries, limit=2, rng=random.Random(7))
50
+ self.assertEqual([e['id'] for e in a['selected']], [e['id'] for e in b['selected']])
51
+ def test_audit_fields(self):
52
+ row = fm.touch([entry('e1', 0.3)], 'e1', NOW + 10)['audit'][0]
53
+ for key in ('field', 'old', 'new', 'source', 'reason'): self.assertIn(key, row)
54
+ def test_no_core_assignment(self):
55
+ source = Path(__file__).resolve().parent.parent.joinpath('feedback_memory.py').read_text(encoding='utf-8')
56
+ self.assertIsNone(re.search(r'core\.[A-Za-z_]+\s*=', source))
57
+ def test_only_memory_fields(self):
58
+ out = fm.touch([entry('e1', 0.3)], 'e1', NOW + 10)
59
+ allowed = {'strength', 'recall_count', 'last_touched', 'last_decay_ts', 'last_touch_ts', 'last_touch_kind', 'core_memory'}
60
+ self.assertTrue(set(out['deltas']['e1']).issubset(allowed))
61
+
62
+ if __name__ == '__main__':
63
+ unittest.main()
@@ -0,0 +1,61 @@
1
+ # -*- coding: utf-8 -*-
2
+ import re, unittest
3
+ from pathlib import Path
4
+ from biocore_model import feedback_user as fu, feedback_llm as fl
5
+
6
+ STATE = {'v': 0.0, 'social': 0.5, 'stress': 0.2, 'social_pain': 0.3, 'self_esteem': 0.5, 'mood_valence': -0.2}
7
+ NOW = 1_700_000_000
8
+
9
+ class UserFeedbackTest(unittest.TestCase):
10
+ def test_reply_deltas(self):
11
+ out = fu.on_message(STATE, {'kind': 'reply', 'id': 'm1'}, NOW)
12
+ self.assertGreater(out['deltas']['v'], 0); self.assertLess(out['deltas']['social_pain'], 0)
13
+ def test_praise(self):
14
+ out = fu.on_message(STATE, {'kind': 'praise', 'id': 'm2'}, NOW)
15
+ self.assertGreater(out['deltas']['self_esteem'], 0); self.assertGreater(out['deltas']['v'], 0)
16
+ def test_insult(self):
17
+ out = fu.on_message(STATE, {'kind': 'insult', 'id': 'm3'}, NOW)
18
+ self.assertLess(out['deltas']['self_esteem'], 0); self.assertGreater(out['deltas']['social_pain'], 0)
19
+ def test_reject(self):
20
+ out = fu.on_message(STATE, {'kind': 'reject', 'id': 'm4'}, NOW)
21
+ self.assertGreater(out['deltas']['social_pain'], 0); self.assertLess(out['deltas']['v'], 0)
22
+ def test_cap_v(self):
23
+ out = fu.on_message(STATE, {'kind': 'reject', 'id': 'm5'}, NOW)
24
+ self.assertGreaterEqual(out['deltas']['v'], -0.03); self.assertLessEqual(abs(out['deltas']['v']), 0.03)
25
+ def test_cap_self_esteem(self):
26
+ out = fu.on_message(STATE, {'kind': 'insult', 'id': 'm6'}, NOW)
27
+ self.assertLessEqual(abs(out['deltas']['self_esteem']), 0.05)
28
+ def test_idempotent_same_event(self):
29
+ seen = {}
30
+ first = fu.on_message(STATE, {'kind': 'reply', 'id': 'm7'}, NOW, seen)
31
+ second = fu.on_message(STATE, {'kind': 'reply', 'id': 'm7'}, NOW, seen)
32
+ self.assertTrue(second['idempotent']); self.assertEqual(second['deltas'], {})
33
+ self.assertTrue(first['deltas'])
34
+ def test_silence_accumulates(self):
35
+ out = fu.on_silence(STATE, 5, NOW)
36
+ self.assertGreater(out['deltas']['social_pain'], 0); self.assertLess(out['deltas']['mood_valence'], 0)
37
+ def test_silence_cap(self):
38
+ out = fu.on_silence(STATE, 100, NOW)
39
+ self.assertLessEqual(out['deltas']['social_pain'], 0.10); self.assertGreaterEqual(out['deltas']['mood_valence'], -0.02)
40
+ def test_silence_idempotent_same_hour(self):
41
+ seen = {}
42
+ fu.on_silence(STATE, 5, NOW, seen)
43
+ second = fu.on_silence(STATE, 5, NOW + 30, seen)
44
+ self.assertTrue(second['idempotent'])
45
+ def test_audit_format(self):
46
+ out = fu.on_message(STATE, {'kind': 'reply', 'id': 'm8'}, NOW)
47
+ row = out['audit'][0]
48
+ for key in ('field', 'old', 'new', 'source', 'reason'): self.assertIn(key, row)
49
+ self.assertEqual(row['source'], 'user')
50
+ def test_no_field_overlap_with_llm(self):
51
+ user_fields = set(fu.EVENTS['reply']) | {'mood_valence'}
52
+ llm_fields = set(fl.CAPS)
53
+ self.assertTrue(user_fields.isdisjoint({'a', 'trust', 'closeness', 'confidence'}))
54
+ def test_deterministic(self):
55
+ self.assertEqual(fu.on_message(STATE, {'kind': 'praise', 'id': 'm9'}, NOW), fu.on_message(STATE, {'kind': 'praise', 'id': 'm9'}, NOW))
56
+ def test_no_core_assignment(self):
57
+ source = Path(__file__).resolve().parent.parent.joinpath('feedback_user.py').read_text(encoding='utf-8')
58
+ self.assertIsNone(re.search(r'core\.[A-Za-z_]+\s*=', source))
59
+
60
+ if __name__ == '__main__':
61
+ unittest.main()
@@ -0,0 +1,66 @@
1
+ # -*- coding: utf-8 -*-
2
+ import random, unittest
3
+ from biocore_model import frustration_curve as fc
4
+
5
+ NOW = 1_700_000_000
6
+
7
+ class CurveTest(unittest.TestCase):
8
+ def test_segment_one(self):
9
+ self.assertAlmostEqual(fc.frustration_mult(1), 1.04); self.assertAlmostEqual(fc.frustration_mult(3), 1.12)
10
+ def test_segment_two(self):
11
+ self.assertAlmostEqual(fc.frustration_mult(4), 1.20); self.assertAlmostEqual(fc.frustration_mult(8), 1.24)
12
+ def test_segment_three(self):
13
+ self.assertLess(fc.frustration_mult(12), 1.24); self.assertGreaterEqual(fc.frustration_mult(30), 0.9)
14
+ def test_phase_names(self):
15
+ self.assertEqual(fc.phase(2), 'invigoration'); self.assertEqual(fc.phase(6), 'plateau'); self.assertEqual(fc.phase(10), 'extinction')
16
+ def test_inverted_u(self):
17
+ self.assertGreater(fc.frustration_mult(3), fc.frustration_mult(1))
18
+ self.assertLess(fc.frustration_mult(20), fc.frustration_mult(8))
19
+ def test_interval_range(self):
20
+ for _ in range(50):
21
+ value = fc.next_interval(rng=random.Random(_))
22
+ self.assertGreaterEqual(value, 36.0); self.assertLessEqual(value, 96.0)
23
+ def test_schedule_sets_next(self):
24
+ state = fc.schedule_tick({}, NOW, rng=random.Random(1))
25
+ self.assertGreater(state['next_allowed_ts'], NOW)
26
+ def test_burst_probability(self):
27
+ state = fc.schedule_tick({}, NOW, rng=random.Random(0))
28
+ self.assertAlmostEqual(state['next_allowed_ts'], NOW) if state['burst'] else self.assertGreater(state['next_allowed_ts'], NOW)
29
+ def test_burst_cooldown(self):
30
+ state = {'burst_cooldown_until': NOW + 3600}
31
+ state = fc.schedule_tick(state, NOW, rng=random.Random(0))
32
+ self.assertFalse(state['burst'])
33
+ def test_wait_blocked_counter(self):
34
+ state = {'next_allowed_ts': NOW + 600}
35
+ self.assertEqual(fc.wait_blocked(state, NOW)['wait_blocked'], 1)
36
+
37
+ class GoalTest(unittest.TestCase):
38
+ def test_goal_frustration(self):
39
+ rows = fc.goals_tick([{'id': 'g1', 'progress': 0.2}], NOW, hours=2)
40
+ self.assertAlmostEqual(rows[0]['frustration_delta'], 0.04)
41
+ def test_goal_anxiety_near_deadline(self):
42
+ rows = fc.goals_tick([{'id': 'g1', 'deadline_ts': NOW + 3600}], NOW)
43
+ self.assertEqual(rows[0]['anxiety_delta'], 0.03)
44
+ def test_goal_completed(self):
45
+ out = fc.goal_completed({'id': 'g1'})
46
+ self.assertEqual(out['joy'], 0.05); self.assertEqual(out['goal']['status'], 'done')
47
+ def test_done_goal_not_ticked(self):
48
+ rows = fc.goals_tick([{'id': 'g1', 'status': 'done'}], NOW)
49
+ self.assertNotIn('frustration_delta', rows[0])
50
+
51
+ class NeedsTest(unittest.TestCase):
52
+ def test_allowed_raises_autonomy(self):
53
+ self.assertGreater(fc.needs_update(allowed=True)['autonomy_need'], 0.5)
54
+ def test_limited_lowers_autonomy(self):
55
+ self.assertLess(fc.needs_update(allowed=False)['autonomy_need'], 0.5)
56
+ def test_helpful_raises_competence(self):
57
+ self.assertGreater(fc.needs_update(helpful=True)['competence_need'], 0.5)
58
+ def test_wrong_lowers_competence(self):
59
+ self.assertLess(fc.needs_update(wrong=True)['competence_need'], 0.5)
60
+ def test_low_autonomy_suppress_bonus(self):
61
+ self.assertEqual(fc.needs_update(autonomy=0.2)['suppress_bonus'], 0.2)
62
+ def test_low_competence_confidence_delta(self):
63
+ self.assertEqual(fc.needs_update(competence=0.2)['confidence_delta'], -0.05)
64
+
65
+ if __name__ == '__main__':
66
+ unittest.main()