lex-agentic-self 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 (249) 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-self.gemspec +31 -0
  7. data/lib/legion/extensions/agentic/self/agency/client.rb +21 -0
  8. data/lib/legion/extensions/agentic/self/agency/helpers/constants.rb +77 -0
  9. data/lib/legion/extensions/agentic/self/agency/helpers/efficacy_model.rb +136 -0
  10. data/lib/legion/extensions/agentic/self/agency/helpers/outcome_event.rb +52 -0
  11. data/lib/legion/extensions/agentic/self/agency/runners/agency.rb +117 -0
  12. data/lib/legion/extensions/agentic/self/agency/version.rb +13 -0
  13. data/lib/legion/extensions/agentic/self/agency.rb +19 -0
  14. data/lib/legion/extensions/agentic/self/anchor/client.rb +15 -0
  15. data/lib/legion/extensions/agentic/self/anchor/helpers/anchor.rb +92 -0
  16. data/lib/legion/extensions/agentic/self/anchor/helpers/anchor_engine.rb +123 -0
  17. data/lib/legion/extensions/agentic/self/anchor/helpers/chain.rb +93 -0
  18. data/lib/legion/extensions/agentic/self/anchor/helpers/constants.rb +46 -0
  19. data/lib/legion/extensions/agentic/self/anchor/runners/cognitive_anchor.rb +70 -0
  20. data/lib/legion/extensions/agentic/self/anchor/version.rb +13 -0
  21. data/lib/legion/extensions/agentic/self/anchor.rb +22 -0
  22. data/lib/legion/extensions/agentic/self/anosognosia/client.rb +28 -0
  23. data/lib/legion/extensions/agentic/self/anosognosia/helpers/anosognosia_engine.rb +153 -0
  24. data/lib/legion/extensions/agentic/self/anosognosia/helpers/cognitive_deficit.rb +71 -0
  25. data/lib/legion/extensions/agentic/self/anosognosia/helpers/constants.rb +29 -0
  26. data/lib/legion/extensions/agentic/self/anosognosia/runners/anosognosia.rb +98 -0
  27. data/lib/legion/extensions/agentic/self/anosognosia/version.rb +13 -0
  28. data/lib/legion/extensions/agentic/self/anosognosia.rb +19 -0
  29. data/lib/legion/extensions/agentic/self/architecture/client.rb +19 -0
  30. data/lib/legion/extensions/agentic/self/architecture/helpers/architecture_engine.rb +167 -0
  31. data/lib/legion/extensions/agentic/self/architecture/helpers/connection.rb +57 -0
  32. data/lib/legion/extensions/agentic/self/architecture/helpers/constants.rb +37 -0
  33. data/lib/legion/extensions/agentic/self/architecture/helpers/subsystem.rb +80 -0
  34. data/lib/legion/extensions/agentic/self/architecture/runners/cognitive_architecture.rb +125 -0
  35. data/lib/legion/extensions/agentic/self/architecture/version.rb +13 -0
  36. data/lib/legion/extensions/agentic/self/architecture.rb +20 -0
  37. data/lib/legion/extensions/agentic/self/default_mode_network/actors/idle.rb +45 -0
  38. data/lib/legion/extensions/agentic/self/default_mode_network/client.rb +28 -0
  39. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/constants.rb +53 -0
  40. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/dmn_engine.rb +221 -0
  41. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/wandering_thought.rb +60 -0
  42. data/lib/legion/extensions/agentic/self/default_mode_network/runners/default_mode_network.rb +122 -0
  43. data/lib/legion/extensions/agentic/self/default_mode_network/version.rb +13 -0
  44. data/lib/legion/extensions/agentic/self/default_mode_network.rb +20 -0
  45. data/lib/legion/extensions/agentic/self/fingerprint/client.rb +28 -0
  46. data/lib/legion/extensions/agentic/self/fingerprint/helpers/cognitive_trait.rb +73 -0
  47. data/lib/legion/extensions/agentic/self/fingerprint/helpers/constants.rb +60 -0
  48. data/lib/legion/extensions/agentic/self/fingerprint/helpers/fingerprint_engine.rb +169 -0
  49. data/lib/legion/extensions/agentic/self/fingerprint/runners/cognitive_fingerprint.rb +86 -0
  50. data/lib/legion/extensions/agentic/self/fingerprint/version.rb +13 -0
  51. data/lib/legion/extensions/agentic/self/fingerprint.rb +19 -0
  52. data/lib/legion/extensions/agentic/self/identity/actors/credential_refresh.rb +49 -0
  53. data/lib/legion/extensions/agentic/self/identity/actors/orphan_check.rb +52 -0
  54. data/lib/legion/extensions/agentic/self/identity/client.rb +27 -0
  55. data/lib/legion/extensions/agentic/self/identity/helpers/dimensions.rb +75 -0
  56. data/lib/legion/extensions/agentic/self/identity/helpers/fingerprint.rb +170 -0
  57. data/lib/legion/extensions/agentic/self/identity/helpers/graph_client.rb +29 -0
  58. data/lib/legion/extensions/agentic/self/identity/helpers/graph_token.rb +36 -0
  59. data/lib/legion/extensions/agentic/self/identity/helpers/token_cache.rb +59 -0
  60. data/lib/legion/extensions/agentic/self/identity/helpers/vault_secrets.rb +80 -0
  61. data/lib/legion/extensions/agentic/self/identity/local_migrations/20260316000030_create_fingerprint.rb +20 -0
  62. data/lib/legion/extensions/agentic/self/identity/runners/entra.rb +402 -0
  63. data/lib/legion/extensions/agentic/self/identity/runners/identity.rb +90 -0
  64. data/lib/legion/extensions/agentic/self/identity/version.rb +13 -0
  65. data/lib/legion/extensions/agentic/self/identity.rb +28 -0
  66. data/lib/legion/extensions/agentic/self/metacognition/client.rb +27 -0
  67. data/lib/legion/extensions/agentic/self/metacognition/helpers/constants.rb +377 -0
  68. data/lib/legion/extensions/agentic/self/metacognition/helpers/narrator_bridge.rb +85 -0
  69. data/lib/legion/extensions/agentic/self/metacognition/helpers/registry_store.rb +70 -0
  70. data/lib/legion/extensions/agentic/self/metacognition/helpers/self_model.rb +160 -0
  71. data/lib/legion/extensions/agentic/self/metacognition/helpers/snapshot_store.rb +82 -0
  72. data/lib/legion/extensions/agentic/self/metacognition/runners/metacognition.rb +116 -0
  73. data/lib/legion/extensions/agentic/self/metacognition/runners/registry.rb +180 -0
  74. data/lib/legion/extensions/agentic/self/metacognition/version.rb +13 -0
  75. data/lib/legion/extensions/agentic/self/metacognition.rb +22 -0
  76. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/client.rb +25 -0
  77. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/calibration_tracker.rb +96 -0
  78. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/constants.rb +47 -0
  79. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_engine.rb +141 -0
  80. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_judgment.rb +79 -0
  81. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/runners/metacognitive_monitoring.rb +151 -0
  82. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/version.rb +13 -0
  83. data/lib/legion/extensions/agentic/self/metacognitive_monitoring.rb +20 -0
  84. data/lib/legion/extensions/agentic/self/narrative_arc/client.rb +29 -0
  85. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/arc.rb +137 -0
  86. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/arc_engine.rb +119 -0
  87. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/beat_event.rb +59 -0
  88. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/constants.rb +66 -0
  89. data/lib/legion/extensions/agentic/self/narrative_arc/runners/narrative.rb +101 -0
  90. data/lib/legion/extensions/agentic/self/narrative_arc/version.rb +13 -0
  91. data/lib/legion/extensions/agentic/self/narrative_arc.rb +20 -0
  92. data/lib/legion/extensions/agentic/self/narrative_identity/actors/narrative_decay.rb +45 -0
  93. data/lib/legion/extensions/agentic/self/narrative_identity/client.rb +22 -0
  94. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/chapter.rb +48 -0
  95. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/constants.rb +62 -0
  96. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/episode.rb +67 -0
  97. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/narrative_engine.rb +187 -0
  98. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/theme.rb +50 -0
  99. data/lib/legion/extensions/agentic/self/narrative_identity/runners/narrative_identity.rb +158 -0
  100. data/lib/legion/extensions/agentic/self/narrative_identity/version.rb +13 -0
  101. data/lib/legion/extensions/agentic/self/narrative_identity.rb +21 -0
  102. data/lib/legion/extensions/agentic/self/narrative_self/client.rb +27 -0
  103. data/lib/legion/extensions/agentic/self/narrative_self/helpers/autobiography.rb +187 -0
  104. data/lib/legion/extensions/agentic/self/narrative_self/helpers/constants.rb +42 -0
  105. data/lib/legion/extensions/agentic/self/narrative_self/helpers/episode.rb +81 -0
  106. data/lib/legion/extensions/agentic/self/narrative_self/helpers/narrative_thread.rb +65 -0
  107. data/lib/legion/extensions/agentic/self/narrative_self/runners/narrative_self.rb +86 -0
  108. data/lib/legion/extensions/agentic/self/narrative_self/version.rb +13 -0
  109. data/lib/legion/extensions/agentic/self/narrative_self.rb +20 -0
  110. data/lib/legion/extensions/agentic/self/personality/client.rb +21 -0
  111. data/lib/legion/extensions/agentic/self/personality/helpers/constants.rb +84 -0
  112. data/lib/legion/extensions/agentic/self/personality/helpers/personality_store.rb +126 -0
  113. data/lib/legion/extensions/agentic/self/personality/helpers/trait_model.rb +147 -0
  114. data/lib/legion/extensions/agentic/self/personality/runners/personality.rb +102 -0
  115. data/lib/legion/extensions/agentic/self/personality/version.rb +13 -0
  116. data/lib/legion/extensions/agentic/self/personality.rb +19 -0
  117. data/lib/legion/extensions/agentic/self/reflection/client.rb +27 -0
  118. data/lib/legion/extensions/agentic/self/reflection/helpers/constants.rb +66 -0
  119. data/lib/legion/extensions/agentic/self/reflection/helpers/llm_enhancer.rb +166 -0
  120. data/lib/legion/extensions/agentic/self/reflection/helpers/monitors.rb +186 -0
  121. data/lib/legion/extensions/agentic/self/reflection/helpers/reflection.rb +54 -0
  122. data/lib/legion/extensions/agentic/self/reflection/helpers/reflection_store.rb +99 -0
  123. data/lib/legion/extensions/agentic/self/reflection/runners/reflection.rb +199 -0
  124. data/lib/legion/extensions/agentic/self/reflection/version.rb +13 -0
  125. data/lib/legion/extensions/agentic/self/reflection.rb +21 -0
  126. data/lib/legion/extensions/agentic/self/self_model/client.rb +19 -0
  127. data/lib/legion/extensions/agentic/self/self_model/helpers/capability.rb +93 -0
  128. data/lib/legion/extensions/agentic/self/self_model/helpers/constants.rb +46 -0
  129. data/lib/legion/extensions/agentic/self/self_model/helpers/knowledge_domain.rb +82 -0
  130. data/lib/legion/extensions/agentic/self/self_model/helpers/self_model.rb +150 -0
  131. data/lib/legion/extensions/agentic/self/self_model/runners/self_model.rb +82 -0
  132. data/lib/legion/extensions/agentic/self/self_model/version.rb +13 -0
  133. data/lib/legion/extensions/agentic/self/self_model.rb +21 -0
  134. data/lib/legion/extensions/agentic/self/self_talk/actors/volume_decay.rb +45 -0
  135. data/lib/legion/extensions/agentic/self/self_talk/client.rb +30 -0
  136. data/lib/legion/extensions/agentic/self/self_talk/helpers/constants.rb +63 -0
  137. data/lib/legion/extensions/agentic/self/self_talk/helpers/dialogue.rb +114 -0
  138. data/lib/legion/extensions/agentic/self/self_talk/helpers/dialogue_turn.rb +43 -0
  139. data/lib/legion/extensions/agentic/self/self_talk/helpers/inner_voice.rb +77 -0
  140. data/lib/legion/extensions/agentic/self/self_talk/helpers/llm_enhancer.rb +135 -0
  141. data/lib/legion/extensions/agentic/self/self_talk/helpers/self_talk_engine.rb +160 -0
  142. data/lib/legion/extensions/agentic/self/self_talk/runners/self_talk.rb +172 -0
  143. data/lib/legion/extensions/agentic/self/self_talk/version.rb +13 -0
  144. data/lib/legion/extensions/agentic/self/self_talk.rb +22 -0
  145. data/lib/legion/extensions/agentic/self/version.rb +11 -0
  146. data/lib/legion/extensions/agentic/self.rb +33 -0
  147. data/spec/legion/extensions/agentic/self/agency/client_spec.rb +67 -0
  148. data/spec/legion/extensions/agentic/self/agency/helpers/constants_spec.rb +73 -0
  149. data/spec/legion/extensions/agentic/self/agency/helpers/efficacy_model_spec.rb +190 -0
  150. data/spec/legion/extensions/agentic/self/agency/helpers/outcome_event_spec.rb +85 -0
  151. data/spec/legion/extensions/agentic/self/agency/runners/agency_spec.rb +132 -0
  152. data/spec/legion/extensions/agentic/self/anchor/client_spec.rb +30 -0
  153. data/spec/legion/extensions/agentic/self/anchor/helpers/anchor_engine_spec.rb +109 -0
  154. data/spec/legion/extensions/agentic/self/anchor/helpers/anchor_spec.rb +124 -0
  155. data/spec/legion/extensions/agentic/self/anchor/helpers/chain_spec.rb +106 -0
  156. data/spec/legion/extensions/agentic/self/anchor/helpers/constants_spec.rb +53 -0
  157. data/spec/legion/extensions/agentic/self/anchor/runners/cognitive_anchor_spec.rb +70 -0
  158. data/spec/legion/extensions/agentic/self/anosognosia/anosognosia_spec.rb +15 -0
  159. data/spec/legion/extensions/agentic/self/anosognosia/client_spec.rb +50 -0
  160. data/spec/legion/extensions/agentic/self/anosognosia/helpers/anosognosia_engine_spec.rb +266 -0
  161. data/spec/legion/extensions/agentic/self/anosognosia/helpers/cognitive_deficit_spec.rb +150 -0
  162. data/spec/legion/extensions/agentic/self/anosognosia/helpers/constants_spec.rb +58 -0
  163. data/spec/legion/extensions/agentic/self/anosognosia/runners/anosognosia_spec.rb +225 -0
  164. data/spec/legion/extensions/agentic/self/architecture/client_spec.rb +51 -0
  165. data/spec/legion/extensions/agentic/self/architecture/helpers/architecture_engine_spec.rb +321 -0
  166. data/spec/legion/extensions/agentic/self/architecture/helpers/connection_spec.rb +118 -0
  167. data/spec/legion/extensions/agentic/self/architecture/helpers/subsystem_spec.rb +189 -0
  168. data/spec/legion/extensions/agentic/self/architecture/runners/cognitive_architecture_spec.rb +181 -0
  169. data/spec/legion/extensions/agentic/self/default_mode_network/client_spec.rb +69 -0
  170. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/constants_spec.rb +76 -0
  171. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/dmn_engine_spec.rb +321 -0
  172. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/wandering_thought_spec.rb +145 -0
  173. data/spec/legion/extensions/agentic/self/default_mode_network/runners/default_mode_network_spec.rb +269 -0
  174. data/spec/legion/extensions/agentic/self/fingerprint/client_spec.rb +54 -0
  175. data/spec/legion/extensions/agentic/self/fingerprint/helpers/cognitive_trait_spec.rb +180 -0
  176. data/spec/legion/extensions/agentic/self/fingerprint/helpers/constants_spec.rb +108 -0
  177. data/spec/legion/extensions/agentic/self/fingerprint/helpers/fingerprint_engine_spec.rb +318 -0
  178. data/spec/legion/extensions/agentic/self/fingerprint/runners/cognitive_fingerprint_spec.rb +232 -0
  179. data/spec/legion/extensions/agentic/self/identity/actors/orphan_check_spec.rb +104 -0
  180. data/spec/legion/extensions/agentic/self/identity/client_spec.rb +32 -0
  181. data/spec/legion/extensions/agentic/self/identity/helpers/dimensions_spec.rb +51 -0
  182. data/spec/legion/extensions/agentic/self/identity/helpers/fingerprint_spec.rb +66 -0
  183. data/spec/legion/extensions/agentic/self/identity/helpers/graph_client_spec.rb +19 -0
  184. data/spec/legion/extensions/agentic/self/identity/helpers/graph_token_spec.rb +31 -0
  185. data/spec/legion/extensions/agentic/self/identity/helpers/token_cache_spec.rb +50 -0
  186. data/spec/legion/extensions/agentic/self/identity/local_persistence_spec.rb +329 -0
  187. data/spec/legion/extensions/agentic/self/identity/runners/entra_spec.rb +655 -0
  188. data/spec/legion/extensions/agentic/self/identity/runners/identity_spec.rb +61 -0
  189. data/spec/legion/extensions/agentic/self/metacognition/client_spec.rb +20 -0
  190. data/spec/legion/extensions/agentic/self/metacognition/helpers/constants_spec.rb +31 -0
  191. data/spec/legion/extensions/agentic/self/metacognition/helpers/narrator_bridge_spec.rb +102 -0
  192. data/spec/legion/extensions/agentic/self/metacognition/helpers/registry_store_spec.rb +227 -0
  193. data/spec/legion/extensions/agentic/self/metacognition/helpers/self_model_spec.rb +117 -0
  194. data/spec/legion/extensions/agentic/self/metacognition/helpers/snapshot_store_spec.rb +128 -0
  195. data/spec/legion/extensions/agentic/self/metacognition/runners/metacognition_spec.rb +110 -0
  196. data/spec/legion/extensions/agentic/self/metacognition/runners/registry_spec.rb +281 -0
  197. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/client_spec.rb +59 -0
  198. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/calibration_tracker_spec.rb +143 -0
  199. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/constants_spec.rb +91 -0
  200. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_engine_spec.rb +198 -0
  201. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_judgment_spec.rb +172 -0
  202. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/runners/metacognitive_monitoring_spec.rb +244 -0
  203. data/spec/legion/extensions/agentic/self/narrative_arc/client_spec.rb +22 -0
  204. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/arc_engine_spec.rb +183 -0
  205. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/arc_spec.rb +177 -0
  206. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/beat_event_spec.rb +96 -0
  207. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/constants_spec.rb +75 -0
  208. data/spec/legion/extensions/agentic/self/narrative_arc/runners/narrative_spec.rb +142 -0
  209. data/spec/legion/extensions/agentic/self/narrative_identity/client_spec.rb +69 -0
  210. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/chapter_spec.rb +85 -0
  211. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/constants_spec.rb +83 -0
  212. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/episode_spec.rb +180 -0
  213. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/narrative_engine_spec.rb +307 -0
  214. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/theme_spec.rb +107 -0
  215. data/spec/legion/extensions/agentic/self/narrative_identity/runners/narrative_identity_spec.rb +240 -0
  216. data/spec/legion/extensions/agentic/self/narrative_self/client_spec.rb +67 -0
  217. data/spec/legion/extensions/agentic/self/narrative_self/helpers/autobiography_spec.rb +155 -0
  218. data/spec/legion/extensions/agentic/self/narrative_self/helpers/constants_spec.rb +28 -0
  219. data/spec/legion/extensions/agentic/self/narrative_self/helpers/episode_spec.rb +144 -0
  220. data/spec/legion/extensions/agentic/self/narrative_self/helpers/narrative_thread_spec.rb +87 -0
  221. data/spec/legion/extensions/agentic/self/narrative_self/runners/narrative_self_spec.rb +118 -0
  222. data/spec/legion/extensions/agentic/self/personality/client_spec.rb +20 -0
  223. data/spec/legion/extensions/agentic/self/personality/helpers/constants_spec.rb +41 -0
  224. data/spec/legion/extensions/agentic/self/personality/helpers/personality_store_spec.rb +66 -0
  225. data/spec/legion/extensions/agentic/self/personality/helpers/trait_model_spec.rb +148 -0
  226. data/spec/legion/extensions/agentic/self/personality/runners/personality_spec.rb +67 -0
  227. data/spec/legion/extensions/agentic/self/reflection/client_spec.rb +24 -0
  228. data/spec/legion/extensions/agentic/self/reflection/helpers/llm_enhancer_spec.rb +191 -0
  229. data/spec/legion/extensions/agentic/self/reflection/helpers/monitors_spec.rb +120 -0
  230. data/spec/legion/extensions/agentic/self/reflection/helpers/reflection_spec.rb +49 -0
  231. data/spec/legion/extensions/agentic/self/reflection/helpers/reflection_store_spec.rb +93 -0
  232. data/spec/legion/extensions/agentic/self/reflection/runners/reflection_spec.rb +204 -0
  233. data/spec/legion/extensions/agentic/self/self_model/client_spec.rb +55 -0
  234. data/spec/legion/extensions/agentic/self/self_model/helpers/capability_spec.rb +160 -0
  235. data/spec/legion/extensions/agentic/self/self_model/helpers/knowledge_domain_spec.rb +128 -0
  236. data/spec/legion/extensions/agentic/self/self_model/helpers/self_model_spec.rb +238 -0
  237. data/spec/legion/extensions/agentic/self/self_model/runners/self_model_spec.rb +143 -0
  238. data/spec/legion/extensions/agentic/self/self_talk/actors/volume_decay_spec.rb +46 -0
  239. data/spec/legion/extensions/agentic/self/self_talk/client_spec.rb +26 -0
  240. data/spec/legion/extensions/agentic/self/self_talk/helpers/constants_spec.rb +110 -0
  241. data/spec/legion/extensions/agentic/self/self_talk/helpers/dialogue_spec.rb +191 -0
  242. data/spec/legion/extensions/agentic/self/self_talk/helpers/dialogue_turn_spec.rb +78 -0
  243. data/spec/legion/extensions/agentic/self/self_talk/helpers/inner_voice_spec.rb +172 -0
  244. data/spec/legion/extensions/agentic/self/self_talk/helpers/llm_enhancer_spec.rb +206 -0
  245. data/spec/legion/extensions/agentic/self/self_talk/helpers/self_talk_engine_spec.rb +239 -0
  246. data/spec/legion/extensions/agentic/self/self_talk/runners/self_talk_llm_spec.rb +169 -0
  247. data/spec/legion/extensions/agentic/self/self_talk/runners/self_talk_spec.rb +196 -0
  248. data/spec/spec_helper.rb +46 -0
  249. metadata +347 -0
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the framework actor base class since legionio gem is not available in test
4
+ module Legion
5
+ module Extensions
6
+ module Actors
7
+ class Every # rubocop:disable Lint/EmptyClass
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ # Intercept the require in the actor file so it doesn't fail
14
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
15
+
16
+ require 'legion/extensions/agentic/self/identity/actors/orphan_check'
17
+
18
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Actor::OrphanCheck do
19
+ subject(:actor) { described_class.new }
20
+
21
+ describe 'ORPHAN_CHECK_INTERVAL' do
22
+ it 'is 14400 seconds (4 hours)' do
23
+ expect(described_class::ORPHAN_CHECK_INTERVAL).to eq(14_400)
24
+ end
25
+ end
26
+
27
+ describe '#runner_class' do
28
+ it 'returns the Entra module' do
29
+ expect(actor.runner_class).to eq(Legion::Extensions::Agentic::Self::Identity::Runners::Entra)
30
+ end
31
+ end
32
+
33
+ describe '#runner_function' do
34
+ it 'returns check_orphans' do
35
+ expect(actor.runner_function).to eq('check_orphans')
36
+ end
37
+ end
38
+
39
+ describe '#time' do
40
+ it 'returns 14400 seconds using the constant' do
41
+ expect(actor.time).to eq(Legion::Extensions::Agentic::Self::Identity::Actor::OrphanCheck::ORPHAN_CHECK_INTERVAL)
42
+ end
43
+
44
+ it 'returns 14400' do
45
+ expect(actor.time).to eq(14_400)
46
+ end
47
+ end
48
+
49
+ describe '#use_runner?' do
50
+ it 'returns false' do
51
+ expect(actor.use_runner?).to be false
52
+ end
53
+ end
54
+
55
+ describe '#check_subtask?' do
56
+ it 'returns false' do
57
+ expect(actor.check_subtask?).to be false
58
+ end
59
+ end
60
+
61
+ describe '#generate_task?' do
62
+ it 'returns false' do
63
+ expect(actor.generate_task?).to be false
64
+ end
65
+ end
66
+
67
+ describe '#enabled?' do
68
+ context 'when Legion::Data is not defined' do
69
+ it 'returns false' do
70
+ hide_const('Legion::Data') if defined?(Legion::Data)
71
+ expect(actor.enabled?).to be_falsey
72
+ end
73
+ end
74
+
75
+ context 'when Legion::Data is defined and data is connected' do
76
+ it 'returns truthy' do
77
+ stub_const('Legion::Data', Module.new)
78
+ stub_const('Legion::Settings', Class.new)
79
+ settings_double = { connected: true }
80
+ allow(Legion::Settings).to receive(:[]).with(:data).and_return(settings_double)
81
+ expect(actor.enabled?).to be_truthy
82
+ end
83
+ end
84
+
85
+ context 'when Legion::Data is defined but connected is false' do
86
+ it 'returns false' do
87
+ stub_const('Legion::Data', Module.new)
88
+ stub_const('Legion::Settings', Class.new)
89
+ settings_double = { connected: false }
90
+ allow(Legion::Settings).to receive(:[]).with(:data).and_return(settings_double)
91
+ expect(actor.enabled?).to be false
92
+ end
93
+ end
94
+
95
+ context 'when Legion::Settings raises an error' do
96
+ it 'returns false' do
97
+ stub_const('Legion::Data', Module.new)
98
+ stub_const('Legion::Settings', Class.new)
99
+ allow(Legion::Settings).to receive(:[]).with(:data).and_raise(StandardError)
100
+ expect(actor.enabled?).to be false
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/self/identity/client'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Client do
6
+ let(:client) { described_class.new }
7
+
8
+ it 'responds to identity runner methods' do
9
+ expect(client).to respond_to(:observe_behavior)
10
+ expect(client).to respond_to(:observe_all)
11
+ expect(client).to respond_to(:check_entropy)
12
+ expect(client).to respond_to(:identity_status)
13
+ expect(client).to respond_to(:identity_maturity)
14
+ end
15
+
16
+ it 'round-trips identity lifecycle' do
17
+ # Build identity
18
+ 50.times do
19
+ client.observe_all(observations: {
20
+ communication_cadence: 0.5 + (rand * 0.1),
21
+ vocabulary_patterns: 0.6 + (rand * 0.1),
22
+ emotional_response: 0.4 + (rand * 0.1)
23
+ })
24
+ end
25
+
26
+ expect(client.identity_maturity[:maturity]).to eq(:established)
27
+
28
+ # Check entropy with consistent behavior
29
+ result = client.check_entropy(observations: { communication_cadence: 0.55 })
30
+ expect(result[:entropy]).to be_between(0.0, 1.0)
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Helpers::Dimensions do
4
+ describe '.new_identity_model' do
5
+ it 'creates a model with 6 dimensions' do
6
+ model = described_class.new_identity_model
7
+ expect(model.size).to eq(6)
8
+ described_class::IDENTITY_DIMENSIONS.each do |dim|
9
+ expect(model[dim][:mean]).to eq(0.5)
10
+ end
11
+ end
12
+ end
13
+
14
+ describe '.compute_entropy' do
15
+ it 'returns 0.5 for empty observations' do
16
+ model = described_class.new_identity_model
17
+ expect(described_class.compute_entropy({}, model)).to eq(0.5)
18
+ end
19
+
20
+ it 'returns low entropy for observations matching baseline' do
21
+ model = described_class.new_identity_model
22
+ model[:communication_cadence][:observations] = 50
23
+ obs = { communication_cadence: 0.5 }
24
+ entropy = described_class.compute_entropy(obs, model)
25
+ expect(entropy).to be < 0.3
26
+ end
27
+
28
+ it 'returns high entropy for observations diverging from baseline' do
29
+ model = described_class.new_identity_model
30
+ model[:communication_cadence][:observations] = 50
31
+ model[:communication_cadence][:variance] = 0.1
32
+ obs = { communication_cadence: 1.0 }
33
+ entropy = described_class.compute_entropy(obs, model)
34
+ expect(entropy).to be > 0.3
35
+ end
36
+ end
37
+
38
+ describe '.classify_entropy' do
39
+ it 'classifies high entropy' do
40
+ expect(described_class.classify_entropy(0.8)).to eq(:high_entropy)
41
+ end
42
+
43
+ it 'classifies low entropy' do
44
+ expect(described_class.classify_entropy(0.1)).to eq(:low_entropy)
45
+ end
46
+
47
+ it 'classifies normal entropy' do
48
+ expect(described_class.classify_entropy(0.5)).to eq(:normal)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Helpers::Fingerprint do
4
+ let(:fp) { described_class.new }
5
+
6
+ describe '#observe' do
7
+ it 'records observation for valid dimension' do
8
+ fp.observe(:communication_cadence, 0.7)
9
+ expect(fp.observation_count).to eq(1)
10
+ end
11
+
12
+ it 'ignores invalid dimensions' do
13
+ fp.observe(:nonexistent, 0.5)
14
+ expect(fp.observation_count).to eq(0)
15
+ end
16
+
17
+ it 'shifts mean toward observed values' do
18
+ original = fp.model[:vocabulary_patterns][:mean]
19
+ 10.times { fp.observe(:vocabulary_patterns, 0.9) }
20
+ expect(fp.model[:vocabulary_patterns][:mean]).to be > original
21
+ end
22
+ end
23
+
24
+ describe '#observe_all' do
25
+ it 'records multiple dimensions at once' do
26
+ fp.observe_all(communication_cadence: 0.6, vocabulary_patterns: 0.7)
27
+ expect(fp.observation_count).to eq(2)
28
+ end
29
+ end
30
+
31
+ describe '#current_entropy' do
32
+ it 'computes entropy against model' do
33
+ entropy = fp.current_entropy(communication_cadence: 0.5)
34
+ expect(entropy).to be_between(0.0, 1.0)
35
+ end
36
+
37
+ it 'tracks entropy history' do
38
+ 3.times { fp.current_entropy(communication_cadence: 0.5) }
39
+ expect(fp.entropy_history.size).to eq(3)
40
+ end
41
+ end
42
+
43
+ describe '#entropy_trend' do
44
+ it 'returns stable for insufficient data' do
45
+ expect(fp.entropy_trend).to eq(:stable)
46
+ end
47
+
48
+ it 'detects rising entropy' do
49
+ 5.times { |i| fp.current_entropy(communication_cadence: 0.5 + (i * 0.1)) }
50
+ # Trend depends on actual computed values
51
+ trend = fp.entropy_trend(window: 5)
52
+ expect(%i[rising stable falling]).to include(trend)
53
+ end
54
+ end
55
+
56
+ describe '#maturity' do
57
+ it 'starts as nascent' do
58
+ expect(fp.maturity).to eq(:nascent)
59
+ end
60
+
61
+ it 'progresses to developing' do
62
+ 15.times { fp.observe(:communication_cadence, 0.5) }
63
+ expect(fp.maturity).to eq(:developing)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/agentic/self/identity/helpers/graph_client'
5
+
6
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Helpers::GraphClient do
7
+ describe '.connection' do
8
+ it 'returns a Faraday connection with bearer token' do
9
+ conn = described_class.connection(token: 'test-token')
10
+ expect(conn).to be_a(Faraday::Connection)
11
+ expect(conn.headers['Authorization']).to eq('Bearer test-token')
12
+ end
13
+
14
+ it 'uses custom base URL when provided' do
15
+ conn = described_class.connection(token: 'tok', base: 'https://custom.api.com')
16
+ expect(conn.url_prefix.to_s).to eq('https://custom.api.com/')
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/agentic/self/identity/helpers/graph_token'
5
+
6
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Helpers::GraphToken do
7
+ describe '.fetch' do
8
+ it 'raises GraphTokenError on failure' do
9
+ stub_request = instance_double(Faraday::Response, success?: false,
10
+ body: { 'error_description' => 'invalid_client' })
11
+ conn = instance_double(Faraday::Connection)
12
+ allow(conn).to receive(:post).and_return(stub_request)
13
+ allow(Faraday).to receive(:new).and_return(conn)
14
+
15
+ expect do
16
+ described_class.fetch(tenant_id: 't1', client_id: 'c1', client_secret: 's1')
17
+ end.to raise_error(described_class::GraphTokenError, 'invalid_client')
18
+ end
19
+
20
+ it 'returns access_token on success' do
21
+ stub_request = instance_double(Faraday::Response, success?: true,
22
+ body: { 'access_token' => 'tok-123' })
23
+ conn = instance_double(Faraday::Connection)
24
+ allow(conn).to receive(:post).and_return(stub_request)
25
+ allow(Faraday).to receive(:new).and_return(conn)
26
+
27
+ token = described_class.fetch(tenant_id: 't1', client_id: 'c1', client_secret: 's1')
28
+ expect(token).to eq('tok-123')
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/agentic/self/identity/helpers/token_cache'
5
+
6
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Helpers::TokenCache do
7
+ before { described_class.clear_all }
8
+ after { described_class.clear_all }
9
+
10
+ describe '.store and .fetch' do
11
+ it 'stores and retrieves a token' do
12
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
13
+ entry = described_class.fetch(worker_id: 'w1')
14
+ expect(entry[:access_token]).to eq('abc')
15
+ end
16
+
17
+ it 'returns nil for unknown worker' do
18
+ expect(described_class.fetch(worker_id: 'unknown')).to be_nil
19
+ end
20
+
21
+ it 'returns nil for expired token' do
22
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: -1)
23
+ expect(described_class.fetch(worker_id: 'w1')).to be_nil
24
+ end
25
+ end
26
+
27
+ describe '.approaching_expiry?' do
28
+ it 'returns true when no token exists' do
29
+ expect(described_class.approaching_expiry?(worker_id: 'w1')).to be true
30
+ end
31
+
32
+ it 'returns true when token is within buffer' do
33
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 100)
34
+ expect(described_class.approaching_expiry?(worker_id: 'w1', buffer: 200)).to be true
35
+ end
36
+
37
+ it 'returns false when token has plenty of time' do
38
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
39
+ expect(described_class.approaching_expiry?(worker_id: 'w1', buffer: 300)).to be false
40
+ end
41
+ end
42
+
43
+ describe '.clear' do
44
+ it 'removes a specific worker token' do
45
+ described_class.store(worker_id: 'w1', token: 'abc', expires_in: 3600)
46
+ described_class.clear(worker_id: 'w1')
47
+ expect(described_class.fetch(worker_id: 'w1')).to be_nil
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Local persistence spec for Legion::Extensions::Agentic::Self::Identity::Helpers::Fingerprint
4
+ #
5
+ # Strategy: stub Legion::Data::Local with a real in-memory Sequel SQLite database
6
+ # when sequel + sqlite3 are available, otherwise use a double that records calls.
7
+ # Either way, the Fingerprint save/load logic is exercised end-to-end.
8
+
9
+ begin
10
+ require 'sequel'
11
+ require 'sqlite3'
12
+ SEQUEL_AVAILABLE = true
13
+ rescue LoadError
14
+ SEQUEL_AVAILABLE = false
15
+ end
16
+
17
+ RSpec.describe Legion::Extensions::Agentic::Self::Identity::Helpers::Fingerprint, 'local persistence' do
18
+ # ---------------------------------------------------------------------------
19
+ # Helpers for setting up the in-memory DB (shared between contexts)
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def build_in_memory_db
23
+ db = Sequel.sqlite
24
+ db.create_table(:identity_fingerprint) do
25
+ primary_key :id
26
+ String :dimension, null: false, unique: true
27
+ Float :mean, default: 0.0
28
+ Float :variance, default: 0.0
29
+ Integer :observations, default: 0
30
+ DateTime :last_observed
31
+ end
32
+ db.create_table(:identity_meta) do
33
+ primary_key :id
34
+ Integer :observation_count, default: 0
35
+ String :entropy_history, text: true
36
+ end
37
+ db
38
+ end
39
+
40
+ def stub_local(db)
41
+ local_mod = Module.new do
42
+ define_singleton_method(:connection) { db }
43
+ define_singleton_method(:connected?) { true }
44
+ end
45
+ stub_const('Legion::Data', Module.new)
46
+ stub_const('Legion::Data::Local', local_mod)
47
+ end
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # When Sequel + sqlite3 are available: full round-trip integration tests
51
+ # ---------------------------------------------------------------------------
52
+
53
+ if SEQUEL_AVAILABLE
54
+ context 'with an in-memory SQLite database' do
55
+ let(:db) { build_in_memory_db }
56
+
57
+ before { stub_local(db) }
58
+
59
+ describe '#save_to_local' do
60
+ it 'returns true on first save' do
61
+ fp = described_class.new
62
+ expect(fp.save_to_local).to be true
63
+ end
64
+
65
+ it 'persists all 6 dimension rows to identity_fingerprint' do
66
+ fp = described_class.new
67
+ fp.observe(:communication_cadence, 0.8)
68
+ fp.save_to_local
69
+
70
+ rows = db[:identity_fingerprint].all
71
+ expect(rows.size).to eq(6)
72
+ end
73
+
74
+ it 'persists updated mean for observed dimension' do
75
+ fp = described_class.new
76
+ 10.times { fp.observe(:vocabulary_patterns, 0.9) }
77
+ fp.save_to_local
78
+
79
+ row = db[:identity_fingerprint].where(dimension: 'vocabulary_patterns').first
80
+ expect(row[:mean]).to be > 0.5
81
+ expect(row[:observations]).to eq(10)
82
+ end
83
+
84
+ it 'persists observation_count in identity_meta' do
85
+ fp = described_class.new
86
+ 3.times { fp.observe(:communication_cadence, 0.6) }
87
+ fp.save_to_local
88
+
89
+ meta = db[:identity_meta].first
90
+ expect(meta[:observation_count]).to eq(3)
91
+ end
92
+
93
+ it 'persists entropy_history as JSON in identity_meta' do
94
+ fp = described_class.new
95
+ fp.current_entropy(communication_cadence: 0.5)
96
+ fp.current_entropy(vocabulary_patterns: 0.6)
97
+ fp.save_to_local
98
+
99
+ meta = db[:identity_meta].first
100
+ parsed = JSON.parse(meta[:entropy_history])
101
+ expect(parsed.size).to eq(2)
102
+ expect(parsed.first).to have_key('entropy')
103
+ expect(parsed.first).to have_key('at')
104
+ end
105
+
106
+ it 'updates existing rows on second save (upsert)' do
107
+ fp = described_class.new
108
+ fp.observe(:emotional_response, 0.3)
109
+ fp.save_to_local
110
+
111
+ # mutate and save again
112
+ 5.times { fp.observe(:emotional_response, 0.9) }
113
+ fp.save_to_local
114
+
115
+ rows = db[:identity_fingerprint].where(dimension: 'emotional_response').all
116
+ expect(rows.size).to eq(1) # still one row, not two
117
+ expect(rows.first[:observations]).to eq(6)
118
+ end
119
+ end
120
+
121
+ describe '#load_from_local' do
122
+ it 'returns true when called with an empty DB' do
123
+ fp = described_class.new # load_from_local called in initialize
124
+ expect(fp.observation_count).to eq(0)
125
+ end
126
+
127
+ it 'restores model dimensions from DB rows' do
128
+ # Pre-seed DB directly
129
+ db[:identity_fingerprint].insert(
130
+ dimension: 'communication_cadence',
131
+ mean: 0.75, variance: 0.05, observations: 42,
132
+ last_observed: Time.now.utc
133
+ )
134
+
135
+ fp = described_class.new # triggers load_from_local
136
+ expect(fp.model[:communication_cadence][:mean]).to be_within(0.001).of(0.75)
137
+ expect(fp.model[:communication_cadence][:observations]).to eq(42)
138
+ end
139
+
140
+ it 'restores observation_count from identity_meta' do
141
+ db[:identity_meta].insert(observation_count: 57, entropy_history: '[]')
142
+
143
+ fp = described_class.new
144
+ expect(fp.observation_count).to eq(57)
145
+ end
146
+
147
+ it 'restores entropy_history from identity_meta JSON' do
148
+ history = [
149
+ { 'entropy' => 0.3, 'at' => Time.now.utc.iso8601 },
150
+ { 'entropy' => 0.4, 'at' => Time.now.utc.iso8601 }
151
+ ]
152
+ db[:identity_meta].insert(observation_count: 0, entropy_history: JSON.generate(history))
153
+
154
+ fp = described_class.new
155
+ expect(fp.entropy_history.size).to eq(2)
156
+ expect(fp.entropy_history.first[:entropy]).to be_within(0.001).of(0.3)
157
+ expect(fp.entropy_history.first[:at]).to be_a(Time)
158
+ end
159
+
160
+ it 'ignores DB rows for unknown dimensions' do
161
+ db[:identity_fingerprint].insert(
162
+ dimension: 'nonexistent_dimension',
163
+ mean: 0.9, variance: 0.1, observations: 5,
164
+ last_observed: nil
165
+ )
166
+
167
+ fp = described_class.new # must not raise
168
+ expect(fp.model.keys).to match_array(
169
+ %i[communication_cadence vocabulary_patterns emotional_response
170
+ decision_patterns contextual_consistency temporal_patterns]
171
+ )
172
+ end
173
+ end
174
+
175
+ describe 'full round-trip' do
176
+ it 'survives a save-then-load cycle with identical state' do
177
+ # Build state in first fingerprint instance
178
+ fp1 = described_class.new
179
+ 12.times { fp1.observe(:communication_cadence, 0.7) }
180
+ 8.times { fp1.observe(:vocabulary_patterns, 0.6) }
181
+ fp1.current_entropy(communication_cadence: 0.65)
182
+ fp1.current_entropy(vocabulary_patterns: 0.55)
183
+ fp1.save_to_local
184
+
185
+ # Load into a fresh instance (DB already populated)
186
+ fp2 = described_class.new
187
+
188
+ expect(fp2.observation_count).to eq(fp1.observation_count)
189
+ expect(fp2.entropy_history.size).to eq(fp1.entropy_history.size)
190
+
191
+ dims = %i[communication_cadence vocabulary_patterns]
192
+ dims.each do |dim|
193
+ expect(fp2.model[dim][:mean]).to be_within(0.0001).of(fp1.model[dim][:mean])
194
+ expect(fp2.model[dim][:variance]).to be_within(0.0001).of(fp1.model[dim][:variance])
195
+ expect(fp2.model[dim][:observations]).to eq(fp1.model[dim][:observations])
196
+ end
197
+ end
198
+
199
+ it 'preserves maturity after round-trip' do
200
+ fp1 = described_class.new
201
+ 15.times { fp1.observe(:decision_patterns, 0.5) }
202
+ expect(fp1.maturity).to eq(:developing)
203
+ fp1.save_to_local
204
+
205
+ fp2 = described_class.new
206
+ expect(fp2.maturity).to eq(:developing)
207
+ end
208
+
209
+ it 'preserves entropy trend direction after round-trip' do
210
+ fp1 = described_class.new
211
+ # Ascending entropy values — second half larger than first half
212
+ [0.1, 0.1, 0.1, 0.1, 0.7, 0.8, 0.9, 0.95, 0.98, 1.0].each do |e|
213
+ fp1.instance_variable_get(:@entropy_history) << { entropy: e, at: Time.now.utc }
214
+ end
215
+ fp1.save_to_local
216
+
217
+ fp2 = described_class.new
218
+ expect(fp2.entropy_history.map { |h| h[:entropy] }).to eq(
219
+ fp1.entropy_history.map { |h| h[:entropy] }
220
+ )
221
+ end
222
+ end
223
+ end
224
+
225
+ else
226
+ # ---------------------------------------------------------------------------
227
+ # Fallback: double-based tests when Sequel is not in the bundle
228
+ # ---------------------------------------------------------------------------
229
+
230
+ context 'when Sequel is not available (double-based fallback)' do
231
+ let(:fingerprint_rows) { {} }
232
+ let(:meta_rows) { [] }
233
+
234
+ let(:fp_dataset) do
235
+ d = double('fingerprint_dataset')
236
+ allow(d).to receive(:where) { |args|
237
+ scoped = double('scoped_fp_dataset')
238
+ allow(scoped).to receive(:first) { fingerprint_rows[args[:dimension]] }
239
+ allow(scoped).to receive(:update) { |row|
240
+ fingerprint_rows[row[:dimension] || args[:dimension]] = row
241
+ }
242
+ scoped
243
+ }
244
+ allow(d).to receive(:insert) { |row| fingerprint_rows[row[:dimension]] = row }
245
+ allow(d).to receive(:each) { |&block| fingerprint_rows.each_value(&block) }
246
+ allow(d).to receive(:first) { fingerprint_rows.values.first }
247
+ d
248
+ end
249
+
250
+ let(:meta_dataset) do
251
+ d = double('meta_dataset')
252
+ allow(d).to receive(:first) { meta_rows.first }
253
+ allow(d).to receive(:insert) { |row| meta_rows << row }
254
+ allow(d).to receive(:where) do
255
+ scoped = double('scoped_meta_dataset')
256
+ allow(scoped).to receive(:update) { |row| meta_rows[0] = meta_rows[0]&.merge(row) }
257
+ scoped
258
+ end
259
+ d
260
+ end
261
+
262
+ let(:db) do
263
+ d = double('Sequel::Database')
264
+ allow(d).to receive(:[]).with(:identity_fingerprint).and_return(fp_dataset)
265
+ allow(d).to receive(:[]).with(:identity_meta).and_return(meta_dataset)
266
+ d
267
+ end
268
+
269
+ before do
270
+ local_mod = Module.new do
271
+ define_singleton_method(:connection) { nil } # overridden per example
272
+ define_singleton_method(:connected?) { true }
273
+ end
274
+ stub_const('Legion::Data', Module.new)
275
+ stub_const('Legion::Data::Local', local_mod)
276
+ allow(Legion::Data::Local).to receive(:connection).and_return(db)
277
+ end
278
+
279
+ it 'calls insert on the fingerprint dataset during save_to_local' do
280
+ fp = described_class.new
281
+ expect(fp_dataset).to receive(:insert).at_least(:once)
282
+ fp.save_to_local
283
+ end
284
+
285
+ it 'calls insert on the meta dataset during save_to_local' do
286
+ fp = described_class.new
287
+ expect(meta_dataset).to receive(:insert).once
288
+ fp.save_to_local
289
+ end
290
+
291
+ it 'does not raise when local is unavailable (Legion::Data::Local not defined)' do
292
+ # Remove the stub so defined? check returns false
293
+ hide_const('Legion::Data::Local')
294
+ expect { described_class.new }.not_to raise_error
295
+ end
296
+ end
297
+ end
298
+
299
+ # ---------------------------------------------------------------------------
300
+ # Behaviour when Legion::Data::Local is not defined (always run)
301
+ # ---------------------------------------------------------------------------
302
+
303
+ describe 'when Legion::Data::Local is not available' do
304
+ before do
305
+ hide_const('Legion::Data::Local') if defined?(Legion::Data::Local)
306
+ end
307
+
308
+ it 'initialize completes without error' do
309
+ expect { described_class.new }.not_to raise_error
310
+ end
311
+
312
+ it 'save_to_local returns nil (guard short-circuits)' do
313
+ fp = described_class.new
314
+ expect(fp.save_to_local).to be_nil
315
+ end
316
+
317
+ it 'load_from_local returns nil (guard short-circuits)' do
318
+ fp = described_class.new
319
+ expect(fp.load_from_local).to be_nil
320
+ end
321
+
322
+ it 'model starts with fresh defaults' do
323
+ fp = described_class.new
324
+ expect(fp.model[:communication_cadence][:mean]).to eq(0.5)
325
+ expect(fp.observation_count).to eq(0)
326
+ expect(fp.entropy_history).to be_empty
327
+ end
328
+ end
329
+ end