lex-agentic-social 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 (235) 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-social.gemspec +30 -0
  7. data/lib/legion/extensions/agentic/social/apprenticeship/client.rb +28 -0
  8. data/lib/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship.rb +90 -0
  9. data/lib/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_engine.rb +109 -0
  10. data/lib/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_model.rb +93 -0
  11. data/lib/legion/extensions/agentic/social/apprenticeship/runners/cognitive_apprenticeship.rb +112 -0
  12. data/lib/legion/extensions/agentic/social/apprenticeship/version.rb +13 -0
  13. data/lib/legion/extensions/agentic/social/apprenticeship.rb +19 -0
  14. data/lib/legion/extensions/agentic/social/conflict/actors/stale_check.rb +45 -0
  15. data/lib/legion/extensions/agentic/social/conflict/client.rb +27 -0
  16. data/lib/legion/extensions/agentic/social/conflict/helpers/conflict_log.rb +70 -0
  17. data/lib/legion/extensions/agentic/social/conflict/helpers/llm_enhancer.rb +130 -0
  18. data/lib/legion/extensions/agentic/social/conflict/helpers/severity.rb +47 -0
  19. data/lib/legion/extensions/agentic/social/conflict/runners/conflict.rb +112 -0
  20. data/lib/legion/extensions/agentic/social/conflict/version.rb +13 -0
  21. data/lib/legion/extensions/agentic/social/conflict.rb +19 -0
  22. data/lib/legion/extensions/agentic/social/conscience/client.rb +26 -0
  23. data/lib/legion/extensions/agentic/social/conscience/helpers/constants.rb +53 -0
  24. data/lib/legion/extensions/agentic/social/conscience/helpers/moral_evaluator.rb +178 -0
  25. data/lib/legion/extensions/agentic/social/conscience/helpers/moral_store.rb +116 -0
  26. data/lib/legion/extensions/agentic/social/conscience/runners/conscience.rb +117 -0
  27. data/lib/legion/extensions/agentic/social/conscience/version.rb +13 -0
  28. data/lib/legion/extensions/agentic/social/conscience.rb +19 -0
  29. data/lib/legion/extensions/agentic/social/consent/actors/tier_evaluation.rb +45 -0
  30. data/lib/legion/extensions/agentic/social/consent/client.rb +27 -0
  31. data/lib/legion/extensions/agentic/social/consent/helpers/consent_map.rb +199 -0
  32. data/lib/legion/extensions/agentic/social/consent/helpers/tiers.rb +54 -0
  33. data/lib/legion/extensions/agentic/social/consent/local_migrations/20260316000010_create_consent_domains.rb +16 -0
  34. data/lib/legion/extensions/agentic/social/consent/runners/consent.rb +228 -0
  35. data/lib/legion/extensions/agentic/social/consent/version.rb +13 -0
  36. data/lib/legion/extensions/agentic/social/consent.rb +25 -0
  37. data/lib/legion/extensions/agentic/social/entrainment/client.rb +19 -0
  38. data/lib/legion/extensions/agentic/social/entrainment/helpers/constants.rb +40 -0
  39. data/lib/legion/extensions/agentic/social/entrainment/helpers/entrainment_engine.rb +120 -0
  40. data/lib/legion/extensions/agentic/social/entrainment/helpers/pairing.rb +86 -0
  41. data/lib/legion/extensions/agentic/social/entrainment/runners/cognitive_entrainment.rb +89 -0
  42. data/lib/legion/extensions/agentic/social/entrainment/version.rb +13 -0
  43. data/lib/legion/extensions/agentic/social/entrainment.rb +19 -0
  44. data/lib/legion/extensions/agentic/social/governance/actors/shadow_ai_scan.rb +19 -0
  45. data/lib/legion/extensions/agentic/social/governance/actors/vote_timeout.rb +45 -0
  46. data/lib/legion/extensions/agentic/social/governance/client.rb +27 -0
  47. data/lib/legion/extensions/agentic/social/governance/helpers/layers.rb +40 -0
  48. data/lib/legion/extensions/agentic/social/governance/helpers/proposal.rb +94 -0
  49. data/lib/legion/extensions/agentic/social/governance/runners/governance.rb +87 -0
  50. data/lib/legion/extensions/agentic/social/governance/runners/shadow_ai.rb +93 -0
  51. data/lib/legion/extensions/agentic/social/governance/version.rb +13 -0
  52. data/lib/legion/extensions/agentic/social/governance.rb +20 -0
  53. data/lib/legion/extensions/agentic/social/joint_attention/actors/decay.rb +45 -0
  54. data/lib/legion/extensions/agentic/social/joint_attention/client.rb +28 -0
  55. data/lib/legion/extensions/agentic/social/joint_attention/helpers/attention_target.rb +124 -0
  56. data/lib/legion/extensions/agentic/social/joint_attention/helpers/constants.rb +34 -0
  57. data/lib/legion/extensions/agentic/social/joint_attention/helpers/joint_focus_manager.rb +157 -0
  58. data/lib/legion/extensions/agentic/social/joint_attention/runners/joint_attention.rb +88 -0
  59. data/lib/legion/extensions/agentic/social/joint_attention/version.rb +13 -0
  60. data/lib/legion/extensions/agentic/social/joint_attention.rb +20 -0
  61. data/lib/legion/extensions/agentic/social/mentalizing/actors/decay.rb +45 -0
  62. data/lib/legion/extensions/agentic/social/mentalizing/client.rb +28 -0
  63. data/lib/legion/extensions/agentic/social/mentalizing/helpers/belief_attribution.rb +58 -0
  64. data/lib/legion/extensions/agentic/social/mentalizing/helpers/constants.rb +33 -0
  65. data/lib/legion/extensions/agentic/social/mentalizing/helpers/mental_model.rb +137 -0
  66. data/lib/legion/extensions/agentic/social/mentalizing/runners/mentalizing.rb +93 -0
  67. data/lib/legion/extensions/agentic/social/mentalizing/version.rb +13 -0
  68. data/lib/legion/extensions/agentic/social/mentalizing.rb +20 -0
  69. data/lib/legion/extensions/agentic/social/mirror/client.rb +33 -0
  70. data/lib/legion/extensions/agentic/social/mirror/helpers/constants.rb +65 -0
  71. data/lib/legion/extensions/agentic/social/mirror/helpers/mirror_engine.rb +132 -0
  72. data/lib/legion/extensions/agentic/social/mirror/helpers/mirror_event.rb +46 -0
  73. data/lib/legion/extensions/agentic/social/mirror/helpers/simulation.rb +43 -0
  74. data/lib/legion/extensions/agentic/social/mirror/runners/observe.rb +52 -0
  75. data/lib/legion/extensions/agentic/social/mirror/runners/resonance.rb +79 -0
  76. data/lib/legion/extensions/agentic/social/mirror/runners/simulate.rb +63 -0
  77. data/lib/legion/extensions/agentic/social/mirror/version.rb +13 -0
  78. data/lib/legion/extensions/agentic/social/mirror.rb +22 -0
  79. data/lib/legion/extensions/agentic/social/mirror_system/actors/decay.rb +45 -0
  80. data/lib/legion/extensions/agentic/social/mirror_system/client.rb +28 -0
  81. data/lib/legion/extensions/agentic/social/mirror_system/helpers/constants.rb +62 -0
  82. data/lib/legion/extensions/agentic/social/mirror_system/helpers/mirror_system.rb +162 -0
  83. data/lib/legion/extensions/agentic/social/mirror_system/helpers/observed_behavior.rb +67 -0
  84. data/lib/legion/extensions/agentic/social/mirror_system/runners/mirror.rb +99 -0
  85. data/lib/legion/extensions/agentic/social/mirror_system/version.rb +13 -0
  86. data/lib/legion/extensions/agentic/social/mirror_system.rb +20 -0
  87. data/lib/legion/extensions/agentic/social/moral_reasoning/client.rb +19 -0
  88. data/lib/legion/extensions/agentic/social/moral_reasoning/helpers/constants.rb +49 -0
  89. data/lib/legion/extensions/agentic/social/moral_reasoning/helpers/dilemma.rb +68 -0
  90. data/lib/legion/extensions/agentic/social/moral_reasoning/helpers/llm_enhancer.rb +140 -0
  91. data/lib/legion/extensions/agentic/social/moral_reasoning/helpers/moral_engine.rb +239 -0
  92. data/lib/legion/extensions/agentic/social/moral_reasoning/helpers/moral_foundation.rb +45 -0
  93. data/lib/legion/extensions/agentic/social/moral_reasoning/runners/moral_reasoning.rb +121 -0
  94. data/lib/legion/extensions/agentic/social/moral_reasoning/version.rb +13 -0
  95. data/lib/legion/extensions/agentic/social/moral_reasoning.rb +21 -0
  96. data/lib/legion/extensions/agentic/social/perspective_shifting/client.rb +29 -0
  97. data/lib/legion/extensions/agentic/social/perspective_shifting/helpers/constants.rb +67 -0
  98. data/lib/legion/extensions/agentic/social/perspective_shifting/helpers/perspective.rb +45 -0
  99. data/lib/legion/extensions/agentic/social/perspective_shifting/helpers/perspective_view.rb +57 -0
  100. data/lib/legion/extensions/agentic/social/perspective_shifting/helpers/shifting_engine.rb +166 -0
  101. data/lib/legion/extensions/agentic/social/perspective_shifting/runners/perspective_shifting.rb +167 -0
  102. data/lib/legion/extensions/agentic/social/perspective_shifting/version.rb +13 -0
  103. data/lib/legion/extensions/agentic/social/perspective_shifting.rb +20 -0
  104. data/lib/legion/extensions/agentic/social/social/client.rb +25 -0
  105. data/lib/legion/extensions/agentic/social/social/helpers/constants.rb +84 -0
  106. data/lib/legion/extensions/agentic/social/social/helpers/social_graph.rb +172 -0
  107. data/lib/legion/extensions/agentic/social/social/runners/social.rb +146 -0
  108. data/lib/legion/extensions/agentic/social/social/version.rb +13 -0
  109. data/lib/legion/extensions/agentic/social/social.rb +18 -0
  110. data/lib/legion/extensions/agentic/social/social_learning/client.rb +25 -0
  111. data/lib/legion/extensions/agentic/social/social_learning/helpers/constants.rb +42 -0
  112. data/lib/legion/extensions/agentic/social/social_learning/helpers/model_agent.rb +82 -0
  113. data/lib/legion/extensions/agentic/social/social_learning/helpers/observed_behavior.rb +61 -0
  114. data/lib/legion/extensions/agentic/social/social_learning/helpers/social_learning_engine.rb +134 -0
  115. data/lib/legion/extensions/agentic/social/social_learning/runners/social_learning.rb +105 -0
  116. data/lib/legion/extensions/agentic/social/social_learning/version.rb +13 -0
  117. data/lib/legion/extensions/agentic/social/social_learning.rb +20 -0
  118. data/lib/legion/extensions/agentic/social/symbiosis/client.rb +23 -0
  119. data/lib/legion/extensions/agentic/social/symbiosis/helpers/constants.rb +50 -0
  120. data/lib/legion/extensions/agentic/social/symbiosis/helpers/ecosystem.rb +113 -0
  121. data/lib/legion/extensions/agentic/social/symbiosis/helpers/symbiosis_engine.rb +104 -0
  122. data/lib/legion/extensions/agentic/social/symbiosis/helpers/symbiotic_bond.rb +112 -0
  123. data/lib/legion/extensions/agentic/social/symbiosis/runners/cognitive_symbiosis.rb +101 -0
  124. data/lib/legion/extensions/agentic/social/symbiosis/version.rb +13 -0
  125. data/lib/legion/extensions/agentic/social/symbiosis.rb +22 -0
  126. data/lib/legion/extensions/agentic/social/theory_of_mind/client.rb +26 -0
  127. data/lib/legion/extensions/agentic/social/theory_of_mind/helpers/agent_model.rb +173 -0
  128. data/lib/legion/extensions/agentic/social/theory_of_mind/helpers/constants.rb +70 -0
  129. data/lib/legion/extensions/agentic/social/theory_of_mind/helpers/mental_state_tracker.rb +169 -0
  130. data/lib/legion/extensions/agentic/social/theory_of_mind/runners/theory_of_mind.rb +159 -0
  131. data/lib/legion/extensions/agentic/social/theory_of_mind/version.rb +13 -0
  132. data/lib/legion/extensions/agentic/social/theory_of_mind.rb +19 -0
  133. data/lib/legion/extensions/agentic/social/trust/actors/decay.rb +45 -0
  134. data/lib/legion/extensions/agentic/social/trust/client.rb +27 -0
  135. data/lib/legion/extensions/agentic/social/trust/helpers/trust_map.rb +160 -0
  136. data/lib/legion/extensions/agentic/social/trust/helpers/trust_model.rb +52 -0
  137. data/lib/legion/extensions/agentic/social/trust/local_migrations/20260316000020_create_trust_entries.rb +23 -0
  138. data/lib/legion/extensions/agentic/social/trust/runners/trust.rb +80 -0
  139. data/lib/legion/extensions/agentic/social/trust/version.rb +13 -0
  140. data/lib/legion/extensions/agentic/social/trust.rb +25 -0
  141. data/lib/legion/extensions/agentic/social/version.rb +11 -0
  142. data/lib/legion/extensions/agentic/social.rb +34 -0
  143. data/spec/legion/extensions/agentic/social/apprenticeship/client_spec.rb +20 -0
  144. data/spec/legion/extensions/agentic/social/apprenticeship/cognitive_apprenticeship_spec.rb +11 -0
  145. data/spec/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_engine_spec.rb +146 -0
  146. data/spec/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_model_spec.rb +124 -0
  147. data/spec/legion/extensions/agentic/social/apprenticeship/helpers/apprenticeship_spec.rb +136 -0
  148. data/spec/legion/extensions/agentic/social/apprenticeship/runners/cognitive_apprenticeship_spec.rb +154 -0
  149. data/spec/legion/extensions/agentic/social/conflict/actors/stale_check_spec.rb +45 -0
  150. data/spec/legion/extensions/agentic/social/conflict/client_spec.rb +15 -0
  151. data/spec/legion/extensions/agentic/social/conflict/helpers/conflict_log_spec.rb +232 -0
  152. data/spec/legion/extensions/agentic/social/conflict/helpers/llm_enhancer_spec.rb +189 -0
  153. data/spec/legion/extensions/agentic/social/conflict/helpers/severity_spec.rb +215 -0
  154. data/spec/legion/extensions/agentic/social/conflict/runners/conflict_spec.rb +151 -0
  155. data/spec/legion/extensions/agentic/social/conscience/client_spec.rb +58 -0
  156. data/spec/legion/extensions/agentic/social/conscience/helpers/constants_spec.rb +124 -0
  157. data/spec/legion/extensions/agentic/social/conscience/helpers/moral_evaluator_spec.rb +253 -0
  158. data/spec/legion/extensions/agentic/social/conscience/helpers/moral_store_spec.rb +230 -0
  159. data/spec/legion/extensions/agentic/social/conscience/runners/conscience_spec.rb +239 -0
  160. data/spec/legion/extensions/agentic/social/consent/actors/tier_evaluation_spec.rb +46 -0
  161. data/spec/legion/extensions/agentic/social/consent/client_spec.rb +33 -0
  162. data/spec/legion/extensions/agentic/social/consent/helpers/tiers_spec.rb +49 -0
  163. data/spec/legion/extensions/agentic/social/consent/local_persistence_spec.rb +234 -0
  164. data/spec/legion/extensions/agentic/social/consent/runners/consent_spec.rb +224 -0
  165. data/spec/legion/extensions/agentic/social/entrainment/client_spec.rb +21 -0
  166. data/spec/legion/extensions/agentic/social/entrainment/helpers/entrainment_engine_spec.rb +116 -0
  167. data/spec/legion/extensions/agentic/social/entrainment/helpers/pairing_spec.rb +103 -0
  168. data/spec/legion/extensions/agentic/social/entrainment/runners/cognitive_entrainment_spec.rb +87 -0
  169. data/spec/legion/extensions/agentic/social/governance/actors/vote_timeout_spec.rb +45 -0
  170. data/spec/legion/extensions/agentic/social/governance/client_spec.rb +14 -0
  171. data/spec/legion/extensions/agentic/social/governance/helpers/layers_spec.rb +190 -0
  172. data/spec/legion/extensions/agentic/social/governance/helpers/proposal_spec.rb +188 -0
  173. data/spec/legion/extensions/agentic/social/governance/runners/governance_spec.rb +101 -0
  174. data/spec/legion/extensions/agentic/social/governance/runners/shadow_ai_spec.rb +65 -0
  175. data/spec/legion/extensions/agentic/social/joint_attention/client_spec.rb +36 -0
  176. data/spec/legion/extensions/agentic/social/joint_attention/helpers/attention_target_spec.rb +258 -0
  177. data/spec/legion/extensions/agentic/social/joint_attention/helpers/joint_focus_manager_spec.rb +238 -0
  178. data/spec/legion/extensions/agentic/social/joint_attention/runners/joint_attention_spec.rb +228 -0
  179. data/spec/legion/extensions/agentic/social/mentalizing/client_spec.rb +19 -0
  180. data/spec/legion/extensions/agentic/social/mentalizing/helpers/belief_attribution_spec.rb +108 -0
  181. data/spec/legion/extensions/agentic/social/mentalizing/helpers/mental_model_spec.rb +179 -0
  182. data/spec/legion/extensions/agentic/social/mentalizing/runners/mentalizing_spec.rb +162 -0
  183. data/spec/legion/extensions/agentic/social/mirror/client_spec.rb +92 -0
  184. data/spec/legion/extensions/agentic/social/mirror/helpers/constants_spec.rb +123 -0
  185. data/spec/legion/extensions/agentic/social/mirror/helpers/mirror_engine_spec.rb +217 -0
  186. data/spec/legion/extensions/agentic/social/mirror/helpers/mirror_event_spec.rb +102 -0
  187. data/spec/legion/extensions/agentic/social/mirror/helpers/simulation_spec.rb +100 -0
  188. data/spec/legion/extensions/agentic/social/mirror/runners/observe_spec.rb +77 -0
  189. data/spec/legion/extensions/agentic/social/mirror/runners/resonance_spec.rb +123 -0
  190. data/spec/legion/extensions/agentic/social/mirror/runners/simulate_spec.rb +103 -0
  191. data/spec/legion/extensions/agentic/social/mirror_system/client_spec.rb +40 -0
  192. data/spec/legion/extensions/agentic/social/mirror_system/helpers/mirror_system_spec.rb +144 -0
  193. data/spec/legion/extensions/agentic/social/mirror_system/helpers/observed_behavior_spec.rb +98 -0
  194. data/spec/legion/extensions/agentic/social/mirror_system/runners/mirror_spec.rb +122 -0
  195. data/spec/legion/extensions/agentic/social/moral_reasoning/client_spec.rb +34 -0
  196. data/spec/legion/extensions/agentic/social/moral_reasoning/helpers/dilemma_spec.rb +108 -0
  197. data/spec/legion/extensions/agentic/social/moral_reasoning/helpers/llm_enhancer_spec.rb +232 -0
  198. data/spec/legion/extensions/agentic/social/moral_reasoning/helpers/moral_engine_spec.rb +266 -0
  199. data/spec/legion/extensions/agentic/social/moral_reasoning/helpers/moral_foundation_spec.rb +70 -0
  200. data/spec/legion/extensions/agentic/social/moral_reasoning/runners/moral_reasoning_spec.rb +275 -0
  201. data/spec/legion/extensions/agentic/social/perspective_shifting/client_spec.rb +30 -0
  202. data/spec/legion/extensions/agentic/social/perspective_shifting/helpers/constants_spec.rb +99 -0
  203. data/spec/legion/extensions/agentic/social/perspective_shifting/helpers/perspective_spec.rb +77 -0
  204. data/spec/legion/extensions/agentic/social/perspective_shifting/helpers/perspective_view_spec.rb +105 -0
  205. data/spec/legion/extensions/agentic/social/perspective_shifting/helpers/shifting_engine_spec.rb +246 -0
  206. data/spec/legion/extensions/agentic/social/perspective_shifting/runners/perspective_shifting_spec.rb +277 -0
  207. data/spec/legion/extensions/agentic/social/social/client_spec.rb +72 -0
  208. data/spec/legion/extensions/agentic/social/social/helpers/constants_spec.rb +99 -0
  209. data/spec/legion/extensions/agentic/social/social/helpers/social_graph_spec.rb +322 -0
  210. data/spec/legion/extensions/agentic/social/social/runners/social_spec.rb +220 -0
  211. data/spec/legion/extensions/agentic/social/social_learning/client_spec.rb +25 -0
  212. data/spec/legion/extensions/agentic/social/social_learning/helpers/constants_spec.rb +44 -0
  213. data/spec/legion/extensions/agentic/social/social_learning/helpers/model_agent_spec.rb +120 -0
  214. data/spec/legion/extensions/agentic/social/social_learning/helpers/observed_behavior_spec.rb +81 -0
  215. data/spec/legion/extensions/agentic/social/social_learning/helpers/social_learning_engine_spec.rb +196 -0
  216. data/spec/legion/extensions/agentic/social/social_learning/runners/social_learning_spec.rb +150 -0
  217. data/spec/legion/extensions/agentic/social/symbiosis/client_spec.rb +45 -0
  218. data/spec/legion/extensions/agentic/social/symbiosis/helpers/constants_spec.rb +73 -0
  219. data/spec/legion/extensions/agentic/social/symbiosis/helpers/ecosystem_spec.rb +185 -0
  220. data/spec/legion/extensions/agentic/social/symbiosis/helpers/symbiosis_engine_spec.rb +182 -0
  221. data/spec/legion/extensions/agentic/social/symbiosis/helpers/symbiotic_bond_spec.rb +209 -0
  222. data/spec/legion/extensions/agentic/social/symbiosis/runners/cognitive_symbiosis_spec.rb +182 -0
  223. data/spec/legion/extensions/agentic/social/theory_of_mind/client_spec.rb +63 -0
  224. data/spec/legion/extensions/agentic/social/theory_of_mind/helpers/agent_model_spec.rb +244 -0
  225. data/spec/legion/extensions/agentic/social/theory_of_mind/helpers/constants_spec.rb +71 -0
  226. data/spec/legion/extensions/agentic/social/theory_of_mind/helpers/mental_state_tracker_spec.rb +228 -0
  227. data/spec/legion/extensions/agentic/social/theory_of_mind/runners/theory_of_mind_spec.rb +221 -0
  228. data/spec/legion/extensions/agentic/social/trust/actors/decay_spec.rb +62 -0
  229. data/spec/legion/extensions/agentic/social/trust/client_spec.rb +17 -0
  230. data/spec/legion/extensions/agentic/social/trust/helpers/trust_map_spec.rb +299 -0
  231. data/spec/legion/extensions/agentic/social/trust/helpers/trust_model_spec.rb +179 -0
  232. data/spec/legion/extensions/agentic/social/trust/local_persistence_spec.rb +359 -0
  233. data/spec/legion/extensions/agentic/social/trust/runners/trust_spec.rb +84 -0
  234. data/spec/spec_helper.rb +54 -0
  235. metadata +319 -0
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'sequel'
5
+ require 'json'
6
+ require 'tmpdir'
7
+
8
+ RSpec.describe 'lex-consent local SQLite persistence' do
9
+ let(:db_path) { File.join(Dir.tmpdir, "consent_test_#{Process.pid}_#{rand(9999)}.db") }
10
+ let(:db) { Sequel.sqlite(db_path) }
11
+
12
+ before do
13
+ # Create the schema in the temp DB
14
+ db.create_table(:consent_domains) do
15
+ primary_key :id
16
+ String :domain_key, null: false, unique: true
17
+ String :tier, null: false, default: 'consult'
18
+ Integer :success_count, default: 0
19
+ Integer :failure_count, default: 0
20
+ Integer :total_actions, default: 0
21
+ DateTime :last_changed_at
22
+ String :history, text: true
23
+ end
24
+
25
+ # Stub Legion::Data::Local to use our temp DB
26
+ stub_const('Legion::Data::Local', Module.new do
27
+ def self.connected?
28
+ true
29
+ end
30
+
31
+ def self.connection
32
+ @_connection
33
+ end
34
+
35
+ def self._set_connection(conn)
36
+ @_connection = conn
37
+ end
38
+ end)
39
+
40
+ Legion::Data::Local._set_connection(db)
41
+ end
42
+
43
+ after do
44
+ db.disconnect
45
+ FileUtils.rm_f(db_path)
46
+ end
47
+
48
+ describe 'save_to_local' do
49
+ it 'writes domain state to the database' do
50
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
51
+ map.record_outcome('email', success: true)
52
+ map.record_outcome('email', success: true)
53
+ map.save_to_local
54
+
55
+ row = db[:consent_domains].where(domain_key: 'email').first
56
+ expect(row).not_to be_nil
57
+ expect(row[:domain_key]).to eq('email')
58
+ expect(row[:tier]).to eq('consult')
59
+ expect(row[:success_count]).to eq(2)
60
+ expect(row[:total_actions]).to eq(2)
61
+ end
62
+
63
+ it 'updates an existing row on second save' do
64
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
65
+ map.record_outcome('email', success: true)
66
+ map.save_to_local
67
+
68
+ map.record_outcome('email', success: false)
69
+ map.save_to_local
70
+
71
+ rows = db[:consent_domains].where(domain_key: 'email').all
72
+ expect(rows.size).to eq(1)
73
+ expect(rows.first[:failure_count]).to eq(1)
74
+ expect(rows.first[:total_actions]).to eq(2)
75
+ end
76
+
77
+ it 'serializes tier as a string' do
78
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
79
+ map.set_tier('scheduling', :autonomous)
80
+ map.save_to_local
81
+
82
+ row = db[:consent_domains].where(domain_key: 'scheduling').first
83
+ expect(row[:tier]).to eq('autonomous')
84
+ end
85
+
86
+ it 'serializes history as JSON' do
87
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
88
+ map.set_tier('scheduling', :autonomous)
89
+ map.save_to_local
90
+
91
+ row = db[:consent_domains].where(domain_key: 'scheduling').first
92
+ parsed = JSON.parse(row[:history])
93
+ expect(parsed).to be_an(Array)
94
+ expect(parsed.first['from']).to eq('consult')
95
+ expect(parsed.first['to']).to eq('autonomous')
96
+ end
97
+
98
+ it 'persists multiple domains' do
99
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
100
+ map.record_outcome('email', success: true)
101
+ map.record_outcome('calendar', success: false)
102
+ map.save_to_local
103
+
104
+ expect(db[:consent_domains].count).to eq(2)
105
+ end
106
+ end
107
+
108
+ describe 'load_from_local' do
109
+ it 'restores domain state from the database' do
110
+ db[:consent_domains].insert(
111
+ domain_key: 'email',
112
+ tier: 'act_notify',
113
+ success_count: 5,
114
+ failure_count: 1,
115
+ total_actions: 6,
116
+ history: '[]'
117
+ )
118
+
119
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
120
+ expect(map.get_tier('email')).to eq(:act_notify)
121
+ expect(map.domains['email'][:success_count]).to eq(5)
122
+ expect(map.domains['email'][:total_actions]).to eq(6)
123
+ end
124
+
125
+ it 'restores history as an array of hashes with symbol keys' do
126
+ history_json = JSON.generate([{ 'from' => 'consult', 'to' => 'act_notify', 'at' => Time.now.utc.to_s }])
127
+ db[:consent_domains].insert(
128
+ domain_key: 'scheduling',
129
+ tier: 'act_notify',
130
+ success_count: 10,
131
+ failure_count: 0,
132
+ total_actions: 10,
133
+ history: history_json
134
+ )
135
+
136
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
137
+ history = map.domains['scheduling'][:history]
138
+ expect(history).to be_an(Array)
139
+ expect(history.first[:from]).to eq(:consult)
140
+ expect(history.first[:to]).to eq(:act_notify)
141
+ end
142
+
143
+ it 'handles empty history JSON gracefully' do
144
+ db[:consent_domains].insert(
145
+ domain_key: 'empty_history',
146
+ tier: 'consult',
147
+ success_count: 0,
148
+ failure_count: 0,
149
+ total_actions: 0,
150
+ history: '[]'
151
+ )
152
+
153
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
154
+ expect(map.domains['empty_history'][:history]).to eq([])
155
+ end
156
+ end
157
+
158
+ describe 'round-trip persistence' do
159
+ it 'saves and restores earned tier' do
160
+ # First instance: build history and promote
161
+ map1 = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
162
+ 15.times { map1.record_outcome('tasks', success: true) }
163
+ map1.set_tier('tasks', :act_notify)
164
+ map1.save_to_local
165
+
166
+ # Second instance: loads from DB
167
+ map2 = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
168
+ expect(map2.get_tier('tasks')).to eq(:act_notify)
169
+ expect(map2.domains['tasks'][:success_count]).to eq(15)
170
+ expect(map2.domains['tasks'][:total_actions]).to eq(15)
171
+ end
172
+
173
+ it 'round-trips multiple domains independently' do
174
+ map1 = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
175
+ map1.set_tier('email', :autonomous)
176
+ map1.record_outcome('calendar', success: true)
177
+ map1.record_outcome('calendar', success: false)
178
+ map1.save_to_local
179
+
180
+ map2 = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
181
+ expect(map2.get_tier('email')).to eq(:autonomous)
182
+ expect(map2.get_tier('calendar')).to eq(:consult)
183
+ expect(map2.domains['calendar'][:success_count]).to eq(1)
184
+ expect(map2.domains['calendar'][:failure_count]).to eq(1)
185
+ end
186
+ end
187
+
188
+ describe 'graceful no-op when Legion::Data::Local is unavailable' do
189
+ before do
190
+ # Simulate no Legion::Data::Local defined
191
+ hide_const('Legion::Data::Local')
192
+ end
193
+
194
+ it 'save_to_local does nothing without raising' do
195
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
196
+ map.record_outcome('email', success: true)
197
+ expect { map.save_to_local }.not_to raise_error
198
+ end
199
+
200
+ it 'load_from_local does nothing without raising (no DB loaded at init)' do
201
+ expect { Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new }.not_to raise_error
202
+ end
203
+
204
+ it 'starts with default in-memory state' do
205
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
206
+ expect(map.get_tier('email')).to eq(:consult)
207
+ end
208
+ end
209
+
210
+ describe 'graceful no-op when Legion::Data::Local is defined but not connected' do
211
+ before do
212
+ stub_const('Legion::Data::Local', Module.new do
213
+ def self.connected?
214
+ false
215
+ end
216
+ end)
217
+ end
218
+
219
+ it 'save_to_local does nothing without raising' do
220
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
221
+ map.record_outcome('email', success: true)
222
+ expect { map.save_to_local }.not_to raise_error
223
+ end
224
+
225
+ it 'initialize completes without raising' do
226
+ expect { Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new }.not_to raise_error
227
+ end
228
+
229
+ it 'starts with default in-memory state' do
230
+ map = Legion::Extensions::Agentic::Social::Consent::Helpers::ConsentMap.new
231
+ expect(map.get_tier('email')).to eq(:consult)
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/social/consent/client'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Social::Consent::Runners::Consent do
6
+ let(:client) { Legion::Extensions::Agentic::Social::Consent::Client.new }
7
+
8
+ describe '#check_consent' do
9
+ it 'returns default tier for new domain' do
10
+ result = client.check_consent(domain: 'email')
11
+ expect(result[:tier]).to eq(:consult)
12
+ expect(result[:needs_consult]).to be true
13
+ end
14
+ end
15
+
16
+ describe '#record_action' do
17
+ it 'records action outcome' do
18
+ result = client.record_action(domain: 'email', success: true)
19
+ expect(result[:success]).to be true
20
+ expect(result[:total]).to eq(1)
21
+ end
22
+
23
+ it 'tracks success rate' do
24
+ 3.times { client.record_action(domain: 'email', success: true) }
25
+ client.record_action(domain: 'email', success: false)
26
+ result = client.record_action(domain: 'email', success: true)
27
+ expect(result[:success_rate]).to eq(0.8)
28
+ end
29
+ end
30
+
31
+ describe '#evaluate_tier_change' do
32
+ it 'returns ineligible when not enough actions' do
33
+ result = client.evaluate_tier_change(domain: 'email')
34
+ expect(result[:recommendation]).to eq(:ineligible)
35
+ end
36
+
37
+ it 'recommends promotion with high success rate' do
38
+ 15.times { client.record_action(domain: 'calendar', success: true) }
39
+ result = client.evaluate_tier_change(domain: 'calendar')
40
+ expect(result[:recommendation]).to eq(:promote)
41
+ expect(result[:proposed_tier]).to eq(:act_notify)
42
+ end
43
+
44
+ it 'recommends demotion with low success rate' do
45
+ 4.times { client.record_action(domain: 'risky', success: true) }
46
+ 8.times { client.record_action(domain: 'risky', success: false) }
47
+ result = client.evaluate_tier_change(domain: 'risky')
48
+ expect(result[:recommendation]).to eq(:demote)
49
+ expect(result[:proposed_tier]).to eq(:human_only)
50
+ end
51
+ end
52
+
53
+ describe '#apply_tier_change' do
54
+ it 'changes tier' do
55
+ result = client.apply_tier_change(domain: 'email', new_tier: :autonomous)
56
+ expect(result[:new_tier]).to eq(:autonomous)
57
+ expect(result[:changed]).to be true
58
+ end
59
+
60
+ it 'rejects invalid tier' do
61
+ result = client.apply_tier_change(domain: 'email', new_tier: :invalid)
62
+ expect(result[:error]).to eq(:invalid_tier)
63
+ end
64
+ end
65
+
66
+ describe '#evaluate_all_tiers' do
67
+ it 'returns zero evaluated for empty consent map' do
68
+ result = client.evaluate_all_tiers
69
+ expect(result[:evaluated]).to eq(0)
70
+ expect(result[:promotions]).to eq([])
71
+ expect(result[:demotions]).to eq([])
72
+ end
73
+
74
+ it 'includes promoted domains in promotions list' do
75
+ 15.times { client.record_action(domain: 'calendar', success: true) }
76
+ result = client.evaluate_all_tiers
77
+ expect(result[:promotions]).to include('calendar')
78
+ end
79
+
80
+ it 'includes demoted domains in demotions list' do
81
+ 4.times { client.record_action(domain: 'risky', success: true) }
82
+ 8.times { client.record_action(domain: 'risky', success: false) }
83
+ result = client.evaluate_all_tiers
84
+ expect(result[:demotions]).to include('risky')
85
+ end
86
+
87
+ it 'returns evaluated count matching number of known domains' do
88
+ client.record_action(domain: 'email', success: true)
89
+ client.record_action(domain: 'calendar', success: true)
90
+ result = client.evaluate_all_tiers
91
+ expect(result[:evaluated]).to eq(2)
92
+ end
93
+
94
+ it 'does not include ineligible domains in promotions or demotions' do
95
+ client.record_action(domain: 'email', success: true)
96
+ result = client.evaluate_all_tiers
97
+ expect(result[:promotions]).not_to include('email')
98
+ expect(result[:demotions]).not_to include('email')
99
+ end
100
+ end
101
+
102
+ describe '#request_autonomous_approval' do
103
+ it 'sets pending state for non-autonomous domain' do
104
+ client.record_action(domain: 'email', success: true)
105
+ result = client.request_autonomous_approval(domain: 'email')
106
+ expect(result[:requested]).to be true
107
+ expect(result[:proposed_tier]).to eq(:autonomous)
108
+ end
109
+
110
+ it 'rejects if already autonomous' do
111
+ client.apply_tier_change(domain: 'email', new_tier: :autonomous)
112
+ result = client.request_autonomous_approval(domain: 'email')
113
+ expect(result[:requested]).to be false
114
+ expect(result[:error]).to eq('already_autonomous')
115
+ end
116
+
117
+ it 'rejects if already pending' do
118
+ client.request_autonomous_approval(domain: 'email')
119
+ result = client.request_autonomous_approval(domain: 'email')
120
+ expect(result[:requested]).to be false
121
+ expect(result[:error]).to eq('already_pending')
122
+ end
123
+ end
124
+
125
+ describe '#approve_promotion' do
126
+ it 'applies pending tier change' do
127
+ client.request_autonomous_approval(domain: 'email')
128
+ result = client.approve_promotion(domain: 'email', approved_by: 'admin')
129
+ expect(result[:approved]).to be true
130
+ expect(result[:new_tier]).to eq(:autonomous)
131
+ expect(result[:approved_by]).to eq('admin')
132
+ end
133
+
134
+ it 'rejects when no pending approval' do
135
+ result = client.approve_promotion(domain: 'email', approved_by: 'admin')
136
+ expect(result[:approved]).to be false
137
+ expect(result[:error]).to eq('no_pending_approval')
138
+ end
139
+ end
140
+
141
+ describe '#reject_promotion' do
142
+ it 'clears pending state' do
143
+ client.request_autonomous_approval(domain: 'email')
144
+ result = client.reject_promotion(domain: 'email', rejected_by: 'admin', reason: 'not ready')
145
+ expect(result[:rejected]).to be true
146
+ expect(result[:reason]).to eq('not ready')
147
+ end
148
+
149
+ it 'rejects when no pending approval' do
150
+ result = client.reject_promotion(domain: 'email', rejected_by: 'admin')
151
+ expect(result[:rejected]).to be false
152
+ end
153
+ end
154
+
155
+ describe '#expire_pending_approvals' do
156
+ it 'expires stale approvals' do
157
+ client.request_autonomous_approval(domain: 'email')
158
+ result = client.expire_pending_approvals(timeout: 0)
159
+ expect(result[:expired]).to eq(1)
160
+ end
161
+
162
+ it 'does not expire fresh approvals' do
163
+ client.request_autonomous_approval(domain: 'email')
164
+ result = client.expire_pending_approvals(timeout: 999_999)
165
+ expect(result[:expired]).to eq(0)
166
+ end
167
+ end
168
+
169
+ describe '#evaluate_and_apply_tiers' do
170
+ it 'returns summary with zero counts for empty map' do
171
+ result = client.evaluate_and_apply_tiers
172
+ expect(result[:evaluated]).to eq(0)
173
+ expect(result[:applied_promotions]).to eq(0)
174
+ expect(result[:applied_demotions]).to eq(0)
175
+ expect(result[:approval_requests]).to eq(0)
176
+ end
177
+
178
+ it 'auto-applies non-autonomous promotions' do
179
+ 15.times { client.record_action(domain: 'calendar', success: true) }
180
+ result = client.evaluate_and_apply_tiers
181
+ expect(result[:applied_promotions]).to eq(1)
182
+ status = client.consent_status(domain: 'calendar')
183
+ expect(status[:tier]).to eq(:act_notify)
184
+ end
185
+
186
+ it 'requests approval for autonomous promotions instead of auto-applying' do
187
+ client.apply_tier_change(domain: 'calendar', new_tier: :act_notify)
188
+ # Backdate last_changed_at to bypass the 24h cooldown
189
+ map = client.send(:consent_map)
190
+ map.domains['calendar'][:last_changed_at] = Time.now.utc - 100_000
191
+ 15.times { client.record_action(domain: 'calendar', success: true) }
192
+ result = client.evaluate_and_apply_tiers
193
+ expect(result[:approval_requests]).to eq(1)
194
+ expect(result[:applied_promotions]).to eq(0)
195
+ status = client.consent_status(domain: 'calendar')
196
+ expect(status[:tier]).to eq(:act_notify)
197
+ end
198
+
199
+ it 'auto-applies demotions' do
200
+ 4.times { client.record_action(domain: 'risky', success: true) }
201
+ 8.times { client.record_action(domain: 'risky', success: false) }
202
+ result = client.evaluate_and_apply_tiers
203
+ expect(result[:applied_demotions]).to eq(1)
204
+ status = client.consent_status(domain: 'risky')
205
+ expect(status[:tier]).to eq(:human_only)
206
+ end
207
+ end
208
+
209
+ describe '#consent_status' do
210
+ it 'returns domain-specific status' do
211
+ client.record_action(domain: 'email', success: true)
212
+ result = client.consent_status(domain: 'email')
213
+ expect(result[:tier]).to eq(:consult)
214
+ expect(result[:total]).to eq(1)
215
+ end
216
+
217
+ it 'returns all domains when no domain specified' do
218
+ client.record_action(domain: 'email', success: true)
219
+ client.record_action(domain: 'calendar', success: true)
220
+ result = client.consent_status
221
+ expect(result[:count]).to eq(2)
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Social::Entrainment::Client do
4
+ subject(:client) { described_class.new }
5
+
6
+ it 'creates a pairing and records interaction' do
7
+ created = client.create_entrainment_pairing(
8
+ agent_a: 'alpha', agent_b: 'beta', domain: :reasoning
9
+ )
10
+ result = client.record_entrainment_interaction(
11
+ pairing_id: created[:pairing_id], aligned: true
12
+ )
13
+ expect(result[:success]).to be true
14
+ expect(result[:synchrony]).to be > 0.0
15
+ end
16
+
17
+ it 'returns stats' do
18
+ result = client.cognitive_entrainment_stats
19
+ expect(result[:success]).to be true
20
+ end
21
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Social::Entrainment::Helpers::EntrainmentEngine do
4
+ subject(:engine) { described_class.new }
5
+
6
+ let(:pairing) { engine.create_pairing(agent_a: 'alpha', agent_b: 'beta', domain: :reasoning) }
7
+
8
+ describe '#create_pairing' do
9
+ it 'creates a pairing' do
10
+ result = pairing
11
+ expect(result).to be_a(Legion::Extensions::Agentic::Social::Entrainment::Helpers::Pairing)
12
+ end
13
+
14
+ it 'returns existing pairing for same agents and domain' do
15
+ first = pairing
16
+ second = engine.create_pairing(agent_a: 'alpha', agent_b: 'beta', domain: :reasoning)
17
+ expect(second.id).to eq(first.id)
18
+ end
19
+
20
+ it 'records history' do
21
+ pairing
22
+ expect(engine.history.size).to eq(1)
23
+ end
24
+ end
25
+
26
+ describe '#record_interaction' do
27
+ it 'records an aligned interaction' do
28
+ result = engine.record_interaction(pairing_id: pairing.id, aligned: true)
29
+ expect(result[:success]).to be true
30
+ expect(result[:synchrony]).to be > 0.0
31
+ end
32
+
33
+ it 'returns error for unknown pairing' do
34
+ result = engine.record_interaction(pairing_id: 'bad', aligned: true)
35
+ expect(result[:success]).to be false
36
+ end
37
+ end
38
+
39
+ describe '#pairings_for' do
40
+ it 'finds pairings involving an agent' do
41
+ pairing
42
+ results = engine.pairings_for(agent_id: 'alpha')
43
+ expect(results.size).to eq(1)
44
+ end
45
+ end
46
+
47
+ describe '#entrained_pairings' do
48
+ it 'returns entrained pairings' do
49
+ 10.times { engine.record_interaction(pairing_id: pairing.id, aligned: true) }
50
+ expect(engine.entrained_pairings.size).to eq(1)
51
+ end
52
+ end
53
+
54
+ describe '#entrained_partners' do
55
+ it 'returns entrained partner agent ids' do
56
+ 10.times { engine.record_interaction(pairing_id: pairing.id, aligned: true) }
57
+ partners = engine.entrained_partners(agent_id: 'alpha')
58
+ expect(partners).to include('beta')
59
+ end
60
+ end
61
+
62
+ describe '#strongest_pairings' do
63
+ it 'returns sorted by synchrony' do
64
+ other = engine.create_pairing(agent_a: 'alpha', agent_b: 'gamma', domain: :planning)
65
+ 5.times { engine.record_interaction(pairing_id: pairing.id, aligned: true) }
66
+ 2.times { engine.record_interaction(pairing_id: other.id, aligned: true) }
67
+ results = engine.strongest_pairings(limit: 2)
68
+ expect(results.first.synchrony).to be >= results.last.synchrony
69
+ end
70
+ end
71
+
72
+ describe '#by_domain' do
73
+ it 'filters by domain' do
74
+ pairing
75
+ results = engine.by_domain(domain: :reasoning)
76
+ expect(results.size).to eq(1)
77
+ end
78
+ end
79
+
80
+ describe '#overall_entrainment' do
81
+ it 'returns 0.0 with no pairings' do
82
+ expect(engine.overall_entrainment).to eq(0.0)
83
+ end
84
+
85
+ it 'returns mean synchrony' do
86
+ 3.times { engine.record_interaction(pairing_id: pairing.id, aligned: true) }
87
+ expect(engine.overall_entrainment).to be > 0.0
88
+ end
89
+ end
90
+
91
+ describe '#drift_all' do
92
+ it 'reduces synchrony' do
93
+ 5.times { engine.record_interaction(pairing_id: pairing.id, aligned: true) }
94
+ original = pairing.synchrony
95
+ engine.drift_all
96
+ expect(pairing.synchrony).to be < original
97
+ end
98
+ end
99
+
100
+ describe '#prune_independent' do
101
+ it 'removes very low synchrony pairings' do
102
+ pairing
103
+ pruned = engine.prune_independent
104
+ expect(pruned).to be >= 1
105
+ end
106
+ end
107
+
108
+ describe '#to_h' do
109
+ it 'returns summary stats' do
110
+ pairing
111
+ stats = engine.to_h
112
+ expect(stats[:total_pairings]).to be >= 0
113
+ expect(stats).to include(:entrained_count, :overall_sync)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Social::Entrainment::Helpers::Pairing do
4
+ subject(:pairing) do
5
+ described_class.new(agent_a: 'alpha', agent_b: 'beta', domain: :reasoning)
6
+ end
7
+
8
+ describe '#initialize' do
9
+ it 'assigns a UUID' do
10
+ expect(pairing.id).to match(/\A[0-9a-f-]{36}\z/)
11
+ end
12
+
13
+ it 'starts with zero synchrony' do
14
+ expect(pairing.synchrony).to eq(0.0)
15
+ end
16
+ end
17
+
18
+ describe '#interact!' do
19
+ it 'increases synchrony on aligned interaction' do
20
+ pairing.interact!(aligned: true)
21
+ expect(pairing.synchrony).to be > 0.0
22
+ end
23
+
24
+ it 'decreases synchrony on misaligned interaction' do
25
+ 3.times { pairing.interact!(aligned: true) }
26
+ original = pairing.synchrony
27
+ pairing.interact!(aligned: false)
28
+ expect(pairing.synchrony).to be < original
29
+ end
30
+
31
+ it 'increments interaction count' do
32
+ expect { pairing.interact!(aligned: true) }.to change(pairing, :interaction_count).by(1)
33
+ end
34
+ end
35
+
36
+ describe '#drift!' do
37
+ it 'reduces synchrony' do
38
+ 3.times { pairing.interact!(aligned: true) }
39
+ original = pairing.synchrony
40
+ pairing.drift!
41
+ expect(pairing.synchrony).to be < original
42
+ end
43
+ end
44
+
45
+ describe '#entrained?' do
46
+ it 'returns false initially' do
47
+ expect(pairing).not_to be_entrained
48
+ end
49
+
50
+ it 'returns true after many aligned interactions' do
51
+ 10.times { pairing.interact!(aligned: true) }
52
+ expect(pairing).to be_entrained
53
+ end
54
+ end
55
+
56
+ describe '#partially_entrained?' do
57
+ it 'returns true at moderate synchrony' do
58
+ 5.times { pairing.interact!(aligned: true) }
59
+ expect(pairing).to be_partially_entrained
60
+ end
61
+ end
62
+
63
+ describe '#sync_label' do
64
+ it 'returns a symbol' do
65
+ expect(pairing.sync_label).to be_a(Symbol)
66
+ end
67
+ end
68
+
69
+ describe '#involves?' do
70
+ it 'returns true for agent_a' do
71
+ expect(pairing.involves?('alpha')).to be true
72
+ end
73
+
74
+ it 'returns true for agent_b' do
75
+ expect(pairing.involves?('beta')).to be true
76
+ end
77
+
78
+ it 'returns false for other agent' do
79
+ expect(pairing.involves?('gamma')).to be false
80
+ end
81
+ end
82
+
83
+ describe '#partner_of' do
84
+ it 'returns agent_b for agent_a' do
85
+ expect(pairing.partner_of('alpha')).to eq('beta')
86
+ end
87
+
88
+ it 'returns agent_a for agent_b' do
89
+ expect(pairing.partner_of('beta')).to eq('alpha')
90
+ end
91
+
92
+ it 'returns nil for unknown agent' do
93
+ expect(pairing.partner_of('gamma')).to be_nil
94
+ end
95
+ end
96
+
97
+ describe '#to_h' do
98
+ it 'returns hash representation' do
99
+ hash = pairing.to_h
100
+ expect(hash).to include(:id, :agent_a, :agent_b, :synchrony, :entrained)
101
+ end
102
+ end
103
+ end