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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/.rubocop.yml +32 -0
- data/CLAUDE.md +123 -0
- data/Gemfile +11 -0
- data/README.md +67 -0
- data/lex-cognitive-quicksilver.gemspec +27 -0
- data/lib/legion/extensions/cognitive_quicksilver/client.rb +25 -0
- data/lib/legion/extensions/cognitive_quicksilver/helpers/constants.rb +46 -0
- data/lib/legion/extensions/cognitive_quicksilver/helpers/droplet.rb +122 -0
- data/lib/legion/extensions/cognitive_quicksilver/helpers/pool.rb +79 -0
- data/lib/legion/extensions/cognitive_quicksilver/helpers/quicksilver_engine.rb +120 -0
- data/lib/legion/extensions/cognitive_quicksilver/runners/cognitive_quicksilver.rb +126 -0
- data/lib/legion/extensions/cognitive_quicksilver/version.rb +9 -0
- data/lib/legion/extensions/cognitive_quicksilver.rb +18 -0
- data/spec/legion/extensions/cognitive_quicksilver/client_spec.rb +72 -0
- data/spec/legion/extensions/cognitive_quicksilver/helpers/constants_spec.rb +105 -0
- data/spec/legion/extensions/cognitive_quicksilver/helpers/droplet_spec.rb +310 -0
- data/spec/legion/extensions/cognitive_quicksilver/helpers/pool_spec.rb +174 -0
- data/spec/legion/extensions/cognitive_quicksilver/helpers/quicksilver_engine_spec.rb +226 -0
- data/spec/legion/extensions/cognitive_quicksilver/runners/cognitive_quicksilver_spec.rb +227 -0
- data/spec/spec_helper.rb +30 -0
- metadata +83 -0
|
@@ -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
|