lex-cognitive-quicksilver 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,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveQuicksilver::Helpers::Droplet do
4
+ let(:droplet) { described_class.new(form: :droplet, content: 'test idea') }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(droplet.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'sets form' do
12
+ expect(droplet.form).to eq(:droplet)
13
+ end
14
+
15
+ it 'sets content' do
16
+ expect(droplet.content).to eq('test idea')
17
+ end
18
+
19
+ it 'defaults mass to 0.3' do
20
+ expect(droplet.mass).to eq(0.3)
21
+ end
22
+
23
+ it 'defaults fluidity to FLUIDITY_BASE' do
24
+ expect(droplet.fluidity).to eq(Legion::Extensions::CognitiveQuicksilver::Helpers::Constants::FLUIDITY_BASE)
25
+ end
26
+
27
+ it 'defaults surface to :glass' do
28
+ expect(droplet.surface).to eq(:glass)
29
+ end
30
+
31
+ it 'defaults captured to false' do
32
+ expect(droplet.captured).to be false
33
+ end
34
+
35
+ it 'clamps mass above 1.0' do
36
+ d = described_class.new(form: :liquid, content: 'x', mass: 1.5)
37
+ expect(d.mass).to eq(1.0)
38
+ end
39
+
40
+ it 'clamps mass below 0.0' do
41
+ d = described_class.new(form: :liquid, content: 'x', mass: -0.5)
42
+ expect(d.mass).to eq(0.0)
43
+ end
44
+
45
+ it 'raises ArgumentError for invalid form' do
46
+ expect { described_class.new(form: :vapor, content: 'x') }.to raise_error(ArgumentError, /invalid form/)
47
+ end
48
+
49
+ it 'raises ArgumentError for invalid surface' do
50
+ expect do
51
+ described_class.new(form: :droplet, content: 'x', surface: :air)
52
+ end.to raise_error(ArgumentError, /invalid surface/)
53
+ end
54
+
55
+ it 'sets created_at' do
56
+ expect(droplet.created_at).to be_a(Time)
57
+ end
58
+ end
59
+
60
+ describe '#shift_form!' do
61
+ it 'changes the form' do
62
+ droplet.shift_form!(:liquid)
63
+ expect(droplet.form).to eq(:liquid)
64
+ end
65
+
66
+ it 'adjusts fluidity based on form' do
67
+ droplet.shift_form!(:liquid)
68
+ expect(droplet.fluidity).to eq(0.9)
69
+ end
70
+
71
+ it 'sets low fluidity for pool form' do
72
+ droplet.shift_form!(:pool)
73
+ expect(droplet.fluidity).to eq(0.3)
74
+ end
75
+
76
+ it 'returns self for chaining' do
77
+ expect(droplet.shift_form!(:bead)).to be(droplet)
78
+ end
79
+
80
+ it 'raises ArgumentError for invalid form' do
81
+ expect { droplet.shift_form!(:fog) }.to raise_error(ArgumentError, /invalid form/)
82
+ end
83
+ end
84
+
85
+ describe '#merge!' do
86
+ let(:other) { described_class.new(form: :liquid, content: 'other', mass: 0.4) }
87
+
88
+ it 'combines mass with coalescence bonus' do
89
+ bonus = Legion::Extensions::CognitiveQuicksilver::Helpers::Constants::COALESCENCE_BONUS
90
+ original_mass = droplet.mass
91
+ droplet.merge!(other)
92
+ expected = [original_mass + other.mass + bonus, 1.0].min
93
+ expect(droplet.mass).to be_within(0.001).of(expected)
94
+ end
95
+
96
+ it 'clamps merged mass to 1.0' do
97
+ heavy_a = described_class.new(form: :pool, content: 'a', mass: 0.7)
98
+ heavy_b = described_class.new(form: :pool, content: 'b', mass: 0.7)
99
+ heavy_a.merge!(heavy_b)
100
+ expect(heavy_a.mass).to eq(1.0)
101
+ end
102
+
103
+ it 'returns self for chaining' do
104
+ expect(droplet.merge!(other)).to be(droplet)
105
+ end
106
+
107
+ it 'averages fluidity' do
108
+ original_fluidity = droplet.fluidity
109
+ expected_fluidity = (original_fluidity + other.fluidity) / 2.0
110
+ droplet.merge!(other)
111
+ expect(droplet.fluidity).to be_within(0.001).of(expected_fluidity)
112
+ end
113
+ end
114
+
115
+ describe '#split!' do
116
+ context 'when mass is sufficient (> 0.2)' do
117
+ let(:heavy_droplet) { described_class.new(form: :droplet, content: 'big idea', mass: 0.6) }
118
+
119
+ it 'returns a twin droplet' do
120
+ twin = heavy_droplet.split!
121
+ expect(twin).to be_a(described_class)
122
+ end
123
+
124
+ it 'halves the original mass' do
125
+ heavy_droplet.split!
126
+ expect(heavy_droplet.mass).to be_within(0.001).of(0.3)
127
+ end
128
+
129
+ it 'twin has half the original mass' do
130
+ original_mass = heavy_droplet.mass
131
+ twin = heavy_droplet.split!
132
+ expect(twin.mass).to be_within(0.001).of(original_mass / 2.0)
133
+ end
134
+
135
+ it 'twin has a different id' do
136
+ twin = heavy_droplet.split!
137
+ expect(twin.id).not_to eq(heavy_droplet.id)
138
+ end
139
+
140
+ it 'twin inherits form and surface' do
141
+ twin = heavy_droplet.split!
142
+ expect(twin.form).to eq(heavy_droplet.form)
143
+ expect(twin.surface).to eq(heavy_droplet.surface)
144
+ end
145
+ end
146
+
147
+ context 'when mass is too small (<= 0.2)' do
148
+ let(:tiny_droplet) { described_class.new(form: :bead, content: 'tiny', mass: 0.15) }
149
+
150
+ it 'returns nil' do
151
+ expect(tiny_droplet.split!).to be_nil
152
+ end
153
+
154
+ it 'does not change the mass' do
155
+ original = tiny_droplet.mass
156
+ tiny_droplet.split!
157
+ expect(tiny_droplet.mass).to eq(original)
158
+ end
159
+ end
160
+ end
161
+
162
+ describe '#capture!' do
163
+ it 'sets captured to true' do
164
+ droplet.capture!
165
+ expect(droplet.captured).to be true
166
+ end
167
+
168
+ it 'halves the fluidity' do
169
+ original_fluidity = droplet.fluidity
170
+ droplet.capture!
171
+ expect(droplet.fluidity).to be_within(0.001).of(original_fluidity / 2.0)
172
+ end
173
+
174
+ it 'returns self' do
175
+ expect(droplet.capture!).to be(droplet)
176
+ end
177
+ end
178
+
179
+ describe '#release!' do
180
+ before { droplet.capture! }
181
+
182
+ it 'sets captured to false' do
183
+ droplet.release!
184
+ expect(droplet.captured).to be false
185
+ end
186
+
187
+ it 'restores fluidity (doubles it, clamped)' do
188
+ fluidity_after_capture = droplet.fluidity
189
+ droplet.release!
190
+ expect(droplet.fluidity).to be_within(0.001).of([fluidity_after_capture * 2.0, 1.0].min)
191
+ end
192
+
193
+ it 'returns self' do
194
+ expect(droplet.release!).to be(droplet)
195
+ end
196
+ end
197
+
198
+ describe '#evaporate!' do
199
+ it 'reduces mass by EVAPORATION_RATE' do
200
+ original_mass = droplet.mass
201
+ droplet.evaporate!
202
+ expected = original_mass - Legion::Extensions::CognitiveQuicksilver::Helpers::Constants::EVAPORATION_RATE
203
+ expect(droplet.mass).to be_within(0.001).of(expected)
204
+ end
205
+
206
+ it 'does not go below 0.0' do
207
+ tiny = described_class.new(form: :bead, content: 'x', mass: 0.01)
208
+ tiny.evaporate!
209
+ expect(tiny.mass).to be >= 0.0
210
+ end
211
+
212
+ it 'returns self' do
213
+ expect(droplet.evaporate!).to be(droplet)
214
+ end
215
+ end
216
+
217
+ describe '#elusive?' do
218
+ it 'returns true when fluidity >= 0.7 and not captured' do
219
+ d = described_class.new(form: :liquid, content: 'elusive', fluidity: 0.8)
220
+ expect(d.elusive?).to be true
221
+ end
222
+
223
+ it 'returns false when captured' do
224
+ droplet.capture!
225
+ expect(droplet.elusive?).to be false
226
+ end
227
+
228
+ it 'returns false when fluidity < 0.7' do
229
+ d = described_class.new(form: :pool, content: 'slow', fluidity: 0.3)
230
+ expect(d.elusive?).to be false
231
+ end
232
+ end
233
+
234
+ describe '#stable?' do
235
+ it 'returns true when captured' do
236
+ droplet.capture!
237
+ expect(droplet.stable?).to be true
238
+ end
239
+
240
+ it 'returns true when fluidity < 0.4' do
241
+ d = described_class.new(form: :pool, content: 'stable', fluidity: 0.2)
242
+ expect(d.stable?).to be true
243
+ end
244
+
245
+ it 'returns false for default high-fluidity uncaptured droplet' do
246
+ d = described_class.new(form: :liquid, content: 'free', fluidity: 0.8)
247
+ expect(d.stable?).to be false
248
+ end
249
+ end
250
+
251
+ describe '#vanishing?' do
252
+ it 'returns true when mass < 0.1' do
253
+ d = described_class.new(form: :bead, content: 'fading', mass: 0.05)
254
+ expect(d.vanishing?).to be true
255
+ end
256
+
257
+ it 'returns false for normal mass' do
258
+ expect(droplet.vanishing?).to be false
259
+ end
260
+ end
261
+
262
+ describe '#cohesion_label' do
263
+ it 'returns a symbol' do
264
+ expect(droplet.cohesion_label).to be_a(Symbol)
265
+ end
266
+
267
+ it 'returns :unified for mass 1.0' do
268
+ d = described_class.new(form: :pool, content: 'x', mass: 1.0)
269
+ expect(d.cohesion_label).to eq(:unified)
270
+ end
271
+
272
+ it 'returns :atomized for mass near 0.0' do
273
+ d = described_class.new(form: :bead, content: 'x', mass: 0.05)
274
+ expect(d.cohesion_label).to eq(:atomized)
275
+ end
276
+ end
277
+
278
+ describe '#fluidity_label' do
279
+ it 'returns a symbol' do
280
+ expect(droplet.fluidity_label).to be_a(Symbol)
281
+ end
282
+
283
+ it 'returns :liquid for fluidity 0.9' do
284
+ d = described_class.new(form: :liquid, content: 'x', fluidity: 0.9)
285
+ expect(d.fluidity_label).to eq(:liquid)
286
+ end
287
+
288
+ it 'returns :solid for fluidity 0.1' do
289
+ d = described_class.new(form: :bead, content: 'x', fluidity: 0.1)
290
+ expect(d.fluidity_label).to eq(:solid)
291
+ end
292
+ end
293
+
294
+ describe '#to_h' do
295
+ it 'returns a hash with all expected keys' do
296
+ h = droplet.to_h
297
+ expect(h.keys).to include(:id, :form, :content, :mass, :fluidity, :surface, :captured,
298
+ :elusive, :stable, :vanishing, :cohesion, :fluidity_lbl, :created_at)
299
+ end
300
+
301
+ it 'id matches droplet id' do
302
+ expect(droplet.to_h[:id]).to eq(droplet.id)
303
+ end
304
+
305
+ it 'reflects captured state' do
306
+ droplet.capture!
307
+ expect(droplet.to_h[:captured]).to be true
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveQuicksilver::Helpers::Pool do
4
+ let(:pool) { described_class.new(surface_type: :glass) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(pool.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'sets surface_type' do
12
+ expect(pool.surface_type).to eq(:glass)
13
+ end
14
+
15
+ it 'defaults depth to 0.5' do
16
+ expect(pool.depth).to eq(0.5)
17
+ end
18
+
19
+ it 'starts with empty droplet_ids' do
20
+ expect(pool.droplet_ids).to be_empty
21
+ end
22
+
23
+ it 'defaults surface_tension to SURFACE_TENSION' do
24
+ expect(pool.surface_tension).to eq(Legion::Extensions::CognitiveQuicksilver::Helpers::Constants::SURFACE_TENSION)
25
+ end
26
+
27
+ it 'sets created_at' do
28
+ expect(pool.created_at).to be_a(Time)
29
+ end
30
+
31
+ it 'raises ArgumentError for invalid surface_type' do
32
+ expect { described_class.new(surface_type: :vapor) }.to raise_error(ArgumentError, /invalid surface_type/)
33
+ end
34
+
35
+ it 'clamps depth at 1.0' do
36
+ p = described_class.new(surface_type: :metal, depth: 1.5)
37
+ expect(p.depth).to eq(1.0)
38
+ end
39
+ end
40
+
41
+ describe '#add_droplet' do
42
+ it 'adds a droplet id' do
43
+ pool.add_droplet('abc-123')
44
+ expect(pool.droplet_ids).to include('abc-123')
45
+ end
46
+
47
+ it 'increases depth' do
48
+ original = pool.depth
49
+ pool.add_droplet('abc-123')
50
+ expect(pool.depth).to be > original
51
+ end
52
+
53
+ it 'does not duplicate ids' do
54
+ pool.add_droplet('dup-id')
55
+ pool.add_droplet('dup-id')
56
+ expect(pool.droplet_ids.count('dup-id')).to eq(1)
57
+ end
58
+
59
+ it 'returns self' do
60
+ expect(pool.add_droplet('xyz')).to be(pool)
61
+ end
62
+ end
63
+
64
+ describe '#remove_droplet' do
65
+ before { pool.add_droplet('remove-me') }
66
+
67
+ it 'removes the droplet id' do
68
+ pool.remove_droplet('remove-me')
69
+ expect(pool.droplet_ids).not_to include('remove-me')
70
+ end
71
+
72
+ it 'decreases depth' do
73
+ depth_before = pool.depth
74
+ pool.remove_droplet('remove-me')
75
+ expect(pool.depth).to be < depth_before
76
+ end
77
+
78
+ it 'does not raise when removing non-existent id' do
79
+ expect { pool.remove_droplet('ghost-id') }.not_to raise_error
80
+ end
81
+ end
82
+
83
+ describe '#agitate!' do
84
+ before do
85
+ 5.times { |i| pool.add_droplet("droplet-#{i}") }
86
+ # Set low surface tension so more droplets are likely to be released
87
+ pool.settle!
88
+ end
89
+
90
+ it 'reduces surface tension' do
91
+ tension_before = pool.surface_tension
92
+ pool.agitate!
93
+ expect(pool.surface_tension).to be < tension_before
94
+ end
95
+
96
+ it 'returns an array' do
97
+ result = pool.agitate!
98
+ expect(result).to be_an(Array)
99
+ end
100
+
101
+ it 'does not raise when pool is empty' do
102
+ empty_pool = described_class.new(surface_type: :stone)
103
+ expect { empty_pool.agitate! }.not_to raise_error
104
+ end
105
+ end
106
+
107
+ describe '#settle!' do
108
+ it 'increases surface tension' do
109
+ pool.agitate! # lower it first
110
+ tension_after_agitate = pool.surface_tension
111
+ pool.settle!
112
+ expect(pool.surface_tension).to be > tension_after_agitate
113
+ end
114
+
115
+ it 'clamps surface tension at 1.0' do
116
+ high_tension_pool = described_class.new(surface_type: :glass, surface_tension: 0.99)
117
+ high_tension_pool.settle!
118
+ expect(high_tension_pool.surface_tension).to eq(1.0)
119
+ end
120
+
121
+ it 'returns self' do
122
+ expect(pool.settle!).to be(pool)
123
+ end
124
+ end
125
+
126
+ describe '#reflective?' do
127
+ it 'returns true when deep and high tension' do
128
+ deep_pool = described_class.new(surface_type: :glass, depth: 0.8, surface_tension: 0.6)
129
+ expect(deep_pool.reflective?).to be true
130
+ end
131
+
132
+ it 'returns false when shallow' do
133
+ shallow = described_class.new(surface_type: :glass, depth: 0.3, surface_tension: 0.8)
134
+ expect(shallow.reflective?).to be false
135
+ end
136
+
137
+ it 'returns false when low tension' do
138
+ tense = described_class.new(surface_type: :glass, depth: 0.9, surface_tension: 0.2)
139
+ expect(tense.reflective?).to be false
140
+ end
141
+ end
142
+
143
+ describe '#shallow?' do
144
+ it 'returns true when depth < 0.2' do
145
+ shallow = described_class.new(surface_type: :wood, depth: 0.1)
146
+ expect(shallow.shallow?).to be true
147
+ end
148
+
149
+ it 'returns false for normal depth' do
150
+ expect(pool.shallow?).to be false
151
+ end
152
+ end
153
+
154
+ describe '#to_h' do
155
+ it 'returns a hash with all expected keys' do
156
+ h = pool.to_h
157
+ expect(h.keys).to include(:id, :surface_type, :depth, :droplet_ids, :droplet_count,
158
+ :surface_tension, :reflective, :shallow, :created_at)
159
+ end
160
+
161
+ it 'reflects droplet_count' do
162
+ pool.add_droplet('d1')
163
+ pool.add_droplet('d2')
164
+ expect(pool.to_h[:droplet_count]).to eq(2)
165
+ end
166
+
167
+ it 'droplet_ids is a copy' do
168
+ pool.add_droplet('original')
169
+ h = pool.to_h
170
+ h[:droplet_ids] << 'injected'
171
+ expect(pool.droplet_ids).not_to include('injected')
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveQuicksilver::Helpers::QuicksilverEngine do
4
+ let(:engine) { described_class.new }
5
+
6
+ describe '#create_droplet' do
7
+ it 'creates and stores a droplet' do
8
+ droplet = engine.create_droplet(form: :liquid, content: 'thought')
9
+ expect(droplet).to be_a(Legion::Extensions::CognitiveQuicksilver::Helpers::Droplet)
10
+ end
11
+
12
+ it 'accepts optional kwargs' do
13
+ droplet = engine.create_droplet(form: :bead, content: 'idea', mass: 0.5, surface: :metal)
14
+ expect(droplet.mass).to eq(0.5)
15
+ expect(droplet.surface).to eq(:metal)
16
+ end
17
+
18
+ it 'raises ArgumentError when limit reached' do
19
+ stub_const('Legion::Extensions::CognitiveQuicksilver::Helpers::Constants::MAX_DROPLETS', 1)
20
+ engine.create_droplet(form: :liquid, content: 'first')
21
+ expect { engine.create_droplet(form: :bead, content: 'second') }.to raise_error(ArgumentError, /limit/)
22
+ end
23
+ end
24
+
25
+ describe '#create_pool' do
26
+ it 'creates and stores a pool' do
27
+ pool = engine.create_pool(surface_type: :glass)
28
+ expect(pool).to be_a(Legion::Extensions::CognitiveQuicksilver::Helpers::Pool)
29
+ end
30
+
31
+ it 'raises ArgumentError when limit reached' do
32
+ stub_const('Legion::Extensions::CognitiveQuicksilver::Helpers::Constants::MAX_POOLS', 1)
33
+ engine.create_pool(surface_type: :glass)
34
+ expect { engine.create_pool(surface_type: :metal) }.to raise_error(ArgumentError, /limit/)
35
+ end
36
+ end
37
+
38
+ describe '#shift_form' do
39
+ let(:droplet) { engine.create_droplet(form: :droplet, content: 'shifting') }
40
+
41
+ it 'changes the droplet form' do
42
+ engine.shift_form(droplet_id: droplet.id, new_form: :liquid)
43
+ expect(droplet.form).to eq(:liquid)
44
+ end
45
+
46
+ it 'raises ArgumentError for unknown droplet' do
47
+ expect { engine.shift_form(droplet_id: 'nope', new_form: :liquid) }.to raise_error(ArgumentError, /not found/)
48
+ end
49
+ end
50
+
51
+ describe '#merge_droplets' do
52
+ let(:a) { engine.create_droplet(form: :droplet, content: 'a', mass: 0.3) }
53
+ let(:b) { engine.create_droplet(form: :liquid, content: 'b', mass: 0.2) }
54
+
55
+ it 'merges b into a' do
56
+ result = engine.merge_droplets(droplet_a_id: a.id, droplet_b_id: b.id)
57
+ expect(result.id).to eq(a.id)
58
+ end
59
+
60
+ it 'removes droplet b from store' do
61
+ b_id = b.id
62
+ engine.merge_droplets(droplet_a_id: a.id, droplet_b_id: b_id)
63
+ expect { engine.shift_form(droplet_id: b_id, new_form: :bead) }.to raise_error(ArgumentError)
64
+ end
65
+
66
+ it 'increases merged mass' do
67
+ original_a_mass = a.mass
68
+ engine.merge_droplets(droplet_a_id: a.id, droplet_b_id: b.id)
69
+ expect(a.mass).to be > original_a_mass
70
+ end
71
+ end
72
+
73
+ describe '#split_droplet' do
74
+ context 'when droplet is large enough' do
75
+ let(:big) { engine.create_droplet(form: :stream, content: 'big', mass: 0.8) }
76
+
77
+ it 'returns an array of two droplets' do
78
+ result = engine.split_droplet(droplet_id: big.id)
79
+ expect(result).to be_an(Array)
80
+ expect(result.length).to eq(2)
81
+ end
82
+
83
+ it 'adds twin to engine' do
84
+ result = engine.split_droplet(droplet_id: big.id)
85
+ twin = result[1]
86
+ expect { engine.shift_form(droplet_id: twin.id, new_form: :bead) }.not_to raise_error
87
+ end
88
+ end
89
+
90
+ context 'when droplet is too small' do
91
+ let(:tiny) { engine.create_droplet(form: :bead, content: 'tiny', mass: 0.1) }
92
+
93
+ it 'returns nil' do
94
+ expect(engine.split_droplet(droplet_id: tiny.id)).to be_nil
95
+ end
96
+ end
97
+
98
+ it 'raises ArgumentError for unknown droplet' do
99
+ expect { engine.split_droplet(droplet_id: 'unknown') }.to raise_error(ArgumentError, /not found/)
100
+ end
101
+ end
102
+
103
+ describe '#capture_droplet' do
104
+ let(:droplet) { engine.create_droplet(form: :liquid, content: 'free') }
105
+
106
+ it 'captures the droplet' do
107
+ engine.capture_droplet(droplet_id: droplet.id)
108
+ expect(droplet.captured).to be true
109
+ end
110
+
111
+ it 'raises for unknown id' do
112
+ expect { engine.capture_droplet(droplet_id: 'ghost') }.to raise_error(ArgumentError)
113
+ end
114
+ end
115
+
116
+ describe '#release_droplet' do
117
+ let(:droplet) { engine.create_droplet(form: :liquid, content: 'trapped') }
118
+ before { engine.capture_droplet(droplet_id: droplet.id) }
119
+
120
+ it 'releases the droplet' do
121
+ engine.release_droplet(droplet_id: droplet.id)
122
+ expect(droplet.captured).to be false
123
+ end
124
+ end
125
+
126
+ describe '#add_to_pool' do
127
+ let(:droplet) { engine.create_droplet(form: :droplet, content: 'pooling') }
128
+ let(:pool) { engine.create_pool(surface_type: :stone) }
129
+
130
+ it 'adds droplet to pool' do
131
+ engine.add_to_pool(droplet_id: droplet.id, pool_id: pool.id)
132
+ expect(pool.droplet_ids).to include(droplet.id)
133
+ end
134
+
135
+ it 'raises for unknown pool' do
136
+ expect { engine.add_to_pool(droplet_id: droplet.id, pool_id: 'ghost') }.to raise_error(ArgumentError)
137
+ end
138
+
139
+ it 'raises for unknown droplet' do
140
+ expect { engine.add_to_pool(droplet_id: 'ghost', pool_id: pool.id) }.to raise_error(ArgumentError)
141
+ end
142
+ end
143
+
144
+ describe '#agitate_pool' do
145
+ let(:pool) { engine.create_pool(surface_type: :fabric) }
146
+
147
+ it 'returns an array of released droplet ids' do
148
+ result = engine.agitate_pool(pool_id: pool.id)
149
+ expect(result).to be_an(Array)
150
+ end
151
+
152
+ it 'raises for unknown pool' do
153
+ expect { engine.agitate_pool(pool_id: 'nope') }.to raise_error(ArgumentError)
154
+ end
155
+ end
156
+
157
+ describe '#evaporate_all!' do
158
+ it 'reduces all droplet masses' do
159
+ d = engine.create_droplet(form: :droplet, content: 'evap', mass: 0.5)
160
+ original = d.mass
161
+ engine.evaporate_all!
162
+ expect(d.mass).to be < original
163
+ end
164
+
165
+ it 'removes vanishing droplets' do
166
+ d = engine.create_droplet(form: :bead, content: 'fading', mass: 0.05)
167
+ d_id = d.id
168
+ engine.evaporate_all!
169
+ expect { engine.capture_droplet(droplet_id: d_id) }.to raise_error(ArgumentError)
170
+ end
171
+
172
+ it 'returns array of removed ids' do
173
+ d = engine.create_droplet(form: :bead, content: 'ghost', mass: 0.05)
174
+ removed = engine.evaporate_all!
175
+ expect(removed).to include(d.id)
176
+ end
177
+ end
178
+
179
+ describe '#quicksilver_report' do
180
+ it 'returns a hash with expected keys' do
181
+ report = engine.quicksilver_report
182
+ expect(report.keys).to include(:total_droplets, :total_pools, :captured_count,
183
+ :elusive_count, :vanishing_count, :avg_mass, :avg_fluidity)
184
+ end
185
+
186
+ it 'counts droplets' do
187
+ engine.create_droplet(form: :liquid, content: 'a')
188
+ engine.create_droplet(form: :bead, content: 'b')
189
+ expect(engine.quicksilver_report[:total_droplets]).to eq(2)
190
+ end
191
+
192
+ it 'counts pools' do
193
+ engine.create_pool(surface_type: :glass)
194
+ expect(engine.quicksilver_report[:total_pools]).to eq(1)
195
+ end
196
+
197
+ it 'computes avg_mass as 0.0 when empty' do
198
+ expect(engine.quicksilver_report[:avg_mass]).to eq(0.0)
199
+ end
200
+
201
+ it 'counts captured droplets' do
202
+ d = engine.create_droplet(form: :liquid, content: 'captured', mass: 0.5)
203
+ engine.capture_droplet(droplet_id: d.id)
204
+ expect(engine.quicksilver_report[:captured_count]).to eq(1)
205
+ end
206
+ end
207
+
208
+ describe '#droplets' do
209
+ it 'returns array of droplet hashes' do
210
+ engine.create_droplet(form: :stream, content: 'flowing')
211
+ result = engine.droplets
212
+ expect(result).to be_an(Array)
213
+ expect(result.first).to be_a(Hash)
214
+ expect(result.first[:form]).to eq(:stream)
215
+ end
216
+ end
217
+
218
+ describe '#pools' do
219
+ it 'returns array of pool hashes' do
220
+ engine.create_pool(surface_type: :wood)
221
+ result = engine.pools
222
+ expect(result).to be_an(Array)
223
+ expect(result.first[:surface_type]).to eq(:wood)
224
+ end
225
+ end
226
+ end