lex-agentic-affect 0.1.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 (218) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +13 -0
  6. data/lex-agentic-affect.gemspec +30 -0
  7. data/lib/legion/extensions/agentic/affect/appraisal/client.rb +20 -0
  8. data/lib/legion/extensions/agentic/affect/appraisal/helpers/appraisal.rb +112 -0
  9. data/lib/legion/extensions/agentic/affect/appraisal/helpers/appraisal_engine.rb +129 -0
  10. data/lib/legion/extensions/agentic/affect/appraisal/helpers/constants.rb +43 -0
  11. data/lib/legion/extensions/agentic/affect/appraisal/runners/appraisal.rb +105 -0
  12. data/lib/legion/extensions/agentic/affect/appraisal/version.rb +13 -0
  13. data/lib/legion/extensions/agentic/affect/appraisal.rb +19 -0
  14. data/lib/legion/extensions/agentic/affect/cognitive_empathy/client.rb +19 -0
  15. data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/constants.rb +37 -0
  16. data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/empathy_engine.rb +151 -0
  17. data/lib/legion/extensions/agentic/affect/cognitive_empathy/helpers/perspective.rb +92 -0
  18. data/lib/legion/extensions/agentic/affect/cognitive_empathy/runners/cognitive_empathy.rb +93 -0
  19. data/lib/legion/extensions/agentic/affect/cognitive_empathy/version.rb +13 -0
  20. data/lib/legion/extensions/agentic/affect/cognitive_empathy.rb +20 -0
  21. data/lib/legion/extensions/agentic/affect/contagion/client.rb +28 -0
  22. data/lib/legion/extensions/agentic/affect/contagion/helpers/constants.rb +34 -0
  23. data/lib/legion/extensions/agentic/affect/contagion/helpers/contagion_engine.rb +184 -0
  24. data/lib/legion/extensions/agentic/affect/contagion/helpers/meme.rb +97 -0
  25. data/lib/legion/extensions/agentic/affect/contagion/runners/cognitive_contagion.rb +125 -0
  26. data/lib/legion/extensions/agentic/affect/contagion/version.rb +13 -0
  27. data/lib/legion/extensions/agentic/affect/contagion.rb +19 -0
  28. data/lib/legion/extensions/agentic/affect/defusion/client.rb +28 -0
  29. data/lib/legion/extensions/agentic/affect/defusion/helpers/constants.rb +64 -0
  30. data/lib/legion/extensions/agentic/affect/defusion/helpers/defusion_engine.rb +167 -0
  31. data/lib/legion/extensions/agentic/affect/defusion/helpers/thought.rb +92 -0
  32. data/lib/legion/extensions/agentic/affect/defusion/runners/cognitive_defusion.rb +127 -0
  33. data/lib/legion/extensions/agentic/affect/defusion/version.rb +13 -0
  34. data/lib/legion/extensions/agentic/affect/defusion.rb +19 -0
  35. data/lib/legion/extensions/agentic/affect/emotion/actors/momentum_decay.rb +45 -0
  36. data/lib/legion/extensions/agentic/affect/emotion/client.rb +36 -0
  37. data/lib/legion/extensions/agentic/affect/emotion/helpers/baseline.rb +52 -0
  38. data/lib/legion/extensions/agentic/affect/emotion/helpers/momentum.rb +52 -0
  39. data/lib/legion/extensions/agentic/affect/emotion/helpers/valence.rb +92 -0
  40. data/lib/legion/extensions/agentic/affect/emotion/runners/gut.rb +102 -0
  41. data/lib/legion/extensions/agentic/affect/emotion/runners/valence.rb +120 -0
  42. data/lib/legion/extensions/agentic/affect/emotion/version.rb +13 -0
  43. data/lib/legion/extensions/agentic/affect/emotion.rb +20 -0
  44. data/lib/legion/extensions/agentic/affect/empathy/client.rb +21 -0
  45. data/lib/legion/extensions/agentic/affect/empathy/helpers/constants.rb +54 -0
  46. data/lib/legion/extensions/agentic/affect/empathy/helpers/mental_model.rb +185 -0
  47. data/lib/legion/extensions/agentic/affect/empathy/helpers/model_store.rb +88 -0
  48. data/lib/legion/extensions/agentic/affect/empathy/runners/empathy.rb +173 -0
  49. data/lib/legion/extensions/agentic/affect/empathy/version.rb +13 -0
  50. data/lib/legion/extensions/agentic/affect/empathy.rb +20 -0
  51. data/lib/legion/extensions/agentic/affect/fatigue/client.rb +26 -0
  52. data/lib/legion/extensions/agentic/affect/fatigue/helpers/constants.rb +54 -0
  53. data/lib/legion/extensions/agentic/affect/fatigue/helpers/energy_model.rb +181 -0
  54. data/lib/legion/extensions/agentic/affect/fatigue/helpers/fatigue_store.rb +146 -0
  55. data/lib/legion/extensions/agentic/affect/fatigue/runners/fatigue.rb +89 -0
  56. data/lib/legion/extensions/agentic/affect/fatigue/version.rb +13 -0
  57. data/lib/legion/extensions/agentic/affect/fatigue.rb +19 -0
  58. data/lib/legion/extensions/agentic/affect/flow/client.rb +25 -0
  59. data/lib/legion/extensions/agentic/affect/flow/helpers/constants.rb +84 -0
  60. data/lib/legion/extensions/agentic/affect/flow/helpers/flow_detector.rb +166 -0
  61. data/lib/legion/extensions/agentic/affect/flow/runners/flow.rb +129 -0
  62. data/lib/legion/extensions/agentic/affect/flow/version.rb +13 -0
  63. data/lib/legion/extensions/agentic/affect/flow.rb +18 -0
  64. data/lib/legion/extensions/agentic/affect/interoception/actors/decay.rb +45 -0
  65. data/lib/legion/extensions/agentic/affect/interoception/client.rb +28 -0
  66. data/lib/legion/extensions/agentic/affect/interoception/helpers/body_budget.rb +152 -0
  67. data/lib/legion/extensions/agentic/affect/interoception/helpers/constants.rb +68 -0
  68. data/lib/legion/extensions/agentic/affect/interoception/helpers/somatic_marker.rb +75 -0
  69. data/lib/legion/extensions/agentic/affect/interoception/runners/interoception.rb +101 -0
  70. data/lib/legion/extensions/agentic/affect/interoception/version.rb +13 -0
  71. data/lib/legion/extensions/agentic/affect/interoception.rb +20 -0
  72. data/lib/legion/extensions/agentic/affect/mood/client.rb +21 -0
  73. data/lib/legion/extensions/agentic/affect/mood/helpers/constants.rb +78 -0
  74. data/lib/legion/extensions/agentic/affect/mood/helpers/mood_state.rb +154 -0
  75. data/lib/legion/extensions/agentic/affect/mood/runners/mood.rb +122 -0
  76. data/lib/legion/extensions/agentic/affect/mood/version.rb +13 -0
  77. data/lib/legion/extensions/agentic/affect/mood.rb +18 -0
  78. data/lib/legion/extensions/agentic/affect/motivation/client.rb +26 -0
  79. data/lib/legion/extensions/agentic/affect/motivation/helpers/constants.rb +48 -0
  80. data/lib/legion/extensions/agentic/affect/motivation/helpers/drive_state.rb +98 -0
  81. data/lib/legion/extensions/agentic/affect/motivation/helpers/motivation_store.rb +106 -0
  82. data/lib/legion/extensions/agentic/affect/motivation/runners/motivation.rb +165 -0
  83. data/lib/legion/extensions/agentic/affect/motivation/version.rb +13 -0
  84. data/lib/legion/extensions/agentic/affect/motivation.rb +19 -0
  85. data/lib/legion/extensions/agentic/affect/reappraisal/actors/auto_regulate.rb +45 -0
  86. data/lib/legion/extensions/agentic/affect/reappraisal/client.rb +28 -0
  87. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/constants.rb +82 -0
  88. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/emotional_event.rb +98 -0
  89. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/llm_enhancer.rb +88 -0
  90. data/lib/legion/extensions/agentic/affect/reappraisal/helpers/reappraisal_engine.rb +153 -0
  91. data/lib/legion/extensions/agentic/affect/reappraisal/runners/cognitive_reappraisal.rb +164 -0
  92. data/lib/legion/extensions/agentic/affect/reappraisal/version.rb +13 -0
  93. data/lib/legion/extensions/agentic/affect/reappraisal.rb +20 -0
  94. data/lib/legion/extensions/agentic/affect/regulation/client.rb +25 -0
  95. data/lib/legion/extensions/agentic/affect/regulation/helpers/constants.rb +71 -0
  96. data/lib/legion/extensions/agentic/affect/regulation/helpers/regulation_model.rb +175 -0
  97. data/lib/legion/extensions/agentic/affect/regulation/runners/emotional_regulation.rb +127 -0
  98. data/lib/legion/extensions/agentic/affect/regulation/version.rb +13 -0
  99. data/lib/legion/extensions/agentic/affect/regulation.rb +18 -0
  100. data/lib/legion/extensions/agentic/affect/resilience/client.rb +27 -0
  101. data/lib/legion/extensions/agentic/affect/resilience/helpers/adversity_tracker.rb +130 -0
  102. data/lib/legion/extensions/agentic/affect/resilience/helpers/constants.rb +79 -0
  103. data/lib/legion/extensions/agentic/affect/resilience/helpers/resilience_model.rb +165 -0
  104. data/lib/legion/extensions/agentic/affect/resilience/runners/resilience.rb +150 -0
  105. data/lib/legion/extensions/agentic/affect/resilience/version.rb +13 -0
  106. data/lib/legion/extensions/agentic/affect/resilience.rb +19 -0
  107. data/lib/legion/extensions/agentic/affect/resonance/client.rb +24 -0
  108. data/lib/legion/extensions/agentic/affect/resonance/helpers/category.rb +75 -0
  109. data/lib/legion/extensions/agentic/affect/resonance/helpers/constants.rb +47 -0
  110. data/lib/legion/extensions/agentic/affect/resonance/helpers/resonance_engine.rb +115 -0
  111. data/lib/legion/extensions/agentic/affect/resonance/runners/cognitive_resonance.rb +94 -0
  112. data/lib/legion/extensions/agentic/affect/resonance/version.rb +13 -0
  113. data/lib/legion/extensions/agentic/affect/resonance.rb +19 -0
  114. data/lib/legion/extensions/agentic/affect/reward/client.rb +26 -0
  115. data/lib/legion/extensions/agentic/affect/reward/helpers/constants.rb +67 -0
  116. data/lib/legion/extensions/agentic/affect/reward/helpers/reward_signal.rb +178 -0
  117. data/lib/legion/extensions/agentic/affect/reward/helpers/reward_store.rb +142 -0
  118. data/lib/legion/extensions/agentic/affect/reward/runners/reward.rb +92 -0
  119. data/lib/legion/extensions/agentic/affect/reward/version.rb +13 -0
  120. data/lib/legion/extensions/agentic/affect/reward.rb +19 -0
  121. data/lib/legion/extensions/agentic/affect/somatic_marker/actors/decay.rb +45 -0
  122. data/lib/legion/extensions/agentic/affect/somatic_marker/client.rb +29 -0
  123. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/body_state.rb +69 -0
  124. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/constants.rb +43 -0
  125. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/marker_store.rb +160 -0
  126. data/lib/legion/extensions/agentic/affect/somatic_marker/helpers/somatic_marker.rb +74 -0
  127. data/lib/legion/extensions/agentic/affect/somatic_marker/runners/somatic_marker.rb +132 -0
  128. data/lib/legion/extensions/agentic/affect/somatic_marker/version.rb +13 -0
  129. data/lib/legion/extensions/agentic/affect/somatic_marker.rb +20 -0
  130. data/lib/legion/extensions/agentic/affect/version.rb +11 -0
  131. data/lib/legion/extensions/agentic/affect.rb +34 -0
  132. data/spec/legion/extensions/agentic/affect/appraisal/client_spec.rb +52 -0
  133. data/spec/legion/extensions/agentic/affect/appraisal/helpers/appraisal_engine_spec.rb +161 -0
  134. data/spec/legion/extensions/agentic/affect/appraisal/helpers/appraisal_spec.rb +175 -0
  135. data/spec/legion/extensions/agentic/affect/appraisal/helpers/constants_spec.rb +49 -0
  136. data/spec/legion/extensions/agentic/affect/appraisal/runners/appraisal_spec.rb +116 -0
  137. data/spec/legion/extensions/agentic/affect/cognitive_empathy/client_spec.rb +62 -0
  138. data/spec/legion/extensions/agentic/affect/cognitive_empathy/helpers/empathy_engine_spec.rb +316 -0
  139. data/spec/legion/extensions/agentic/affect/cognitive_empathy/helpers/perspective_spec.rb +132 -0
  140. data/spec/legion/extensions/agentic/affect/cognitive_empathy/runners/cognitive_empathy_spec.rb +200 -0
  141. data/spec/legion/extensions/agentic/affect/contagion/client_spec.rb +63 -0
  142. data/spec/legion/extensions/agentic/affect/contagion/helpers/constants_spec.rb +86 -0
  143. data/spec/legion/extensions/agentic/affect/contagion/helpers/contagion_engine_spec.rb +241 -0
  144. data/spec/legion/extensions/agentic/affect/contagion/helpers/meme_spec.rb +160 -0
  145. data/spec/legion/extensions/agentic/affect/contagion/runners/cognitive_contagion_spec.rb +211 -0
  146. data/spec/legion/extensions/agentic/affect/defusion/client_spec.rb +80 -0
  147. data/spec/legion/extensions/agentic/affect/defusion/helpers/constants_spec.rb +84 -0
  148. data/spec/legion/extensions/agentic/affect/defusion/helpers/defusion_engine_spec.rb +250 -0
  149. data/spec/legion/extensions/agentic/affect/defusion/helpers/thought_spec.rb +178 -0
  150. data/spec/legion/extensions/agentic/affect/defusion/runners/cognitive_defusion_spec.rb +185 -0
  151. data/spec/legion/extensions/agentic/affect/emotion/actors/momentum_decay_spec.rb +46 -0
  152. data/spec/legion/extensions/agentic/affect/emotion/client_spec.rb +46 -0
  153. data/spec/legion/extensions/agentic/affect/emotion/helpers/baseline_spec.rb +48 -0
  154. data/spec/legion/extensions/agentic/affect/emotion/helpers/momentum_spec.rb +45 -0
  155. data/spec/legion/extensions/agentic/affect/emotion/helpers/valence_spec.rb +91 -0
  156. data/spec/legion/extensions/agentic/affect/emotion/runners/gut_spec.rb +73 -0
  157. data/spec/legion/extensions/agentic/affect/emotion/runners/valence_spec.rb +67 -0
  158. data/spec/legion/extensions/agentic/affect/empathy/client_spec.rb +20 -0
  159. data/spec/legion/extensions/agentic/affect/empathy/helpers/constants_spec.rb +23 -0
  160. data/spec/legion/extensions/agentic/affect/empathy/helpers/mental_model_spec.rb +150 -0
  161. data/spec/legion/extensions/agentic/affect/empathy/helpers/model_store_spec.rb +94 -0
  162. data/spec/legion/extensions/agentic/affect/empathy/runners/empathy_spec.rb +127 -0
  163. data/spec/legion/extensions/agentic/affect/fatigue/client_spec.rb +66 -0
  164. data/spec/legion/extensions/agentic/affect/fatigue/helpers/constants_spec.rb +130 -0
  165. data/spec/legion/extensions/agentic/affect/fatigue/helpers/energy_model_spec.rb +281 -0
  166. data/spec/legion/extensions/agentic/affect/fatigue/helpers/fatigue_store_spec.rb +157 -0
  167. data/spec/legion/extensions/agentic/affect/fatigue/runners/fatigue_spec.rb +127 -0
  168. data/spec/legion/extensions/agentic/affect/flow/client_spec.rb +58 -0
  169. data/spec/legion/extensions/agentic/affect/flow/helpers/constants_spec.rb +112 -0
  170. data/spec/legion/extensions/agentic/affect/flow/helpers/flow_detector_spec.rb +268 -0
  171. data/spec/legion/extensions/agentic/affect/flow/runners/flow_spec.rb +222 -0
  172. data/spec/legion/extensions/agentic/affect/interoception/client_spec.rb +52 -0
  173. data/spec/legion/extensions/agentic/affect/interoception/helpers/body_budget_spec.rb +178 -0
  174. data/spec/legion/extensions/agentic/affect/interoception/helpers/somatic_marker_spec.rb +120 -0
  175. data/spec/legion/extensions/agentic/affect/interoception/runners/interoception_spec.rb +108 -0
  176. data/spec/legion/extensions/agentic/affect/mood/client_spec.rb +20 -0
  177. data/spec/legion/extensions/agentic/affect/mood/helpers/constants_spec.rb +29 -0
  178. data/spec/legion/extensions/agentic/affect/mood/helpers/mood_state_spec.rb +94 -0
  179. data/spec/legion/extensions/agentic/affect/mood/runners/mood_spec.rb +71 -0
  180. data/spec/legion/extensions/agentic/affect/motivation/client_spec.rb +35 -0
  181. data/spec/legion/extensions/agentic/affect/motivation/helpers/constants_spec.rb +111 -0
  182. data/spec/legion/extensions/agentic/affect/motivation/helpers/drive_state_spec.rb +183 -0
  183. data/spec/legion/extensions/agentic/affect/motivation/helpers/motivation_store_spec.rb +185 -0
  184. data/spec/legion/extensions/agentic/affect/motivation/runners/motivation_spec.rb +248 -0
  185. data/spec/legion/extensions/agentic/affect/reappraisal/actors/auto_regulate_spec.rb +46 -0
  186. data/spec/legion/extensions/agentic/affect/reappraisal/client_spec.rb +64 -0
  187. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/constants_spec.rb +102 -0
  188. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/emotional_event_spec.rb +177 -0
  189. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/llm_enhancer_spec.rb +161 -0
  190. data/spec/legion/extensions/agentic/affect/reappraisal/helpers/reappraisal_engine_spec.rb +211 -0
  191. data/spec/legion/extensions/agentic/affect/reappraisal/runners/cognitive_reappraisal_spec.rb +312 -0
  192. data/spec/legion/extensions/agentic/affect/regulation/client_spec.rb +61 -0
  193. data/spec/legion/extensions/agentic/affect/regulation/helpers/constants_spec.rb +108 -0
  194. data/spec/legion/extensions/agentic/affect/regulation/helpers/regulation_model_spec.rb +200 -0
  195. data/spec/legion/extensions/agentic/affect/regulation/runners/emotional_regulation_spec.rb +190 -0
  196. data/spec/legion/extensions/agentic/affect/resilience/client_spec.rb +36 -0
  197. data/spec/legion/extensions/agentic/affect/resilience/helpers/adversity_tracker_spec.rb +164 -0
  198. data/spec/legion/extensions/agentic/affect/resilience/helpers/constants_spec.rb +78 -0
  199. data/spec/legion/extensions/agentic/affect/resilience/helpers/resilience_model_spec.rb +133 -0
  200. data/spec/legion/extensions/agentic/affect/resilience/runners/resilience_spec.rb +150 -0
  201. data/spec/legion/extensions/agentic/affect/resonance/client_spec.rb +66 -0
  202. data/spec/legion/extensions/agentic/affect/resonance/cognitive_resonance_spec.rb +27 -0
  203. data/spec/legion/extensions/agentic/affect/resonance/helpers/category_spec.rb +146 -0
  204. data/spec/legion/extensions/agentic/affect/resonance/helpers/constants_spec.rb +104 -0
  205. data/spec/legion/extensions/agentic/affect/resonance/helpers/resonance_engine_spec.rb +189 -0
  206. data/spec/legion/extensions/agentic/affect/resonance/runners/cognitive_resonance_spec.rb +197 -0
  207. data/spec/legion/extensions/agentic/affect/reward/client_spec.rb +42 -0
  208. data/spec/legion/extensions/agentic/affect/reward/helpers/constants_spec.rb +91 -0
  209. data/spec/legion/extensions/agentic/affect/reward/helpers/reward_signal_spec.rb +296 -0
  210. data/spec/legion/extensions/agentic/affect/reward/helpers/reward_store_spec.rb +167 -0
  211. data/spec/legion/extensions/agentic/affect/reward/runners/reward_spec.rb +149 -0
  212. data/spec/legion/extensions/agentic/affect/somatic_marker/client_spec.rb +83 -0
  213. data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/body_state_spec.rb +155 -0
  214. data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/marker_store_spec.rb +233 -0
  215. data/spec/legion/extensions/agentic/affect/somatic_marker/helpers/somatic_marker_spec.rb +172 -0
  216. data/spec/legion/extensions/agentic/affect/somatic_marker/runners/somatic_marker_spec.rb +181 -0
  217. data/spec/spec_helper.rb +46 -0
  218. metadata +302 -0
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Affect::Fatigue::Helpers::EnergyModel do
4
+ subject(:model) { described_class.new }
5
+
6
+ let(:constants) { Legion::Extensions::Agentic::Affect::Fatigue::Helpers::Constants }
7
+
8
+ describe '#initialize' do
9
+ it 'starts at MAX_ENERGY' do
10
+ expect(model.energy).to eq(constants::MAX_ENERGY)
11
+ end
12
+
13
+ it 'starts as :fresh' do
14
+ expect(model.fatigue_level).to eq(:fresh)
15
+ end
16
+
17
+ it 'starts with zero ticks' do
18
+ expect(model.ticks_active).to eq(0)
19
+ expect(model.ticks_resting).to eq(0)
20
+ end
21
+
22
+ it 'starts without burnout' do
23
+ expect(model.burnout).to eq(false)
24
+ end
25
+
26
+ it 'starts with no recovery mode' do
27
+ expect(model.recovery_mode).to be_nil
28
+ end
29
+
30
+ it 'starts with empty history' do
31
+ expect(model.history).to be_empty
32
+ end
33
+
34
+ it 'sets peak_energy to MAX_ENERGY' do
35
+ expect(model.peak_energy).to eq(constants::MAX_ENERGY)
36
+ end
37
+ end
38
+
39
+ describe '#tick (active)' do
40
+ it 'reduces energy on an active tick' do
41
+ initial = model.energy
42
+ model.tick(cognitive_load: 0.5, emotional_arousal: 0.5, is_resting: false)
43
+ expect(model.energy).to be < initial
44
+ end
45
+
46
+ it 'increments ticks_active' do
47
+ model.tick(is_resting: false)
48
+ expect(model.ticks_active).to eq(1)
49
+ end
50
+
51
+ it 'does not increment ticks_resting on active tick' do
52
+ model.tick(is_resting: false)
53
+ expect(model.ticks_resting).to eq(0)
54
+ end
55
+
56
+ it 'drains more with high cognitive load' do
57
+ m1 = described_class.new
58
+ m2 = described_class.new
59
+ m1.tick(cognitive_load: 0.1, emotional_arousal: 0.5, is_resting: false)
60
+ m2.tick(cognitive_load: 0.9, emotional_arousal: 0.5, is_resting: false)
61
+ expect(m2.energy).to be < m1.energy
62
+ end
63
+
64
+ it 'drains more with high emotional arousal' do
65
+ m1 = described_class.new
66
+ m2 = described_class.new
67
+ m1.tick(cognitive_load: 0.5, emotional_arousal: 0.1, is_resting: false)
68
+ m2.tick(cognitive_load: 0.5, emotional_arousal: 0.9, is_resting: false)
69
+ expect(m2.energy).to be < m1.energy
70
+ end
71
+
72
+ it 'records a history snapshot' do
73
+ model.tick
74
+ expect(model.history.size).to eq(1)
75
+ end
76
+
77
+ it 'returns a hash with energy and fatigue_level' do
78
+ result = model.tick
79
+ expect(result).to include(:energy, :fatigue_level, :performance_factor)
80
+ end
81
+ end
82
+
83
+ describe '#tick (resting)' do
84
+ before do
85
+ # Drain some energy first
86
+ 20.times { model.tick(cognitive_load: 0.8, emotional_arousal: 0.8, is_resting: false) }
87
+ end
88
+
89
+ it 'recovers energy on a rest tick' do
90
+ drained = model.energy
91
+ model.tick(is_resting: true)
92
+ expect(model.energy).to be > drained
93
+ end
94
+
95
+ it 'increments ticks_resting' do
96
+ model.tick(is_resting: true)
97
+ expect(model.ticks_resting).to be >= 1
98
+ end
99
+
100
+ it 'resets consecutive_low_ticks when resting' do
101
+ model.tick(is_resting: true)
102
+ expect(model.consecutive_low_ticks).to eq(0)
103
+ end
104
+ end
105
+
106
+ describe '#fatigue_level classification' do
107
+ it 'returns :fresh when energy is high' do
108
+ expect(model.fatigue_level).to eq(:fresh)
109
+ end
110
+
111
+ it 'returns :depleted when energy is very low' do
112
+ model.instance_variable_set(:@energy, 0.05)
113
+ model.send(:classify_fatigue)
114
+ expect(model.fatigue_level).to eq(:depleted)
115
+ end
116
+
117
+ it 'returns :alert when energy is between alert and fresh thresholds' do
118
+ # Drain to alert range (0.6-0.8)
119
+ model.instance_variable_set(:@energy, 0.7)
120
+ model.send(:classify_fatigue)
121
+ expect(model.fatigue_level).to eq(:alert)
122
+ end
123
+
124
+ it 'returns :tired when energy is in tired range' do
125
+ model.instance_variable_set(:@energy, 0.5)
126
+ model.send(:classify_fatigue)
127
+ expect(model.fatigue_level).to eq(:tired)
128
+ end
129
+
130
+ it 'returns :exhausted when energy is in exhausted range' do
131
+ model.instance_variable_set(:@energy, 0.3)
132
+ model.send(:classify_fatigue)
133
+ expect(model.fatigue_level).to eq(:exhausted)
134
+ end
135
+ end
136
+
137
+ describe '#performance_factor' do
138
+ it 'returns 1.0 when fresh' do
139
+ expect(model.performance_factor).to eq(1.0)
140
+ end
141
+
142
+ it 'returns degraded performance when tired' do
143
+ model.instance_variable_set(:@energy, 0.5)
144
+ model.send(:classify_fatigue)
145
+ expect(model.performance_factor).to be < 1.0
146
+ end
147
+ end
148
+
149
+ describe '#needs_rest?' do
150
+ it 'returns false when energy is high' do
151
+ expect(model.needs_rest?).to eq(false)
152
+ end
153
+
154
+ it 'returns true when energy is below REST_THRESHOLD' do
155
+ model.instance_variable_set(:@energy, constants::REST_THRESHOLD - 0.01)
156
+ expect(model.needs_rest?).to eq(true)
157
+ end
158
+ end
159
+
160
+ describe '#critically_fatigued?' do
161
+ it 'returns false when energy is normal' do
162
+ expect(model.critically_fatigued?).to eq(false)
163
+ end
164
+
165
+ it 'returns true when energy is below CRITICAL_THRESHOLD' do
166
+ model.instance_variable_set(:@energy, constants::CRITICAL_THRESHOLD - 0.01)
167
+ expect(model.critically_fatigued?).to eq(true)
168
+ end
169
+ end
170
+
171
+ describe '#burnout?' do
172
+ it 'returns false initially' do
173
+ expect(model.burnout?).to eq(false)
174
+ end
175
+
176
+ it 'returns true after BURNOUT_THRESHOLD consecutive low-energy ticks' do
177
+ model.instance_variable_set(:@energy, 0.1)
178
+ model.instance_variable_set(:@consecutive_low_ticks, constants::BURNOUT_THRESHOLD + 1)
179
+ model.send(:check_burnout)
180
+ expect(model.burnout?).to eq(true)
181
+ end
182
+ end
183
+
184
+ describe '#enter_recovery' do
185
+ it 'sets recovery mode to a valid mode' do
186
+ model.enter_recovery(:full_rest)
187
+ expect(model.recovery_mode).to eq(:full_rest)
188
+ end
189
+
190
+ it 'rejects invalid recovery modes' do
191
+ model.enter_recovery(:turbo_nap)
192
+ expect(model.recovery_mode).to be_nil
193
+ end
194
+
195
+ it 'accepts all defined recovery modes' do
196
+ constants::RECOVERY_MODES.each do |mode|
197
+ m = described_class.new
198
+ m.enter_recovery(mode)
199
+ expect(m.recovery_mode).to eq(mode)
200
+ end
201
+ end
202
+ end
203
+
204
+ describe '#exit_recovery' do
205
+ it 'clears the recovery mode' do
206
+ model.enter_recovery(:sleep)
207
+ model.exit_recovery
208
+ expect(model.recovery_mode).to be_nil
209
+ end
210
+ end
211
+
212
+ describe '#time_to_rest_threshold' do
213
+ it 'returns 0 when already needs rest' do
214
+ model.instance_variable_set(:@energy, 0.1)
215
+ expect(model.time_to_rest_threshold).to eq(0)
216
+ end
217
+
218
+ it 'returns a positive integer when energy is above REST_THRESHOLD' do
219
+ expect(model.time_to_rest_threshold).to be > 0
220
+ end
221
+ end
222
+
223
+ describe '#time_to_full_recovery' do
224
+ it 'returns 0 when already at MAX_ENERGY' do
225
+ expect(model.time_to_full_recovery).to eq(0)
226
+ end
227
+
228
+ it 'returns a positive integer when below MAX_ENERGY' do
229
+ model.instance_variable_set(:@energy, 0.5)
230
+ expect(model.time_to_full_recovery).to be > 0
231
+ end
232
+
233
+ it 'recovers faster with sleep mode' do
234
+ m1 = described_class.new
235
+ m2 = described_class.new
236
+ m1.instance_variable_set(:@energy, 0.5)
237
+ m2.instance_variable_set(:@energy, 0.5)
238
+ m1.enter_recovery(:active_rest)
239
+ m2.enter_recovery(:sleep)
240
+ expect(m2.time_to_full_recovery).to be < m1.time_to_full_recovery
241
+ end
242
+ end
243
+
244
+ describe '#trend' do
245
+ it 'returns :stable with less than 5 history entries' do
246
+ 3.times { model.tick }
247
+ expect(model.trend).to eq(:stable)
248
+ end
249
+
250
+ it 'returns :draining after continuous active ticks' do
251
+ # Ensure 5+ history entries with declining energy
252
+ 10.times { model.tick(cognitive_load: 0.9, emotional_arousal: 0.9, is_resting: false) }
253
+ expect(model.trend).to eq(:draining)
254
+ end
255
+
256
+ it 'returns :recovering after rest ticks from low energy' do
257
+ model.instance_variable_set(:@energy, 0.2)
258
+ 10.times { model.tick(is_resting: true) }
259
+ expect(model.trend).to eq(:recovering)
260
+ end
261
+ end
262
+
263
+ describe 'history capping' do
264
+ it 'does not exceed MAX_HISTORY entries' do
265
+ (constants::MAX_HISTORY + 20).times { model.tick }
266
+ expect(model.history.size).to eq(constants::MAX_HISTORY)
267
+ end
268
+ end
269
+
270
+ describe '#to_h' do
271
+ it 'returns a hash with all expected keys' do
272
+ result = model.to_h
273
+ expect(result).to include(
274
+ :energy, :fatigue_level, :performance_factor,
275
+ :needs_rest, :critically_fatigued, :burnout,
276
+ :recovery_mode, :peak_energy, :ticks_active,
277
+ :ticks_resting, :trend, :history_size
278
+ )
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Affect::Fatigue::Helpers::FatigueStore do
4
+ subject(:store) { described_class.new }
5
+
6
+ let(:constants) { Legion::Extensions::Agentic::Affect::Fatigue::Helpers::Constants }
7
+
8
+ describe '#initialize' do
9
+ it 'creates a default EnergyModel' do
10
+ expect(store.model).to be_a(Legion::Extensions::Agentic::Affect::Fatigue::Helpers::EnergyModel)
11
+ end
12
+
13
+ it 'accepts an injected model' do
14
+ custom_model = Legion::Extensions::Agentic::Affect::Fatigue::Helpers::EnergyModel.new
15
+ s = described_class.new(model: custom_model)
16
+ expect(s.model).to be(custom_model)
17
+ end
18
+
19
+ it 'records session start time' do
20
+ expect(store.session_start).to be_a(Time)
21
+ end
22
+
23
+ it 'starts with zero ticks' do
24
+ expect(store.total_active_ticks).to eq(0)
25
+ expect(store.total_rest_ticks).to eq(0)
26
+ end
27
+ end
28
+
29
+ describe '#update' do
30
+ it 'calls model.tick and returns results' do
31
+ result = store.update(tick_results: { cognitive_load: 0.5, emotional_arousal: 0.5 })
32
+ expect(result).to include(:energy, :fatigue_level)
33
+ end
34
+
35
+ it 'increments total_active_ticks on active update' do
36
+ store.update(tick_results: { cognitive_load: 0.5 })
37
+ expect(store.total_active_ticks).to eq(1)
38
+ end
39
+
40
+ it 'increments total_rest_ticks when mode is :resting' do
41
+ store.update(tick_results: { mode: :resting })
42
+ expect(store.total_rest_ticks).to eq(1)
43
+ end
44
+
45
+ it 'uses default cognitive_load and emotional_arousal when not provided' do
46
+ expect { store.update(tick_results: {}) }.not_to raise_error
47
+ end
48
+
49
+ it 'increments rest ticks when recovery_mode is set on model' do
50
+ store.model.enter_recovery(:full_rest)
51
+ store.update(tick_results: {})
52
+ expect(store.total_rest_ticks).to be >= 1
53
+ end
54
+ end
55
+
56
+ describe '#recommend_action' do
57
+ it 'returns :continue when energy is high' do
58
+ expect(store.recommend_action).to eq(:continue)
59
+ end
60
+
61
+ it 'returns :reduce_load when tired' do
62
+ store.model.instance_variable_set(:@energy, 0.5)
63
+ store.model.instance_variable_set(:@fatigue_level, :tired)
64
+ expect(store.recommend_action).to eq(:reduce_load)
65
+ end
66
+
67
+ it 'returns :take_break when needs_rest' do
68
+ store.model.instance_variable_set(:@energy, constants::REST_THRESHOLD - 0.01)
69
+ expect(store.recommend_action).to eq(:take_break)
70
+ end
71
+
72
+ it 'returns :enter_rest when critically fatigued' do
73
+ store.model.instance_variable_set(:@energy, constants::CRITICAL_THRESHOLD - 0.01)
74
+ expect(store.recommend_action).to eq(:enter_rest)
75
+ end
76
+
77
+ it 'returns :emergency_shutdown when burnout' do
78
+ store.model.instance_variable_set(:@burnout, true)
79
+ expect(store.recommend_action).to eq(:emergency_shutdown)
80
+ end
81
+
82
+ it 'returns :emergency_shutdown when critically fatigued with burnout' do
83
+ store.model.instance_variable_set(:@energy, 0.01)
84
+ store.model.instance_variable_set(:@burnout, true)
85
+ expect(store.recommend_action).to eq(:emergency_shutdown)
86
+ end
87
+ end
88
+
89
+ describe '#session_stats' do
90
+ it 'returns a hash with expected keys' do
91
+ result = store.session_stats
92
+ expect(result).to include(
93
+ :duration_seconds, :total_ticks, :active_ticks, :rest_ticks,
94
+ :active_ratio, :current_energy, :fatigue_level, :burnout
95
+ )
96
+ end
97
+
98
+ it 'reflects updated tick counts' do
99
+ 3.times { store.update(tick_results: {}) }
100
+ stats = store.session_stats
101
+ expect(stats[:active_ticks]).to eq(3)
102
+ expect(stats[:total_ticks]).to eq(3)
103
+ end
104
+
105
+ it 'reports active_ratio of 0.0 before any ticks' do
106
+ expect(store.session_stats[:active_ratio]).to eq(0.0)
107
+ end
108
+ end
109
+
110
+ describe '#energy_forecast' do
111
+ it 'returns forecast for requested number of ticks' do
112
+ result = store.energy_forecast(ticks: 10)
113
+ expect(result[:forecast].size).to eq(10)
114
+ end
115
+
116
+ it 'includes current_energy and ticks_to_rest' do
117
+ result = store.energy_forecast(ticks: 5)
118
+ expect(result).to include(:current_energy, :ticks_to_rest, :forecast)
119
+ end
120
+
121
+ it 'energy in forecast is non-increasing (draining scenario)' do
122
+ result = store.energy_forecast(ticks: 5)
123
+ energies = result[:forecast].map { |f| f[:energy] }
124
+ energies.each_cons(2) do |a, b|
125
+ expect(b).to be <= a
126
+ end
127
+ end
128
+
129
+ it 'each forecast entry includes tick, energy, and fatigue_level' do
130
+ result = store.energy_forecast(ticks: 3)
131
+ result[:forecast].each do |entry|
132
+ expect(entry).to include(:tick, :energy, :fatigue_level)
133
+ end
134
+ end
135
+ end
136
+
137
+ describe '#optimal_rest_schedule' do
138
+ it 'returns schedule hash with expected keys' do
139
+ result = store.optimal_rest_schedule
140
+ expect(result).to include(
141
+ :recommend_rest_in, :full_recovery_ticks, :current_energy,
142
+ :recommended_mode, :trend
143
+ )
144
+ end
145
+
146
+ it 'recommends sleep mode for critically low energy' do
147
+ store.model.instance_variable_set(:@energy, constants::CRITICAL_THRESHOLD - 0.01)
148
+ result = store.optimal_rest_schedule
149
+ expect(result[:recommended_mode]).to eq(:sleep)
150
+ end
151
+
152
+ it 'recommends active_rest when energy is normal' do
153
+ result = store.optimal_rest_schedule
154
+ expect(result[:recommended_mode]).to eq(:active_rest)
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Affect::Fatigue::Runners::Fatigue do
4
+ let(:client) { Legion::Extensions::Agentic::Affect::Fatigue::Client.new }
5
+
6
+ describe '#update_fatigue' do
7
+ it 'returns energy, fatigue_level, performance_factor, and recommendation' do
8
+ result = client.update_fatigue(tick_results: { cognitive_load: 0.5, emotional_arousal: 0.5 })
9
+ expect(result).to include(:energy, :fatigue_level, :performance_factor, :recommendation)
10
+ end
11
+
12
+ it 'accepts empty tick_results' do
13
+ expect { client.update_fatigue(tick_results: {}) }.not_to raise_error
14
+ end
15
+
16
+ it 'accepts no arguments' do
17
+ expect { client.update_fatigue }.not_to raise_error
18
+ end
19
+
20
+ it 'returns a valid fatigue level symbol' do
21
+ result = client.update_fatigue
22
+ expected_levels = Legion::Extensions::Agentic::Affect::Fatigue::Helpers::Constants::FATIGUE_LEVELS.keys
23
+ expect(expected_levels).to include(result[:fatigue_level])
24
+ end
25
+
26
+ it 'returns a valid recommendation symbol' do
27
+ result = client.update_fatigue
28
+ valid_recs = %i[continue reduce_load take_break enter_rest emergency_shutdown]
29
+ expect(valid_recs).to include(result[:recommendation])
30
+ end
31
+
32
+ it 'includes needs_rest and burnout in result' do
33
+ result = client.update_fatigue
34
+ expect(result).to include(:needs_rest, :burnout)
35
+ end
36
+ end
37
+
38
+ describe '#energy_status' do
39
+ it 'returns current energy state' do
40
+ result = client.energy_status
41
+ expect(result).to include(
42
+ :energy, :fatigue_level, :performance_factor,
43
+ :needs_rest, :critically_fatigued, :burnout, :recovery_mode, :trend
44
+ )
45
+ end
46
+
47
+ it 'starts fresh' do
48
+ result = client.energy_status
49
+ expect(result[:fatigue_level]).to eq(:fresh)
50
+ expect(result[:burnout]).to eq(false)
51
+ end
52
+ end
53
+
54
+ describe '#enter_rest' do
55
+ it 'sets recovery mode on the model' do
56
+ result = client.enter_rest(mode: :full_rest)
57
+ expect(result[:success]).to eq(true)
58
+ expect(result[:mode]).to eq(:full_rest)
59
+ end
60
+
61
+ it 'defaults to :full_rest mode' do
62
+ result = client.enter_rest
63
+ expect(result[:mode]).to eq(:full_rest)
64
+ end
65
+
66
+ it 'returns error for unknown mode' do
67
+ result = client.enter_rest(mode: :hyper_sleep)
68
+ expect(result[:success]).to eq(false)
69
+ expect(result[:error]).to include('hyper_sleep')
70
+ end
71
+
72
+ it 'accepts all valid recovery modes' do
73
+ Legion::Extensions::Agentic::Affect::Fatigue::Helpers::Constants::RECOVERY_MODES.each do |mode|
74
+ c = Legion::Extensions::Agentic::Affect::Fatigue::Client.new
75
+ result = c.enter_rest(mode: mode)
76
+ expect(result[:success]).to eq(true)
77
+ end
78
+ end
79
+ end
80
+
81
+ describe '#exit_rest' do
82
+ it 'clears recovery mode' do
83
+ client.enter_rest(mode: :sleep)
84
+ result = client.exit_rest
85
+ expect(result[:success]).to eq(true)
86
+ end
87
+
88
+ it 'returns current energy and fatigue_level' do
89
+ result = client.exit_rest
90
+ expect(result).to include(:energy, :fatigue_level)
91
+ end
92
+ end
93
+
94
+ describe '#energy_forecast' do
95
+ it 'returns forecast for default ticks' do
96
+ result = client.energy_forecast
97
+ expect(result[:forecast]).not_to be_empty
98
+ end
99
+
100
+ it 'returns forecast for specified ticks' do
101
+ result = client.energy_forecast(ticks: 20)
102
+ expect(result[:forecast].size).to eq(20)
103
+ end
104
+
105
+ it 'includes current_energy and ticks_to_rest' do
106
+ result = client.energy_forecast(ticks: 10)
107
+ expect(result).to include(:current_energy, :ticks_to_rest, :forecast)
108
+ end
109
+ end
110
+
111
+ describe '#fatigue_stats' do
112
+ it 'returns session, history, trend, and schedule' do
113
+ result = client.fatigue_stats
114
+ expect(result).to include(:session, :history, :trend, :schedule)
115
+ end
116
+
117
+ it 'session includes expected keys' do
118
+ result = client.fatigue_stats
119
+ expect(result[:session]).to include(:total_ticks, :current_energy, :fatigue_level, :burnout)
120
+ end
121
+
122
+ it 'trend is a symbol' do
123
+ result = client.fatigue_stats
124
+ expect(result[:trend]).to be_a(Symbol)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Affect::Flow::Client do
6
+ describe '#initialize' do
7
+ it 'creates a default flow detector' do
8
+ client = described_class.new
9
+ expect(client.flow_detector).to be_a(Legion::Extensions::Agentic::Affect::Flow::Helpers::FlowDetector)
10
+ end
11
+
12
+ it 'accepts an injected flow detector' do
13
+ detector = Legion::Extensions::Agentic::Affect::Flow::Helpers::FlowDetector.new
14
+ client = described_class.new(flow_detector: detector)
15
+ expect(client.flow_detector).to be(detector)
16
+ end
17
+
18
+ it 'ignores unknown keyword arguments' do
19
+ expect { described_class.new(unknown: 'value') }.not_to raise_error
20
+ end
21
+ end
22
+
23
+ describe 'runner integration' do
24
+ it 'responds to update_flow' do
25
+ expect(described_class.new).to respond_to(:update_flow)
26
+ end
27
+
28
+ it 'responds to flow_status' do
29
+ expect(described_class.new).to respond_to(:flow_status)
30
+ end
31
+
32
+ it 'responds to flow_effects' do
33
+ expect(described_class.new).to respond_to(:flow_effects)
34
+ end
35
+
36
+ it 'responds to flow_history' do
37
+ expect(described_class.new).to respond_to(:flow_history)
38
+ end
39
+
40
+ it 'responds to flow_stats' do
41
+ expect(described_class.new).to respond_to(:flow_stats)
42
+ end
43
+ end
44
+
45
+ describe 'shared state across calls' do
46
+ it 'accumulates flow state across multiple update_flow calls' do
47
+ client = described_class.new
48
+ tick = {
49
+ prediction_engine: { rolling_accuracy: 0.6, error_rate: 0.2 },
50
+ action_selection: { complexity: 0.5 },
51
+ memory_retrieval: { avg_strength: 0.6 },
52
+ habit: { automation_level: 0.5 }
53
+ }
54
+ 20.times { client.update_flow(tick_results: tick) }
55
+ expect(client.flow_history[:total]).to eq(20)
56
+ end
57
+ end
58
+ end