lex-cognitive-pendulum 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.
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitivePendulum::Helpers::PendulumEngine do
4
+ subject(:engine) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'starts with empty pendulums hash' do
8
+ expect(engine.pendulums).to be_empty
9
+ end
10
+
11
+ it 'reports count of 0' do
12
+ expect(engine.count).to eq(0)
13
+ end
14
+ end
15
+
16
+ describe '#create_pendulum' do
17
+ it 'creates a pendulum and returns it' do
18
+ p = engine.create_pendulum(pole_pair: :certainty_doubt)
19
+ expect(p).to be_a(Legion::Extensions::CognitivePendulum::Helpers::Pendulum)
20
+ end
21
+
22
+ it 'stores the pendulum by id' do
23
+ p = engine.create_pendulum(pole_pair: :focus_diffusion)
24
+ expect(engine.pendulums[p.id]).to eq(p)
25
+ end
26
+
27
+ it 'increments count' do
28
+ engine.create_pendulum(pole_pair: :certainty_doubt)
29
+ expect(engine.count).to eq(1)
30
+ end
31
+
32
+ it 'accepts amplitude, period, and damping' do
33
+ p = engine.create_pendulum(pole_pair: :analysis_intuition, amplitude: 0.8, period: 20.0, damping: 0.05)
34
+ expect(p.amplitude).to eq(0.8)
35
+ expect(p.period).to eq(20.0)
36
+ expect(p.damping).to eq(0.05)
37
+ end
38
+
39
+ it 'raises when max pendulums reached' do
40
+ Legion::Extensions::CognitivePendulum::Helpers::Constants::MAX_PENDULUMS.times do
41
+ engine.create_pendulum(pole_pair: :certainty_doubt)
42
+ end
43
+ expect { engine.create_pendulum(pole_pair: :certainty_doubt) }.to raise_error(ArgumentError, /max pendulums/)
44
+ end
45
+ end
46
+
47
+ describe '#swing' do
48
+ let!(:pendulum) { engine.create_pendulum(pole_pair: :focus_diffusion) }
49
+
50
+ it 'swings the pendulum and returns it' do
51
+ result = engine.swing(pendulum.id, force: 0.5)
52
+ expect(result).to eq(pendulum)
53
+ end
54
+
55
+ it 'updates position on the pendulum' do
56
+ engine.swing(pendulum.id, force: 0.6)
57
+ expect(pendulum.current_position).to eq(0.6)
58
+ end
59
+
60
+ it 'returns nil for unknown id' do
61
+ expect(engine.swing('no-such-id', force: 0.1)).to be_nil
62
+ end
63
+ end
64
+
65
+ describe '#damp_all!' do
66
+ it 'damps every pendulum' do
67
+ p1 = engine.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.8)
68
+ p2 = engine.create_pendulum(pole_pair: :focus_diffusion, amplitude: 0.6)
69
+ engine.damp_all!
70
+ expect(p1.amplitude).to be < 0.8
71
+ expect(p2.amplitude).to be < 0.6
72
+ end
73
+
74
+ it 'does nothing with no pendulums' do
75
+ expect { engine.damp_all! }.not_to raise_error
76
+ end
77
+ end
78
+
79
+ describe '#check_resonance' do
80
+ it 'returns ids of resonant pendulums' do
81
+ p = engine.create_pendulum(pole_pair: :analysis_intuition, period: 10.0)
82
+ natural = 1.0 / 10.0
83
+ result = engine.check_resonance(natural)
84
+ expect(result).to include(p.id)
85
+ end
86
+
87
+ it 'returns empty array when no resonance' do
88
+ engine.create_pendulum(pole_pair: :analysis_intuition, period: 10.0)
89
+ expect(engine.check_resonance(99.0)).to be_empty
90
+ end
91
+
92
+ it 'returns empty array for zero or negative frequency' do
93
+ engine.create_pendulum(pole_pair: :analysis_intuition, period: 10.0)
94
+ expect(engine.check_resonance(0.0)).to be_empty
95
+ end
96
+ end
97
+
98
+ describe '#dominant_pole' do
99
+ let!(:pendulum) { engine.create_pendulum(pole_pair: :certainty_doubt) }
100
+
101
+ it 'returns :neutral at center' do
102
+ expect(engine.dominant_pole(pendulum.id)).to eq(:neutral)
103
+ end
104
+
105
+ it 'returns the correct pole after swinging' do
106
+ engine.swing(pendulum.id, force: 0.9)
107
+ expect(engine.dominant_pole(pendulum.id)).to eq(:doubt)
108
+ end
109
+
110
+ it 'returns nil for unknown id' do
111
+ expect(engine.dominant_pole('missing-id')).to be_nil
112
+ end
113
+ end
114
+
115
+ describe '#most_active' do
116
+ before do
117
+ engine.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.9)
118
+ engine.create_pendulum(pole_pair: :focus_diffusion, amplitude: 0.3)
119
+ engine.create_pendulum(pole_pair: :analysis_intuition, amplitude: 0.6)
120
+ end
121
+
122
+ it 'returns pendulums sorted by amplitude descending' do
123
+ result = engine.most_active(limit: 3)
124
+ amplitudes = result.map(&:amplitude)
125
+ expect(amplitudes).to eq(amplitudes.sort.reverse)
126
+ end
127
+
128
+ it 'respects limit' do
129
+ expect(engine.most_active(limit: 2).size).to eq(2)
130
+ end
131
+
132
+ it 'returns all if limit exceeds count' do
133
+ expect(engine.most_active(limit: 10).size).to eq(3)
134
+ end
135
+ end
136
+
137
+ describe '#most_damped' do
138
+ before do
139
+ engine.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.1)
140
+ engine.create_pendulum(pole_pair: :focus_diffusion, amplitude: 0.8)
141
+ engine.create_pendulum(pole_pair: :analysis_intuition, amplitude: 0.4)
142
+ end
143
+
144
+ it 'returns pendulums sorted by amplitude ascending' do
145
+ result = engine.most_damped(limit: 3)
146
+ amplitudes = result.map(&:amplitude)
147
+ expect(amplitudes).to eq(amplitudes.sort)
148
+ end
149
+
150
+ it 'respects limit' do
151
+ expect(engine.most_damped(limit: 1).size).to eq(1)
152
+ end
153
+ end
154
+
155
+ describe '#pendulum_report' do
156
+ before do
157
+ engine.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.8)
158
+ engine.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.5)
159
+ engine.create_pendulum(pole_pair: :focus_diffusion, amplitude: 0.3)
160
+ end
161
+
162
+ it 'includes total count' do
163
+ expect(engine.pendulum_report[:total]).to eq(3)
164
+ end
165
+
166
+ it 'includes max constant' do
167
+ expect(engine.pendulum_report[:max]).to eq(Legion::Extensions::CognitivePendulum::Helpers::Constants::MAX_PENDULUMS)
168
+ end
169
+
170
+ it 'groups pendulums by pole_pair' do
171
+ report = engine.pendulum_report
172
+ expect(report[:pole_pairs][:certainty_doubt]).to eq(2)
173
+ expect(report[:pole_pairs][:focus_diffusion]).to eq(1)
174
+ end
175
+
176
+ it 'includes most_active as hashes' do
177
+ report = engine.pendulum_report
178
+ expect(report[:most_active]).to be_an(Array)
179
+ expect(report[:most_active].first).to be_a(Hash)
180
+ end
181
+
182
+ it 'includes most_damped as hashes' do
183
+ report = engine.pendulum_report
184
+ expect(report[:most_damped]).to be_an(Array)
185
+ end
186
+ end
187
+
188
+ describe '#get' do
189
+ it 'returns the pendulum by id' do
190
+ p = engine.create_pendulum(pole_pair: :approach_avoidance)
191
+ expect(engine.get(p.id)).to eq(p)
192
+ end
193
+
194
+ it 'returns nil for unknown id' do
195
+ expect(engine.get('not-here')).to be_nil
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitivePendulum::Helpers::Pendulum do
4
+ subject(:pendulum) { described_class.new(pole_pair: :certainty_doubt) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(pendulum.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'stores the pole_pair' do
12
+ expect(pendulum.pole_pair).to eq(:certainty_doubt)
13
+ end
14
+
15
+ it 'defaults amplitude to 0.5' do
16
+ expect(pendulum.amplitude).to eq(0.5)
17
+ end
18
+
19
+ it 'defaults period to 10.0' do
20
+ expect(pendulum.period).to eq(10.0)
21
+ end
22
+
23
+ it 'defaults damping to DAMPING_RATE' do
24
+ expect(pendulum.damping).to eq(Legion::Extensions::CognitivePendulum::Helpers::Constants::DAMPING_RATE)
25
+ end
26
+
27
+ it 'initializes current_position to 0.0' do
28
+ expect(pendulum.current_position).to eq(0.0)
29
+ end
30
+
31
+ it 'initializes swings to 0' do
32
+ expect(pendulum.swings).to eq(0)
33
+ end
34
+
35
+ it 'sets created_at' do
36
+ expect(pendulum.created_at).to be_a(Time)
37
+ end
38
+
39
+ it 'raises for invalid pole_pair' do
40
+ expect { described_class.new(pole_pair: :nonsense) }.to raise_error(ArgumentError)
41
+ end
42
+
43
+ it 'raises when amplitude out of range' do
44
+ expect { described_class.new(pole_pair: :certainty_doubt, amplitude: 1.5) }.to raise_error(ArgumentError)
45
+ end
46
+
47
+ it 'raises when period is zero or negative' do
48
+ expect { described_class.new(pole_pair: :certainty_doubt, period: 0.0) }.to raise_error(ArgumentError)
49
+ end
50
+
51
+ it 'raises when damping is negative' do
52
+ expect { described_class.new(pole_pair: :certainty_doubt, damping: -0.1) }.to raise_error(ArgumentError)
53
+ end
54
+
55
+ it 'accepts custom amplitude' do
56
+ p = described_class.new(pole_pair: :focus_diffusion, amplitude: 0.8)
57
+ expect(p.amplitude).to eq(0.8)
58
+ end
59
+
60
+ it 'accepts custom period' do
61
+ p = described_class.new(pole_pair: :focus_diffusion, period: 30.0)
62
+ expect(p.period).to eq(30.0)
63
+ end
64
+
65
+ it 'accepts zero damping' do
66
+ p = described_class.new(pole_pair: :analysis_intuition, damping: 0.0)
67
+ expect(p.damping).to eq(0.0)
68
+ end
69
+ end
70
+
71
+ describe '#swing!' do
72
+ it 'moves position toward positive with positive force' do
73
+ pendulum.swing!(force: 0.5)
74
+ expect(pendulum.current_position).to be > 0.0
75
+ end
76
+
77
+ it 'moves position toward negative with negative force' do
78
+ pendulum.swing!(force: -0.5)
79
+ expect(pendulum.current_position).to be < 0.0
80
+ end
81
+
82
+ it 'clamps position to 1.0 maximum' do
83
+ 5.times { pendulum.swing!(force: 1.0) }
84
+ expect(pendulum.current_position).to eq(1.0)
85
+ end
86
+
87
+ it 'clamps position to -1.0 minimum' do
88
+ 5.times { pendulum.swing!(force: -1.0) }
89
+ expect(pendulum.current_position).to eq(-1.0)
90
+ end
91
+
92
+ it 'clamps force to valid range' do
93
+ pendulum.swing!(force: 99.0)
94
+ expect(pendulum.current_position).to be <= 1.0
95
+ end
96
+
97
+ it 'increments swing count' do
98
+ pendulum.swing!(force: 0.3)
99
+ expect(pendulum.swings).to eq(1)
100
+ end
101
+
102
+ it 'returns the new position' do
103
+ result = pendulum.swing!(force: 0.4)
104
+ expect(result).to eq(pendulum.current_position)
105
+ end
106
+ end
107
+
108
+ describe '#damp!' do
109
+ it 'reduces amplitude' do
110
+ original = pendulum.amplitude
111
+ pendulum.damp!
112
+ expect(pendulum.amplitude).to be < original
113
+ end
114
+
115
+ it 'reduces current_position magnitude' do
116
+ pendulum.swing!(force: 0.8)
117
+ original = pendulum.current_position.abs
118
+ pendulum.damp!
119
+ expect(pendulum.current_position.abs).to be < original
120
+ end
121
+
122
+ it 'returns the new amplitude' do
123
+ result = pendulum.damp!
124
+ expect(result).to eq(pendulum.amplitude)
125
+ end
126
+
127
+ it 'never goes below 0' do
128
+ 100.times { pendulum.damp! }
129
+ expect(pendulum.amplitude).to be >= 0.0
130
+ end
131
+ end
132
+
133
+ describe '#position_at' do
134
+ let(:p) { described_class.new(pole_pair: :focus_diffusion, amplitude: 1.0, period: 10.0, damping: 0.0) }
135
+
136
+ it 'returns a float' do
137
+ expect(p.position_at(0.0)).to be_a(Float)
138
+ end
139
+
140
+ it 'returns amplitude at time 0 (cos(0) = 1)' do
141
+ expect(p.position_at(0.0)).to eq(1.0)
142
+ end
143
+
144
+ it 'returns a value within [-1.0, 1.0]' do
145
+ expect(p.position_at(5.0)).to be_between(-1.0, 1.0)
146
+ end
147
+
148
+ it 'decays with positive damping' do
149
+ damped = described_class.new(pole_pair: :focus_diffusion, amplitude: 1.0, period: 10.0, damping: 0.1)
150
+ expect(damped.position_at(10.0).abs).to be < p.position_at(10.0).abs
151
+ end
152
+ end
153
+
154
+ describe '#at_pole_a?' do
155
+ it 'is false at neutral position' do
156
+ expect(pendulum.at_pole_a?).to be false
157
+ end
158
+
159
+ it 'is true when position <= -0.5' do
160
+ 3.times { pendulum.swing!(force: -1.0) }
161
+ expect(pendulum.at_pole_a?).to be true
162
+ end
163
+ end
164
+
165
+ describe '#at_pole_b?' do
166
+ it 'is false at neutral position' do
167
+ expect(pendulum.at_pole_b?).to be false
168
+ end
169
+
170
+ it 'is true when position >= 0.5' do
171
+ 3.times { pendulum.swing!(force: 1.0) }
172
+ expect(pendulum.at_pole_b?).to be true
173
+ end
174
+ end
175
+
176
+ describe '#amplitude_label' do
177
+ it 'returns :moderate for default amplitude' do
178
+ expect(pendulum.amplitude_label).to eq(:moderate)
179
+ end
180
+
181
+ it 'returns :minimal for low amplitude' do
182
+ p = described_class.new(pole_pair: :certainty_doubt, amplitude: 0.1)
183
+ expect(p.amplitude_label).to eq(:minimal)
184
+ end
185
+
186
+ it 'returns :maximal for high amplitude' do
187
+ p = described_class.new(pole_pair: :certainty_doubt, amplitude: 0.9)
188
+ expect(p.amplitude_label).to eq(:maximal)
189
+ end
190
+ end
191
+
192
+ describe '#resonant_with?' do
193
+ let(:p) { described_class.new(pole_pair: :analysis_intuition, period: 10.0) }
194
+
195
+ it 'returns true when frequency matches natural frequency' do
196
+ natural = 1.0 / 10.0
197
+ expect(p.resonant_with?(natural)).to be true
198
+ end
199
+
200
+ it 'returns false when frequency is far off' do
201
+ expect(p.resonant_with?(0.5)).to be false
202
+ end
203
+
204
+ it 'returns false for zero or negative frequency' do
205
+ expect(p.resonant_with?(0.0)).to be false
206
+ end
207
+
208
+ it 'returns true within 5% tolerance' do
209
+ natural = 1.0 / 10.0
210
+ expect(p.resonant_with?(natural * 1.04)).to be true
211
+ end
212
+ end
213
+
214
+ describe '#dominant_pole' do
215
+ it 'returns :neutral near center' do
216
+ expect(pendulum.dominant_pole).to eq(:neutral)
217
+ end
218
+
219
+ it 'returns pole_a when position is negative' do
220
+ 3.times { pendulum.swing!(force: -1.0) }
221
+ expect(pendulum.dominant_pole).to eq(:certainty)
222
+ end
223
+
224
+ it 'returns pole_b when position is positive' do
225
+ 3.times { pendulum.swing!(force: 1.0) }
226
+ expect(pendulum.dominant_pole).to eq(:doubt)
227
+ end
228
+ end
229
+
230
+ describe '#to_h' do
231
+ it 'includes all expected keys' do
232
+ h = pendulum.to_h
233
+ %i[id pole_pair pole_a pole_b amplitude amplitude_label period damping
234
+ current_position dominant_pole at_pole_a at_pole_b swings created_at].each do |key|
235
+ expect(h).to have_key(key)
236
+ end
237
+ end
238
+
239
+ it 'pole_a is :certainty for certainty_doubt pair' do
240
+ expect(pendulum.to_h[:pole_a]).to eq(:certainty)
241
+ end
242
+
243
+ it 'pole_b is :doubt for certainty_doubt pair' do
244
+ expect(pendulum.to_h[:pole_b]).to eq(:doubt)
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_pendulum/client'
4
+
5
+ RSpec.describe Legion::Extensions::CognitivePendulum::Runners::CognitivePendulum do
6
+ let(:client) { Legion::Extensions::CognitivePendulum::Client.new }
7
+
8
+ describe '#create_pendulum' do
9
+ it 'returns success with valid pole_pair' do
10
+ result = client.create_pendulum(pole_pair: :certainty_doubt)
11
+ expect(result[:success]).to be true
12
+ end
13
+
14
+ it 'returns a pendulum_id uuid' do
15
+ result = client.create_pendulum(pole_pair: :focus_diffusion)
16
+ expect(result[:pendulum_id]).to match(/\A[0-9a-f-]{36}\z/)
17
+ end
18
+
19
+ it 'echoes the pole_pair' do
20
+ result = client.create_pendulum(pole_pair: :analysis_intuition)
21
+ expect(result[:pole_pair]).to eq(:analysis_intuition)
22
+ end
23
+
24
+ it 'returns the amplitude' do
25
+ result = client.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.7)
26
+ expect(result[:amplitude]).to eq(0.7)
27
+ end
28
+
29
+ it 'returns error for invalid pole_pair' do
30
+ result = client.create_pendulum(pole_pair: :bogus)
31
+ expect(result[:success]).to be false
32
+ expect(result[:error]).to eq(:invalid_pole_pair)
33
+ end
34
+
35
+ it 'includes valid_pairs in error response' do
36
+ result = client.create_pendulum(pole_pair: :bogus)
37
+ expect(result[:valid_pairs]).to be_an(Array)
38
+ end
39
+
40
+ it 'returns argument_error for invalid amplitude' do
41
+ result = client.create_pendulum(pole_pair: :certainty_doubt, amplitude: 5.0)
42
+ expect(result[:success]).to be false
43
+ expect(result[:error]).to eq(:argument_error)
44
+ end
45
+
46
+ it 'returns argument_error for non-positive period' do
47
+ result = client.create_pendulum(pole_pair: :certainty_doubt, period: -1.0)
48
+ expect(result[:success]).to be false
49
+ end
50
+
51
+ it 'accepts all five pole pairs' do
52
+ %i[certainty_doubt focus_diffusion analysis_intuition approach_avoidance convergent_divergent].each do |pair|
53
+ result = client.create_pendulum(pole_pair: pair)
54
+ expect(result[:success]).to be true
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '#swing' do
60
+ let(:pendulum_id) { client.create_pendulum(pole_pair: :certainty_doubt)[:pendulum_id] }
61
+
62
+ it 'returns success for valid pendulum' do
63
+ result = client.swing(pendulum_id: pendulum_id, force: 0.5)
64
+ expect(result[:success]).to be true
65
+ end
66
+
67
+ it 'returns the current_position' do
68
+ result = client.swing(pendulum_id: pendulum_id, force: 0.5)
69
+ expect(result[:current_position]).to eq(0.5)
70
+ end
71
+
72
+ it 'returns the dominant_pole' do
73
+ result = client.swing(pendulum_id: pendulum_id, force: 0.8)
74
+ expect(result).to have_key(:dominant_pole)
75
+ end
76
+
77
+ it 'returns not_found for unknown id' do
78
+ result = client.swing(pendulum_id: 'no-such-id', force: 0.1)
79
+ expect(result[:success]).to be false
80
+ expect(result[:error]).to eq(:not_found)
81
+ end
82
+
83
+ it 'swings multiple times and accumulates position' do
84
+ client.swing(pendulum_id: pendulum_id, force: 0.3)
85
+ result = client.swing(pendulum_id: pendulum_id, force: 0.3)
86
+ expect(result[:current_position]).to be_within(0.001).of(0.6)
87
+ end
88
+ end
89
+
90
+ describe '#damp_all' do
91
+ it 'returns success' do
92
+ result = client.damp_all
93
+ expect(result[:success]).to be true
94
+ end
95
+
96
+ it 'reports number of damped pendulums' do
97
+ client.create_pendulum(pole_pair: :focus_diffusion)
98
+ client.create_pendulum(pole_pair: :analysis_intuition)
99
+ result = client.damp_all
100
+ expect(result[:damped]).to eq(2)
101
+ end
102
+
103
+ it 'reports zero when no pendulums exist' do
104
+ result = client.damp_all
105
+ expect(result[:damped]).to eq(0)
106
+ end
107
+ end
108
+
109
+ describe '#check_resonance' do
110
+ before { client.create_pendulum(pole_pair: :analysis_intuition, period: 10.0) }
111
+
112
+ it 'returns success' do
113
+ result = client.check_resonance(frequency: 0.1)
114
+ expect(result[:success]).to be true
115
+ end
116
+
117
+ it 'detects resonance at natural frequency' do
118
+ result = client.check_resonance(frequency: 0.1)
119
+ expect(result[:count]).to eq(1)
120
+ end
121
+
122
+ it 'returns empty when no resonance' do
123
+ result = client.check_resonance(frequency: 99.0)
124
+ expect(result[:count]).to eq(0)
125
+ end
126
+
127
+ it 'returns error for non-positive frequency' do
128
+ result = client.check_resonance(frequency: 0.0)
129
+ expect(result[:success]).to be false
130
+ expect(result[:error]).to eq(:invalid_frequency)
131
+ end
132
+
133
+ it 'returns resonant_pendulum_ids array' do
134
+ result = client.check_resonance(frequency: 0.1)
135
+ expect(result[:resonant_pendulum_ids]).to be_an(Array)
136
+ end
137
+
138
+ it 'echoes the frequency' do
139
+ result = client.check_resonance(frequency: 0.1)
140
+ expect(result[:frequency]).to eq(0.1)
141
+ end
142
+ end
143
+
144
+ describe '#get_dominant_pole' do
145
+ let(:pendulum_id) { client.create_pendulum(pole_pair: :certainty_doubt)[:pendulum_id] }
146
+
147
+ it 'returns success for known pendulum' do
148
+ result = client.get_dominant_pole(pendulum_id: pendulum_id)
149
+ expect(result[:success]).to be true
150
+ end
151
+
152
+ it 'returns :neutral for centered pendulum' do
153
+ result = client.get_dominant_pole(pendulum_id: pendulum_id)
154
+ expect(result[:dominant_pole]).to eq(:neutral)
155
+ end
156
+
157
+ it 'returns the active pole after swinging' do
158
+ client.swing(pendulum_id: pendulum_id, force: 0.8)
159
+ result = client.get_dominant_pole(pendulum_id: pendulum_id)
160
+ expect(result[:dominant_pole]).to eq(:doubt)
161
+ end
162
+
163
+ it 'returns not_found for unknown id' do
164
+ result = client.get_dominant_pole(pendulum_id: 'ghost')
165
+ expect(result[:success]).to be false
166
+ expect(result[:error]).to eq(:not_found)
167
+ end
168
+ end
169
+
170
+ describe '#most_active' do
171
+ before do
172
+ client.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.9)
173
+ client.create_pendulum(pole_pair: :focus_diffusion, amplitude: 0.2)
174
+ client.create_pendulum(pole_pair: :analysis_intuition, amplitude: 0.6)
175
+ end
176
+
177
+ it 'returns success' do
178
+ expect(client.most_active[:success]).to be true
179
+ end
180
+
181
+ it 'returns pendulums sorted by descending amplitude' do
182
+ result = client.most_active(limit: 3)
183
+ amplitudes = result[:pendulums].map { |p| p[:amplitude] }
184
+ expect(amplitudes).to eq(amplitudes.sort.reverse)
185
+ end
186
+
187
+ it 'respects limit' do
188
+ result = client.most_active(limit: 2)
189
+ expect(result[:count]).to eq(2)
190
+ end
191
+ end
192
+
193
+ describe '#most_damped' do
194
+ before do
195
+ client.create_pendulum(pole_pair: :certainty_doubt, amplitude: 0.1)
196
+ client.create_pendulum(pole_pair: :focus_diffusion, amplitude: 0.7)
197
+ end
198
+
199
+ it 'returns success' do
200
+ expect(client.most_damped[:success]).to be true
201
+ end
202
+
203
+ it 'returns pendulums sorted by ascending amplitude' do
204
+ result = client.most_damped(limit: 2)
205
+ amplitudes = result[:pendulums].map { |p| p[:amplitude] }
206
+ expect(amplitudes).to eq(amplitudes.sort)
207
+ end
208
+ end
209
+
210
+ describe '#pendulum_report' do
211
+ before do
212
+ client.create_pendulum(pole_pair: :certainty_doubt)
213
+ client.create_pendulum(pole_pair: :focus_diffusion)
214
+ end
215
+
216
+ it 'returns success' do
217
+ expect(client.pendulum_report[:success]).to be true
218
+ end
219
+
220
+ it 'includes a report hash' do
221
+ result = client.pendulum_report
222
+ expect(result[:report]).to be_a(Hash)
223
+ end
224
+
225
+ it 'report has total of 2' do
226
+ expect(client.pendulum_report[:report][:total]).to eq(2)
227
+ end
228
+ end
229
+
230
+ describe '#get_pendulum' do
231
+ let(:pendulum_id) { client.create_pendulum(pole_pair: :approach_avoidance)[:pendulum_id] }
232
+
233
+ it 'returns success with the pendulum hash' do
234
+ result = client.get_pendulum(pendulum_id: pendulum_id)
235
+ expect(result[:success]).to be true
236
+ expect(result[:pendulum]).to be_a(Hash)
237
+ end
238
+
239
+ it 'returns not_found for unknown id' do
240
+ result = client.get_pendulum(pendulum_id: 'fake-id')
241
+ expect(result[:success]).to be false
242
+ expect(result[:error]).to eq(:not_found)
243
+ end
244
+
245
+ it 'pendulum hash includes pole_pair' do
246
+ result = client.get_pendulum(pendulum_id: pendulum_id)
247
+ expect(result[:pendulum][:pole_pair]).to eq(:approach_avoidance)
248
+ end
249
+ end
250
+ end