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,350 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-187 共召回统计单测:配对、去重、阈值、30 天时间窗、标记、畸形/空输入。
3
+
4
+ 只依赖 biocore_memory.co_recall;使用临时 SQLite 文件,不碰真实 memory 库,
5
+ 不导入 main.py,不新增 LLM 调用。
6
+ """
7
+ import sqlite3
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ from biocore_memory import co_recall as cr
13
+
14
+ NOW = 1_700_000_000.0
15
+ DAY = 86400.0
16
+
17
+
18
+ class CoRecallStorageTest(unittest.TestCase):
19
+ def setUp(self):
20
+ self._tmp = tempfile.TemporaryDirectory()
21
+ self.db = Path(self._tmp.name) / "memory" / "memory.sqlite3"
22
+
23
+ def tearDown(self):
24
+ self._tmp.cleanup()
25
+
26
+
27
+ class PureFunctionTest(unittest.TestCase):
28
+ def test_normalize_id(self):
29
+ self.assertEqual(cr.normalize_id(" a "), "a")
30
+ self.assertEqual(cr.normalize_id(3), "3")
31
+ for bad in (None, "", " ", {}, [], ("x",)):
32
+ self.assertEqual(cr.normalize_id(bad), "")
33
+
34
+ def test_unique_ids(self):
35
+ self.assertEqual(cr.unique_ids(["b", "a", "b", "", None, "a"]),
36
+ ["b", "a"])
37
+ self.assertEqual(cr.unique_ids("a,b"), [])
38
+ self.assertEqual(cr.unique_ids(None), [])
39
+
40
+ def test_pair_key_is_unordered(self):
41
+ self.assertEqual(cr.pair_key("b", "a"), ("a", "b"))
42
+ self.assertEqual(cr.pair_key("a", "b"), ("a", "b"))
43
+ self.assertIsNone(cr.pair_key("a", "a"))
44
+ self.assertIsNone(cr.pair_key("", "b"))
45
+ self.assertIsNone(cr.pair_key(None, "b"))
46
+
47
+ def test_pairs_unordered_and_deduped(self):
48
+ self.assertEqual(cr.pairs(["c", "a", "b", "a", None, ""]),
49
+ [("a", "b"), ("a", "c"), ("b", "c")])
50
+ self.assertEqual(cr.pairs([]), [])
51
+ self.assertEqual(cr.pairs(None), [])
52
+ self.assertEqual(cr.pairs("not-a-list"), [])
53
+
54
+ def test_pairs_skips_container_ids(self):
55
+ self.assertEqual(cr.pairs(["a", "b", {"id": "c"}, ["d"]]),
56
+ [("a", "b")])
57
+
58
+ def test_qualifies_rule(self):
59
+ self.assertTrue(cr.qualifies(3, NOW - DAY, 3, 30, now=NOW))
60
+ self.assertFalse(cr.qualifies(2, NOW - DAY, 3, 30, now=NOW))
61
+ self.assertFalse(cr.qualifies(5, NOW - 40 * DAY, 3, 30, now=NOW))
62
+ self.assertTrue(cr.qualifies(5, NOW - 40 * DAY, 3, 0, now=NOW))
63
+ self.assertFalse(cr.qualifies("x", None, 3, 30, now=NOW))
64
+
65
+
66
+ class RecordTest(CoRecallStorageTest):
67
+ def test_record_inserts_pairs(self):
68
+ out = cr.record_recall(["m1", "m2", "m3"], ts=NOW, db_path=self.db)
69
+ self.assertEqual(out["pairs"], 3)
70
+ self.assertEqual(out["ids"], 3)
71
+ rows = cr.candidates(threshold=1, window_days=0, now=NOW,
72
+ db_path=self.db)
73
+ self.assertEqual(len(rows), 3)
74
+ for row in rows:
75
+ self.assertEqual(row["count"], 1)
76
+ self.assertEqual(row["first_ts"], NOW)
77
+ self.assertEqual(row["last_ts"], NOW)
78
+ self.assertEqual(row["marked"], False)
79
+
80
+ def test_repeat_updates_count_and_last_ts(self):
81
+ cr.record_recall(["a", "b"], ts=NOW - 10 * DAY, db_path=self.db)
82
+ cr.record_recall(["a", "b"], ts=NOW - DAY, db_path=self.db)
83
+ row = cr.candidates(threshold=1, window_days=0, now=NOW,
84
+ db_path=self.db)[0]
85
+ self.assertEqual(row["count"], 2)
86
+ self.assertEqual(row["first_ts"], NOW - 10 * DAY)
87
+ self.assertEqual(row["last_ts"], NOW - DAY)
88
+
89
+ def test_unordered_pairs_merge(self):
90
+ cr.record_recall(["a", "b"], ts=NOW, db_path=self.db)
91
+ cr.record_recall(["b", "a"], ts=NOW, db_path=self.db)
92
+ rows = cr.candidates(threshold=1, window_days=0, now=NOW,
93
+ db_path=self.db)
94
+ self.assertEqual(len(rows), 1)
95
+ self.assertEqual(rows[0]["count"], 2)
96
+ self.assertEqual(rows[0]["pair"], ("a", "b"))
97
+
98
+ def test_duplicate_id_in_one_batch_counts_once(self):
99
+ out = cr.record_recall(["a", "a", "b", "a"], ts=NOW, db_path=self.db)
100
+ self.assertEqual(out["pairs"], 1)
101
+ self.assertEqual(out["ids"], 2)
102
+ rows = cr.candidates(threshold=1, window_days=0, now=NOW,
103
+ db_path=self.db)
104
+ self.assertEqual(rows[0]["count"], 1)
105
+
106
+ def test_empty_batch_is_noop(self):
107
+ out = cr.record_recall([], ts=NOW, db_path=self.db)
108
+ self.assertEqual(out["pairs"], 0)
109
+ self.assertFalse(self.db.exists())
110
+ self.assertEqual(cr.stats(db_path=self.db)["pairs"], 0)
111
+
112
+ def test_invalid_ids_are_skipped(self):
113
+ out = cr.record_recall(["a", "b", "", None, {"x": 1}, ["c"]],
114
+ ts=NOW, db_path=self.db)
115
+ self.assertEqual(out["pairs"], 1)
116
+ self.assertEqual(out["ids"], 2)
117
+
118
+
119
+ class ThresholdAndWindowTest(CoRecallStorageTest):
120
+ def test_threshold_boundary(self):
121
+ for tick in range(2):
122
+ cr.record_recall(["a", "b"], ts=NOW - tick * DAY, db_path=self.db)
123
+ self.assertEqual(cr.candidates(threshold=3, window_days=30, now=NOW,
124
+ db_path=self.db), [])
125
+ cr.record_recall(["a", "b"], ts=NOW, db_path=self.db)
126
+ rows = cr.candidates(threshold=3, window_days=30, now=NOW,
127
+ db_path=self.db)
128
+ self.assertEqual(len(rows), 1)
129
+ self.assertEqual(rows[0]["count"], 3)
130
+
131
+ def test_window_excludes_old_pairs(self):
132
+ for stamp in (40, 39, 38):
133
+ cr.record_recall(["old", "pair"], ts=NOW - stamp * DAY,
134
+ db_path=self.db)
135
+ for stamp in (2, 1, 0):
136
+ cr.record_recall(["new", "pair"], ts=NOW - stamp * DAY,
137
+ db_path=self.db)
138
+ recent = cr.candidates(threshold=3, window_days=30, now=NOW,
139
+ db_path=self.db)
140
+ self.assertEqual([row["pair"] for row in recent], [("new", "pair")])
141
+ every = cr.candidates(threshold=3, window_days=0, now=NOW,
142
+ db_path=self.db)
143
+ self.assertEqual(len(every), 2)
144
+
145
+ def test_candidates_sorted_by_count_then_recency(self):
146
+ for _ in range(2):
147
+ cr.record_recall(["a", "b"], ts=NOW - DAY, db_path=self.db)
148
+ for _ in range(4):
149
+ cr.record_recall(["c", "d"], ts=NOW, db_path=self.db)
150
+ rows = cr.candidates(threshold=2, window_days=30, now=NOW,
151
+ db_path=self.db)
152
+ self.assertEqual(rows[0]["pair"], ("c", "d"))
153
+ self.assertEqual(rows[1]["pair"], ("a", "b"))
154
+ self.assertGreaterEqual(rows[0]["count"], rows[1]["count"])
155
+
156
+ def test_30_day_sample(self):
157
+ for stamp in (20, 10, 1):
158
+ cr.record_recall(["m1", "m2", "m3"], ts=NOW - stamp * DAY,
159
+ db_path=self.db)
160
+ for stamp in (40, 39, 38):
161
+ cr.record_recall(["m4", "m5"], ts=NOW - stamp * DAY,
162
+ db_path=self.db)
163
+ rows = cr.candidates(threshold=3, window_days=30, now=NOW,
164
+ db_path=self.db)
165
+ self.assertEqual([row["pair"] for row in rows],
166
+ [("m1", "m2"), ("m1", "m3"), ("m2", "m3")])
167
+ for row in rows:
168
+ self.assertEqual(row["count"], 3)
169
+ self.assertEqual(row["age_days"], 1.0)
170
+
171
+
172
+ class MarkTest(CoRecallStorageTest):
173
+ def _seed(self):
174
+ for stamp in (3, 2, 1):
175
+ cr.record_recall(["a", "b"], ts=NOW - stamp * DAY, db_path=self.db)
176
+ for _ in range(2):
177
+ cr.record_recall(["c", "d"], ts=NOW, db_path=self.db)
178
+
179
+ def test_mark_only_over_threshold(self):
180
+ self._seed()
181
+ out = cr.mark_candidates(threshold=3, window_days=30, now=NOW,
182
+ db_path=self.db)
183
+ self.assertEqual(out["marked"], 1)
184
+ marked = cr.read_marked(db_path=self.db)
185
+ self.assertEqual([row["pair"] for row in marked], [("a", "b")])
186
+ self.assertEqual(marked[0]["count"], 3)
187
+
188
+ def test_mark_is_additive_not_destructive(self):
189
+ self._seed()
190
+ cr.mark_candidates(threshold=3, window_days=30, now=NOW,
191
+ db_path=self.db)
192
+ rows = cr.candidates(threshold=1, window_days=0, now=NOW,
193
+ db_path=self.db)
194
+ self.assertEqual(len(rows), 2)
195
+ self.assertTrue(any(row["pair"] == ("a", "b") and row["marked"]
196
+ for row in rows))
197
+ self.assertEqual(cr.stats(db_path=self.db)["pairs"], 2)
198
+
199
+ def test_mark_is_idempotent(self):
200
+ self._seed()
201
+ cr.mark_candidates(threshold=3, window_days=30, now=NOW,
202
+ db_path=self.db)
203
+ out = cr.mark_candidates(threshold=3, window_days=30, now=NOW + DAY,
204
+ db_path=self.db)
205
+ self.assertEqual(out["marked"], 1)
206
+ marked = cr.read_marked(db_path=self.db)
207
+ self.assertEqual(len(marked), 1)
208
+ self.assertEqual(marked[0]["marked_ts"], NOW + DAY)
209
+
210
+ def test_mark_then_candidates_show_flag(self):
211
+ self._seed()
212
+ cr.mark_candidates(threshold=3, window_days=30, now=NOW,
213
+ db_path=self.db)
214
+ rows = cr.candidates(threshold=3, window_days=30, now=NOW,
215
+ db_path=self.db)
216
+ self.assertTrue(rows[0]["marked"])
217
+
218
+ def test_read_marked_limit(self):
219
+ self._seed()
220
+ for stamp in (2, 1, 0):
221
+ cr.record_recall(["e", "f"], ts=NOW - stamp * DAY,
222
+ db_path=self.db)
223
+ cr.mark_candidates(threshold=3, window_days=30, now=NOW,
224
+ db_path=self.db)
225
+ self.assertEqual(len(cr.read_marked(db_path=self.db)), 2)
226
+ self.assertEqual(len(cr.read_marked(db_path=self.db, limit=1)), 1)
227
+
228
+
229
+ class ReadOnlyTest(CoRecallStorageTest):
230
+ def test_stats(self):
231
+ cr.record_recall(["a", "b", "c"], ts=NOW, db_path=self.db)
232
+ cr.record_recall(["a", "b"], ts=NOW, db_path=self.db)
233
+ cr.mark_candidates(threshold=2, window_days=30, now=NOW,
234
+ db_path=self.db)
235
+ out = cr.stats(db_path=self.db)
236
+ self.assertEqual(out["pairs"], 3)
237
+ self.assertEqual(out["recalls"], 4)
238
+ self.assertEqual(out["marked"], 1)
239
+
240
+ def test_readout_shape(self):
241
+ cr.record_recall(["a", "b", "c", "d"], ts=NOW, db_path=self.db)
242
+ out = cr.readout(threshold=1, window_days=30, now=NOW,
243
+ db_path=self.db, limit=2)
244
+ self.assertEqual(out["threshold"], 1)
245
+ self.assertIn("stats", out)
246
+ self.assertEqual(len(out["candidates"]), 2)
247
+ self.assertEqual(out["stats"]["pairs"], 6)
248
+
249
+ def test_default_path_is_memory_sqlite3(self):
250
+ path = cr.default_db_path()
251
+ self.assertEqual(path.name, "memory.sqlite3")
252
+ self.assertEqual(path.parent.name, "memory")
253
+
254
+ def test_entries_table_is_untouched(self):
255
+ self.db.parent.mkdir(parents=True, exist_ok=True)
256
+ raw = sqlite3.connect(str(self.db))
257
+ raw.execute("CREATE TABLE entries(id TEXT PRIMARY KEY, text TEXT)")
258
+ raw.execute("INSERT INTO entries VALUES('m1', 'hello')")
259
+ raw.commit()
260
+ raw.close()
261
+ cr.record_recall(["m1", "m2"], ts=NOW, db_path=self.db)
262
+ cr.record_recall(["m1", "m2"], ts=NOW, db_path=self.db)
263
+ cr.record_recall(["m1", "m2"], ts=NOW, db_path=self.db)
264
+ cr.mark_candidates(threshold=3, window_days=30, now=NOW,
265
+ db_path=self.db)
266
+ raw = sqlite3.connect(str(self.db))
267
+ rows = raw.execute("SELECT id, text FROM entries").fetchall()
268
+ tables = {row[0] for row in raw.execute(
269
+ "SELECT name FROM sqlite_master WHERE type='table'")}
270
+ raw.close()
271
+ self.assertEqual(rows, [("m1", "hello")])
272
+ self.assertIn("entries", tables)
273
+ self.assertIn("co_recall", tables)
274
+ self.assertIn("co_recall_marks", tables)
275
+
276
+
277
+ class SchemaAndConnectionTest(CoRecallStorageTest):
278
+ def test_schema_columns(self):
279
+ conn = cr.connect(self.db)
280
+ try:
281
+ names = [row[1] for row in conn.execute(
282
+ "PRAGMA table_info(co_recall)")]
283
+ self.assertEqual(names, ["pair_a", "pair_b", "count",
284
+ "first_ts", "last_ts"])
285
+ mark_names = [row[1] for row in conn.execute(
286
+ "PRAGMA table_info(co_recall_marks)")]
287
+ for column in ("pair_a", "pair_b", "marked_ts", "threshold",
288
+ "window_days"):
289
+ self.assertIn(column, mark_names)
290
+ finally:
291
+ conn.close()
292
+
293
+ def test_ensure_schema_is_idempotent(self):
294
+ conn = cr.connect(self.db)
295
+ try:
296
+ cr.ensure_schema(conn)
297
+ cr.ensure_schema(conn)
298
+ finally:
299
+ conn.close()
300
+ cr.record_recall(["a", "b"], ts=NOW, db_path=self.db)
301
+ self.assertEqual(cr.stats(db_path=self.db)["pairs"], 1)
302
+
303
+ def test_injected_connection_stays_open(self):
304
+ conn = sqlite3.connect(":memory:")
305
+ conn.row_factory = sqlite3.Row
306
+ try:
307
+ cr.record_recall(["a", "b"], ts=NOW, conn=conn)
308
+ cr.record_recall(["a", "b"], ts=NOW, conn=conn)
309
+ cr.record_recall(["a", "b"], ts=NOW, conn=conn)
310
+ rows = cr.candidates(threshold=3, window_days=30, now=NOW,
311
+ conn=conn)
312
+ self.assertEqual(rows[0]["count"], 3)
313
+ self.assertEqual(conn.execute(
314
+ "SELECT COUNT(*) FROM co_recall").fetchone()[0], 1)
315
+ finally:
316
+ conn.close()
317
+
318
+ def test_parent_dir_is_created(self):
319
+ self.assertFalse(self.db.exists())
320
+ cr.record_recall(["a", "b"], ts=NOW, db_path=self.db)
321
+ self.assertTrue(self.db.exists())
322
+
323
+
324
+ class RobustnessTest(CoRecallStorageTest):
325
+ def test_bad_threshold_and_window(self):
326
+ for stamp in range(3):
327
+ cr.record_recall(["a", "b"], ts=NOW - stamp * DAY,
328
+ db_path=self.db)
329
+ for threshold in (None, "x", 0, -3):
330
+ rows = cr.candidates(threshold=threshold, window_days=30,
331
+ now=NOW, db_path=self.db)
332
+ self.assertIsInstance(rows, list)
333
+ for days in (None, "x", -5):
334
+ rows = cr.candidates(threshold=1, window_days=days, now=NOW,
335
+ db_path=self.db)
336
+ self.assertIsInstance(rows, list)
337
+
338
+ def test_record_tolerates_odd_args(self):
339
+ out = cr.record_recall(None, ts="x", db_path=self.db)
340
+ self.assertEqual(out["pairs"], 0)
341
+ self.assertIsInstance(out["ts"], float)
342
+
343
+ def test_mark_empty_db(self):
344
+ out = cr.mark_candidates(db_path=self.db)
345
+ self.assertEqual(out["marked"], 0)
346
+ self.assertEqual(out["pairs"], [])
347
+
348
+
349
+ if __name__ == "__main__":
350
+ unittest.main()
@@ -0,0 +1,197 @@
1
+ # -*- coding: utf-8 -*-
2
+ import datetime
3
+ import unittest
4
+
5
+ from biocore_model import commitment_extractor as ce
6
+ from biocore_model import prospective
7
+
8
+ NOW_DT = datetime.datetime(2026, 11, 2, 22, 25) # Monday
9
+ NOW = NOW_DT.timestamp()
10
+
11
+
12
+ def stamp(year, month, day, hour=9, minute=0):
13
+ return datetime.datetime(year, month, day, hour, minute).timestamp()
14
+
15
+
16
+ class CommitmentTimeTest(unittest.TestCase):
17
+ def test_relative_tomorrow(self):
18
+ out = ce.extract("明天提醒我交报告", now=NOW)
19
+ self.assertTrue(out["has_commitment"])
20
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 3))
21
+ self.assertEqual(out["surface_phrase"], "明天")
22
+ self.assertIn("交报告", out["what"])
23
+
24
+ def test_relative_day_after_tomorrow(self):
25
+ out = ce.extract("后天体检", now=NOW)
26
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 4))
27
+
28
+ def test_relative_three_days_out(self):
29
+ out = ce.extract("大后天交报告", now=NOW)
30
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 5))
31
+
32
+ def test_relative_day_with_afternoon_time(self):
33
+ out = ce.extract("我明天下午三点开会", now=NOW)
34
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 3, 15, 0))
35
+ self.assertEqual(out["surface_phrase"], "明天下午三点")
36
+ self.assertEqual(out["what"], "开会")
37
+
38
+ def test_relative_day_with_evening_digits(self):
39
+ out = ce.extract("明天晚上8点开会", now=NOW)
40
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 3, 20, 0))
41
+
42
+ def test_weekday_next_week(self):
43
+ out = ce.extract("下周三开会", now=NOW)
44
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 11))
45
+
46
+ def test_weekday_this_week(self):
47
+ out = ce.extract("本周五交报告", now=NOW)
48
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 6))
49
+
50
+ def test_weekday_bare_is_next_occurrence(self):
51
+ out = ce.extract("周五体检", now=NOW)
52
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 6))
53
+
54
+ def test_absolute_date_this_year(self):
55
+ out = ce.extract("11月5日交报告", now=NOW)
56
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 5))
57
+
58
+ def test_absolute_date_already_passed_rolls_to_next_year(self):
59
+ out = ce.extract("3月5日交报告", now=NOW)
60
+ self.assertAlmostEqual(out["due_ts"], stamp(2027, 3, 5))
61
+
62
+ def test_bare_clock_rolls_to_tomorrow_when_passed(self):
63
+ out = ce.extract("下午3点开会", now=NOW)
64
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 3, 15, 0))
65
+
66
+
67
+ class CommitmentBoundaryTest(unittest.TestCase):
68
+ def test_vague_changeday_has_no_due(self):
69
+ out = ce.extract("改天一起吃饭", now=NOW)
70
+ self.assertTrue(out["has_commitment"])
71
+ self.assertIsNone(out["due_ts"])
72
+ self.assertEqual(out["reason"], "vague")
73
+ self.assertLess(out["confidence"], 0.5)
74
+
75
+ def test_vague_find_time_has_no_due(self):
76
+ out = ce.extract("找时间聊", now=NOW)
77
+ self.assertTrue(out["has_commitment"])
78
+ self.assertIsNone(out["due_ts"])
79
+
80
+ def test_past_relative_is_not_a_commitment(self):
81
+ out = ce.extract("昨天开会到很晚", now=NOW)
82
+ self.assertFalse(out["has_commitment"])
83
+ self.assertEqual(out["reason"], "past")
84
+
85
+ def test_past_weekday_is_not_a_commitment(self):
86
+ out = ce.extract("上周三开会", now=NOW)
87
+ self.assertFalse(out["has_commitment"])
88
+ self.assertEqual(out["reason"], "past")
89
+
90
+ def test_no_time_no_commitment(self):
91
+ out = ce.extract("今天天气不错", now=NOW)
92
+ self.assertFalse(out["has_commitment"])
93
+ self.assertEqual(out["reason"], "no_intent")
94
+
95
+ def test_most_specific_commitment_wins(self):
96
+ out = ce.extract("明天提醒我交报告,下周三下午三点开会", now=NOW)
97
+ self.assertTrue(out["has_commitment"])
98
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 11, 15, 0))
99
+ self.assertIn("下周三", out["surface_phrase"])
100
+ self.assertEqual(out["what"], "开会")
101
+
102
+ def test_confidence_is_bounded(self):
103
+ for text in ("明天提醒我交报告", "下周三开会", "改天吃饭",
104
+ "今天天气不错"):
105
+ out = ce.extract(text, now=NOW)
106
+ self.assertGreaterEqual(out["confidence"], 0.0)
107
+ self.assertLessEqual(out["confidence"], 1.0)
108
+
109
+
110
+ class CommitmentWhoTest(unittest.TestCase):
111
+ def test_request_to_her_is_her_promise(self):
112
+ out = ce.extract("你明天记得问我体检结果", now=NOW)
113
+ self.assertEqual(out["who"], "her")
114
+ self.assertEqual(out["kind"], "her_promise")
115
+ self.assertIn("体检结果", out["what"])
116
+
117
+ def test_first_person_is_his_event(self):
118
+ out = ce.extract("我周五体检", now=NOW)
119
+ self.assertEqual(out["who"], "him")
120
+ self.assertEqual(out["kind"], "his_event")
121
+ self.assertEqual(out["what"], "体检")
122
+
123
+ def test_reminder_request_is_her_promise(self):
124
+ out = ce.extract("明天提醒我交报告", now=NOW)
125
+ self.assertEqual(out["who"], "her")
126
+
127
+
128
+ class CommitmentTagsTest(unittest.TestCase):
129
+ def test_structured_due_overrides_text(self):
130
+ due = NOW + 86400
131
+ out = ce.extract("记得那件事", tags={"due_ts": due}, now=NOW)
132
+ self.assertTrue(out["has_commitment"])
133
+ self.assertAlmostEqual(out["due_ts"], due)
134
+ self.assertGreaterEqual(out["confidence"], 0.95)
135
+
136
+ def test_structured_time_and_fact(self):
137
+ out = ce.extract("回头弄一下",
138
+ tags={"time": "明天下午3点", "fact": "弄材料",
139
+ "entities": ["材料"]},
140
+ now=NOW)
141
+ self.assertTrue(out["has_commitment"])
142
+ self.assertAlmostEqual(out["due_ts"], stamp(2026, 11, 3, 15, 0))
143
+ self.assertEqual(out["what"], "弄材料")
144
+ self.assertEqual(out["entities"], ["材料"])
145
+
146
+ def test_disabled_switch(self):
147
+ for config in (False, {"enabled": False}):
148
+ out = ce.extract("明天提醒我交报告", now=NOW, config=config)
149
+ self.assertFalse(out["enabled"])
150
+ self.assertFalse(out["has_commitment"])
151
+ self.assertEqual(out["reason"], "disabled")
152
+
153
+ def test_malformed_inputs_do_not_crash(self):
154
+ for text in (None, 123, "", " "):
155
+ out = ce.extract(text, now=NOW)
156
+ self.assertIn("has_commitment", out)
157
+ self.assertFalse(out["has_commitment"])
158
+ missing_now = ce.extract("明天开会", now=None)
159
+ self.assertFalse(missing_now["has_commitment"])
160
+
161
+ def test_reason_is_always_present(self):
162
+ for text in ("明天开会", "改天吃饭", "昨天开会", "今天天气不错"):
163
+ self.assertTrue(ce.extract(text, now=NOW)["reason"])
164
+
165
+
166
+ class CommitmentProspectiveBridgeTest(unittest.TestCase):
167
+ def test_bridge_builds_prospective_record(self):
168
+ result = ce.extract("你明天记得问我体检结果", now=NOW)
169
+ record = ce.to_prospective(result, now=NOW)
170
+ self.assertIsNotNone(record)
171
+ self.assertEqual(record["kind"], "her_promise")
172
+ self.assertEqual(record["text"], result["what"])
173
+ self.assertAlmostEqual(record["due_ts"], result["due_ts"])
174
+ self.assertEqual(record["status"], "pending")
175
+ self.assertTrue(record["id"])
176
+ self.assertEqual(record["meta"]["surface_phrase"], "明天")
177
+
178
+ def test_bridge_rejects_vague(self):
179
+ result = ce.extract("改天一起吃饭", now=NOW)
180
+ self.assertIsNone(ce.to_prospective(result, now=NOW))
181
+
182
+ def test_bridge_rejects_no_commitment(self):
183
+ result = ce.extract("今天天气不错", now=NOW)
184
+ self.assertIsNone(ce.to_prospective(result, now=NOW))
185
+
186
+ def test_bridge_record_roundtrips_into_prospective_store(self):
187
+ result = ce.extract("明天下午三点开会", now=NOW)
188
+ record = ce.to_prospective(result, now=NOW)
189
+ store, changed = prospective.add([], record)
190
+ self.assertTrue(changed)
191
+ self.assertEqual(len(store), 1)
192
+ due = prospective.due_now(store, record["due_ts"])
193
+ self.assertEqual(len(due), 1)
194
+
195
+
196
+ if __name__ == "__main__":
197
+ unittest.main()
@@ -0,0 +1,84 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Compassion fatigue: decay, floor, recovery."""
3
+ import unittest
4
+
5
+ from biocore_model import compassion_fatigue as cf
6
+
7
+
8
+ class DecayTest(unittest.TestCase):
9
+ def test_baseline_full_empathy(self):
10
+ out = cf.fatigue(0, 0.0, 0.0)
11
+ self.assertEqual(out["empathy"], 1.0)
12
+ self.assertEqual(out["fatigue"], 0.0)
13
+
14
+ def test_consecutive_comfort_lowers(self):
15
+ one = cf.fatigue(1, 0.0, 0.0)
16
+ five = cf.fatigue(5, 0.0, 0.0)
17
+ self.assertLess(five["empathy"], one["empathy"])
18
+
19
+ def test_negative_density_lowers(self):
20
+ calm = cf.fatigue(2, 0.0, 0.0)
21
+ heavy = cf.fatigue(2, 0.9, 0.0)
22
+ self.assertLess(heavy["empathy"], calm["empathy"])
23
+
24
+ def test_empathy_never_below_floor(self):
25
+ out = cf.fatigue(999, 1.0, 0.0)
26
+ self.assertGreaterEqual(out["empathy"], cf.EMPATHY_FLOOR)
27
+ self.assertEqual(out["band"], "low")
28
+
29
+ def test_fatigue_bounded(self):
30
+ out = cf.fatigue(999, 1.0, 0.0)
31
+ self.assertLessEqual(out["fatigue"], 1.0)
32
+ self.assertGreaterEqual(out["fatigue"], 0.0)
33
+
34
+
35
+ class RecoveryTest(unittest.TestCase):
36
+ def test_idle_restores(self):
37
+ tired = cf.fatigue(6, 0.8, 0.0)
38
+ rested = cf.fatigue(6, 0.8, 12.0)
39
+ self.assertGreater(rested["empathy"], tired["empathy"])
40
+
41
+ def test_recovery_reported(self):
42
+ out = cf.fatigue(4, 0.5, 6.0)
43
+ self.assertGreater(out["recovery"], 0.0)
44
+
45
+ def test_full_rest_clears(self):
46
+ out = cf.fatigue(4, 0.5, 200.0)
47
+ self.assertGreater(out["empathy"], 0.95)
48
+
49
+
50
+ class BandTest(unittest.TestCase):
51
+ def test_bands_present(self):
52
+ for count, density in ((0, 0.0), (3, 0.4), (10, 1.0)):
53
+ out = cf.fatigue(count, density, 0.0)
54
+ self.assertIn(out["band"], ("high", "mid", "low"))
55
+ self.assertTrue(out["advice"])
56
+
57
+ def test_low_band_advice(self):
58
+ out = cf.fatigue(20, 1.0, 0.0)
59
+ self.assertEqual(out["advice"], cf.ADVICE["low"])
60
+
61
+
62
+ class FallbackTest(unittest.TestCase):
63
+ def test_disabled(self):
64
+ out = cf.fatigue(10, 1.0, 0.0, flags={"enabled": False})
65
+ self.assertFalse(out["enabled"])
66
+ self.assertEqual(out["empathy"], 1.0)
67
+
68
+ def test_malformed(self):
69
+ out = cf.fatigue("x", "y", "z")
70
+ self.assertGreaterEqual(out["empathy"], cf.EMPATHY_FLOOR)
71
+
72
+
73
+ class DirectiveTest(unittest.TestCase):
74
+ def test_low_empathy_shortens(self):
75
+ out = cf.directives(cf.fatigue(20, 1.0, 0.0))
76
+ self.assertLess(out["length_scale"], 1.0)
77
+ self.assertTrue(out["tone"])
78
+
79
+ def test_disabled(self):
80
+ self.assertEqual(cf.directives({"enabled": False})["tone"], "")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ unittest.main()