lex-cognitive-chunking 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,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::Chunk do
4
+ let(:chunk) { described_class.new(label: 'chess opening') }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(chunk.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'stores label' do
12
+ expect(chunk.label).to eq('chess opening')
13
+ end
14
+
15
+ it 'starts with empty item_ids' do
16
+ expect(chunk.item_ids).to be_empty
17
+ end
18
+
19
+ it 'starts with empty sub_chunk_ids' do
20
+ expect(chunk.sub_chunk_ids).to be_empty
21
+ end
22
+
23
+ it 'starts with DEFAULT_COHERENCE' do
24
+ expect(chunk.coherence).to eq(Legion::Extensions::CognitiveChunking::Helpers::Constants::DEFAULT_COHERENCE)
25
+ end
26
+
27
+ it 'starts with recall_strength of 0.8' do
28
+ expect(chunk.recall_strength).to eq(0.8)
29
+ end
30
+
31
+ it 'starts with access_count of 0' do
32
+ expect(chunk.access_count).to eq(0)
33
+ end
34
+
35
+ it 'pre-populates item_ids from constructor' do
36
+ c = described_class.new(label: 'test', item_ids: %w[a b c])
37
+ expect(c.item_ids).to eq(%w[a b c])
38
+ end
39
+
40
+ it 'does not share item_ids reference' do
41
+ ids = %w[a b]
42
+ c = described_class.new(label: 'test', item_ids: ids)
43
+ ids << 'c'
44
+ expect(c.item_ids.size).to eq(2)
45
+ end
46
+ end
47
+
48
+ describe '#add_item!' do
49
+ it 'adds item_id to item_ids' do
50
+ chunk.add_item!(item_id: 'abc')
51
+ expect(chunk.item_ids).to include('abc')
52
+ end
53
+
54
+ it 'does not add duplicates' do
55
+ chunk.add_item!(item_id: 'abc')
56
+ chunk.add_item!(item_id: 'abc')
57
+ expect(chunk.item_ids.count('abc')).to eq(1)
58
+ end
59
+ end
60
+
61
+ describe '#remove_item!' do
62
+ before { chunk.add_item!(item_id: 'abc') }
63
+
64
+ it 'removes item_id' do
65
+ chunk.remove_item!(item_id: 'abc')
66
+ expect(chunk.item_ids).not_to include('abc')
67
+ end
68
+ end
69
+
70
+ describe '#add_sub_chunk!' do
71
+ it 'adds sub_chunk_id' do
72
+ chunk.add_sub_chunk!(chunk_id: 'sub-1')
73
+ expect(chunk.sub_chunk_ids).to include('sub-1')
74
+ end
75
+
76
+ it 'does not add duplicates' do
77
+ chunk.add_sub_chunk!(chunk_id: 'sub-1')
78
+ chunk.add_sub_chunk!(chunk_id: 'sub-1')
79
+ expect(chunk.sub_chunk_ids.count('sub-1')).to eq(1)
80
+ end
81
+ end
82
+
83
+ describe '#reinforce!' do
84
+ it 'increments access_count' do
85
+ chunk.reinforce!
86
+ expect(chunk.access_count).to eq(1)
87
+ end
88
+
89
+ it 'boosts coherence' do
90
+ original = chunk.coherence
91
+ chunk.reinforce!
92
+ expect(chunk.coherence).to be > original
93
+ end
94
+
95
+ it 'boosts recall_strength' do
96
+ original = chunk.recall_strength
97
+ chunk.reinforce!
98
+ expect(chunk.recall_strength).to be > original
99
+ end
100
+
101
+ it 'caps coherence at 1.0' do
102
+ 20.times { chunk.reinforce! }
103
+ expect(chunk.coherence).to eq(1.0)
104
+ end
105
+
106
+ it 'caps recall_strength at 1.0' do
107
+ 20.times { chunk.reinforce! }
108
+ expect(chunk.recall_strength).to eq(1.0)
109
+ end
110
+ end
111
+
112
+ describe '#decay!' do
113
+ it 'reduces recall_strength' do
114
+ original = chunk.recall_strength
115
+ chunk.decay!
116
+ expect(chunk.recall_strength).to be < original
117
+ end
118
+
119
+ it 'reduces coherence' do
120
+ original = chunk.coherence
121
+ chunk.decay!
122
+ expect(chunk.coherence).to be < original
123
+ end
124
+
125
+ it 'floors recall_strength at 0.0' do
126
+ 100.times { chunk.decay! }
127
+ expect(chunk.recall_strength).to eq(0.0)
128
+ end
129
+ end
130
+
131
+ describe '#size' do
132
+ it 'returns count of item_ids' do
133
+ chunk.add_item!(item_id: 'a')
134
+ chunk.add_item!(item_id: 'b')
135
+ expect(chunk.size).to eq(2)
136
+ end
137
+ end
138
+
139
+ describe '#hierarchical?' do
140
+ it 'returns false when no sub_chunks' do
141
+ expect(chunk.hierarchical?).to be false
142
+ end
143
+
144
+ it 'returns true when sub_chunks exist' do
145
+ chunk.add_sub_chunk!(chunk_id: 'child')
146
+ expect(chunk.hierarchical?).to be true
147
+ end
148
+ end
149
+
150
+ describe '#coherence_label' do
151
+ it 'returns :loosely_chunked for default coherence (0.5)' do
152
+ expect(chunk.coherence_label).to eq(:loosely_chunked)
153
+ end
154
+ end
155
+
156
+ describe '#recall_label' do
157
+ it 'returns :instant for high recall_strength (0.8)' do
158
+ expect(chunk.recall_label).to eq(:instant)
159
+ end
160
+ end
161
+
162
+ describe '#size_label' do
163
+ it 'returns :micro for empty chunk' do
164
+ expect(chunk.size_label).to eq(:micro)
165
+ end
166
+
167
+ it 'returns :large for 7+ items' do
168
+ 7.times { |i| chunk.add_item!(item_id: "item-#{i}") }
169
+ expect(chunk.size_label).to eq(:large)
170
+ end
171
+ end
172
+
173
+ describe '#to_h' do
174
+ it 'includes all expected keys' do
175
+ h = chunk.to_h
176
+ expected = %i[id label item_ids sub_chunk_ids coherence recall_strength
177
+ access_count created_at size hierarchical coherence_label
178
+ recall_label size_label]
179
+ expect(h.keys).to include(*expected)
180
+ end
181
+
182
+ it 'rounds coherence to 10 decimal places' do
183
+ chunk.reinforce!
184
+ expect(chunk.to_h[:coherence]).to eq(chunk.coherence.round(10))
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::ChunkingEngine do
4
+ let(:engine) { described_class.new }
5
+
6
+ def add_items(count, domain: :general)
7
+ count.times.map { |i| engine.add_item(content: "item #{i}", domain: domain) }
8
+ end
9
+
10
+ describe '#add_item' do
11
+ it 'returns success with item_id' do
12
+ result = engine.add_item(content: 'rook pins queen', domain: :chess)
13
+ expect(result[:success]).to be true
14
+ expect(result[:item_id]).to be_a(String)
15
+ end
16
+
17
+ it 'stores the item' do
18
+ result = engine.add_item(content: 'test', domain: :general)
19
+ expect(engine.items[result[:item_id]]).not_to be_nil
20
+ end
21
+
22
+ it 'returns item hash in result' do
23
+ result = engine.add_item(content: 'test')
24
+ expect(result[:item]).to have_key(:id)
25
+ end
26
+ end
27
+
28
+ describe '#create_chunk' do
29
+ let(:items) { add_items(3) }
30
+ let(:item_ids) { items.map { |r| r[:item_id] } }
31
+
32
+ it 'returns success with chunk_id' do
33
+ result = engine.create_chunk(label: 'group', item_ids: item_ids)
34
+ expect(result[:success]).to be true
35
+ expect(result[:chunk_id]).to be_a(String)
36
+ end
37
+
38
+ it 'stores the chunk' do
39
+ result = engine.create_chunk(label: 'group', item_ids: item_ids)
40
+ expect(engine.chunks[result[:chunk_id]]).not_to be_nil
41
+ end
42
+
43
+ it 'marks items as chunked' do
44
+ result = engine.create_chunk(label: 'group', item_ids: item_ids)
45
+ item_ids.each do |id|
46
+ expect(engine.items[id].chunked?).to be true
47
+ expect(engine.items[id].chunk_id).to eq(result[:chunk_id])
48
+ end
49
+ end
50
+
51
+ it 'fails for empty item_ids' do
52
+ result = engine.create_chunk(label: 'empty', item_ids: [])
53
+ expect(result[:success]).to be false
54
+ expect(result[:error]).to eq(:empty_item_ids)
55
+ end
56
+
57
+ it 'fails for non-existent item_ids' do
58
+ result = engine.create_chunk(label: 'bad', item_ids: %w[nonexistent])
59
+ expect(result[:success]).to be false
60
+ expect(result[:error]).to eq(:no_valid_items)
61
+ end
62
+ end
63
+
64
+ describe '#merge_chunks' do
65
+ def setup_two_chunks
66
+ items_a = add_items(2)
67
+ items_b = add_items(2)
68
+ chunk_a = engine.create_chunk(label: 'A', item_ids: items_a.map { |r| r[:item_id] })
69
+ chunk_b = engine.create_chunk(label: 'B', item_ids: items_b.map { |r| r[:item_id] })
70
+ [chunk_a[:chunk_id], chunk_b[:chunk_id]]
71
+ end
72
+
73
+ it 'creates a hierarchical parent chunk' do
74
+ chunk_ids = setup_two_chunks
75
+ result = engine.merge_chunks(chunk_ids: chunk_ids, label: 'Parent')
76
+ expect(result[:success]).to be true
77
+ parent = engine.chunks[result[:chunk_id]]
78
+ expect(parent.hierarchical?).to be true
79
+ end
80
+
81
+ it 'parent has all item_ids from children' do
82
+ chunk_ids = setup_two_chunks
83
+ result = engine.merge_chunks(chunk_ids: chunk_ids, label: 'Parent')
84
+ parent = engine.chunks[result[:chunk_id]]
85
+ expect(parent.item_ids.size).to eq(4)
86
+ end
87
+
88
+ it 'includes merged_from in result' do
89
+ chunk_ids = setup_two_chunks
90
+ result = engine.merge_chunks(chunk_ids: chunk_ids, label: 'Parent')
91
+ expect(result[:merged_from]).to match_array(chunk_ids)
92
+ end
93
+
94
+ it 'fails with fewer than 2 chunk_ids' do
95
+ add_items(2)
96
+ result = engine.merge_chunks(chunk_ids: ['only-one'], label: 'Bad')
97
+ expect(result[:success]).to be false
98
+ expect(result[:error]).to eq(:insufficient_chunks)
99
+ end
100
+ end
101
+
102
+ describe '#load_to_working_memory' do
103
+ let(:item_id) { engine.add_item(content: 'test')[:item_id] }
104
+ let(:chunk_id) { engine.create_chunk(label: 'wm test', item_ids: [item_id])[:chunk_id] }
105
+
106
+ it 'adds chunk to working memory' do
107
+ result = engine.load_to_working_memory(chunk_id: chunk_id)
108
+ expect(result[:success]).to be true
109
+ expect(engine.working_memory).to include(chunk_id)
110
+ end
111
+
112
+ it 'reinforces the chunk on load' do
113
+ original_count = engine.chunks[chunk_id].access_count
114
+ engine.load_to_working_memory(chunk_id: chunk_id)
115
+ expect(engine.chunks[chunk_id].access_count).to be > original_count
116
+ end
117
+
118
+ it 'fails for unknown chunk' do
119
+ result = engine.load_to_working_memory(chunk_id: 'nonexistent')
120
+ expect(result[:success]).to be false
121
+ expect(result[:error]).to eq(:chunk_not_found)
122
+ end
123
+
124
+ it 'fails when already loaded' do
125
+ engine.load_to_working_memory(chunk_id: chunk_id)
126
+ result = engine.load_to_working_memory(chunk_id: chunk_id)
127
+ expect(result[:success]).to be false
128
+ expect(result[:error]).to eq(:already_loaded)
129
+ end
130
+
131
+ it 'fails when working memory is at capacity' do
132
+ # Fill working memory to WORKING_MEMORY_CAPACITY
133
+ 7.times do
134
+ id = engine.add_item(content: 'filler')[:item_id]
135
+ cid = engine.create_chunk(label: 'filler', item_ids: [id])[:chunk_id]
136
+ engine.load_to_working_memory(chunk_id: cid)
137
+ end
138
+ result = engine.load_to_working_memory(chunk_id: chunk_id)
139
+ expect(result[:success]).to be false
140
+ expect(result[:error]).to eq(:capacity_exceeded)
141
+ end
142
+ end
143
+
144
+ describe '#unload_from_working_memory' do
145
+ let(:item_id) { engine.add_item(content: 'test')[:item_id] }
146
+ let(:chunk_id) { engine.create_chunk(label: 'unload test', item_ids: [item_id])[:chunk_id] }
147
+
148
+ before { engine.load_to_working_memory(chunk_id: chunk_id) }
149
+
150
+ it 'removes chunk from working memory' do
151
+ engine.unload_from_working_memory(chunk_id: chunk_id)
152
+ expect(engine.working_memory).not_to include(chunk_id)
153
+ end
154
+
155
+ it 'returns success' do
156
+ result = engine.unload_from_working_memory(chunk_id: chunk_id)
157
+ expect(result[:success]).to be true
158
+ end
159
+
160
+ it 'fails if not in working memory' do
161
+ engine.unload_from_working_memory(chunk_id: chunk_id)
162
+ result = engine.unload_from_working_memory(chunk_id: chunk_id)
163
+ expect(result[:success]).to be false
164
+ expect(result[:error]).to eq(:not_in_working_memory)
165
+ end
166
+ end
167
+
168
+ describe '#working_memory_load' do
169
+ it 'returns 0.0 for empty working memory' do
170
+ expect(engine.working_memory_load).to eq(0.0)
171
+ end
172
+
173
+ it 'returns load as a ratio' do
174
+ id = engine.add_item(content: 'x')[:item_id]
175
+ cid = engine.create_chunk(label: 'x', item_ids: [id])[:chunk_id]
176
+ engine.load_to_working_memory(chunk_id: cid)
177
+ expected = (1.0 / 7).round(10)
178
+ expect(engine.working_memory_load).to be_within(0.0001).of(expected)
179
+ end
180
+ end
181
+
182
+ describe '#working_memory_overloaded?' do
183
+ it 'returns false when under capacity' do
184
+ expect(engine.working_memory_overloaded?).to be false
185
+ end
186
+ end
187
+
188
+ describe '#decay_all!' do
189
+ it 'decays all chunks' do
190
+ id = engine.add_item(content: 'x')[:item_id]
191
+ cid = engine.create_chunk(label: 'x', item_ids: [id])[:chunk_id]
192
+ original_recall = engine.chunks[cid].recall_strength
193
+ engine.decay_all!
194
+ expect(engine.chunks[cid].recall_strength).to be < original_recall
195
+ end
196
+
197
+ it 'returns success with count' do
198
+ add_items(3).each_with_index do |r, i|
199
+ engine.create_chunk(label: "c#{i}", item_ids: [r[:item_id]])
200
+ end
201
+ result = engine.decay_all!
202
+ expect(result[:success]).to be true
203
+ expect(result[:chunks_decayed]).to eq(3)
204
+ end
205
+ end
206
+
207
+ describe '#reinforce_chunk' do
208
+ let(:item_id) { engine.add_item(content: 'reinforce me')[:item_id] }
209
+ let(:chunk_id) { engine.create_chunk(label: 'reinforce', item_ids: [item_id])[:chunk_id] }
210
+
211
+ it 'boosts recall strength' do
212
+ before_recall = engine.chunks[chunk_id].recall_strength
213
+ engine.reinforce_chunk(chunk_id: chunk_id)
214
+ expect(engine.chunks[chunk_id].recall_strength).to be > before_recall
215
+ end
216
+
217
+ it 'fails for unknown chunk_id' do
218
+ result = engine.reinforce_chunk(chunk_id: 'bad')
219
+ expect(result[:success]).to be false
220
+ expect(result[:error]).to eq(:chunk_not_found)
221
+ end
222
+ end
223
+
224
+ describe '#strongest_chunks' do
225
+ it 'returns chunks sorted by recall_strength descending' do
226
+ r1 = engine.add_item(content: 'a')[:item_id]
227
+ r2 = engine.add_item(content: 'b')[:item_id]
228
+ c1 = engine.create_chunk(label: 'weak', item_ids: [r1])[:chunk_id]
229
+ c2 = engine.create_chunk(label: 'strong', item_ids: [r2])[:chunk_id]
230
+ engine.chunks[c1].decay!
231
+ engine.chunks[c2].reinforce!
232
+ chunks = engine.strongest_chunks(limit: 2)
233
+ expect(chunks.first[:recall_strength]).to be >= chunks.last[:recall_strength]
234
+ end
235
+
236
+ it 'respects limit' do
237
+ add_items(5).each_with_index { |r, i| engine.create_chunk(label: "c#{i}", item_ids: [r[:item_id]]) }
238
+ expect(engine.strongest_chunks(limit: 3).size).to eq(3)
239
+ end
240
+ end
241
+
242
+ describe '#unchunked_items' do
243
+ it 'returns items not in any chunk' do
244
+ r1 = engine.add_item(content: 'chunked item')[:item_id]
245
+ engine.add_item(content: 'free item')
246
+ engine.create_chunk(label: 'group', item_ids: [r1])
247
+ unchunked = engine.unchunked_items
248
+ expect(unchunked.size).to eq(1)
249
+ expect(unchunked.first[:content]).to eq('free item')
250
+ end
251
+ end
252
+
253
+ describe '#chunking_efficiency' do
254
+ it 'returns 0.0 for no items' do
255
+ expect(engine.chunking_efficiency).to eq(0.0)
256
+ end
257
+
258
+ it 'returns ratio of chunked to total' do
259
+ r1 = engine.add_item(content: 'a')[:item_id]
260
+ engine.add_item(content: 'b')
261
+ engine.create_chunk(label: 'test', item_ids: [r1])
262
+ expect(engine.chunking_efficiency).to be_within(0.001).of(0.5)
263
+ end
264
+ end
265
+
266
+ describe '#chunking_report' do
267
+ it 'includes required report keys' do
268
+ report = engine.chunking_report
269
+ expect(report).to have_key(:total_items)
270
+ expect(report).to have_key(:total_chunks)
271
+ expect(report).to have_key(:unchunked_items)
272
+ expect(report).to have_key(:chunking_efficiency)
273
+ expect(report).to have_key(:working_memory)
274
+ expect(report).to have_key(:strongest_chunks)
275
+ end
276
+
277
+ it 'includes working memory capacity label' do
278
+ expect(engine.chunking_report[:working_memory][:label]).to be_a(Symbol)
279
+ end
280
+ end
281
+
282
+ describe '#to_h' do
283
+ it 'includes items, chunks, and working_memory keys' do
284
+ h = engine.to_h
285
+ expect(h).to have_key(:items)
286
+ expect(h).to have_key(:chunks)
287
+ expect(h).to have_key(:working_memory)
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::Constants do
4
+ describe 'core capacity constants' do
5
+ it 'defines MAX_ITEMS as 500' do
6
+ expect(described_class::MAX_ITEMS).to eq(500)
7
+ end
8
+
9
+ it 'defines MAX_CHUNKS as 200' do
10
+ expect(described_class::MAX_CHUNKS).to eq(200)
11
+ end
12
+
13
+ it 'defines WORKING_MEMORY_CAPACITY as 7' do
14
+ expect(described_class::WORKING_MEMORY_CAPACITY).to eq(7)
15
+ end
16
+
17
+ it 'defines CAPACITY_VARIANCE as 2' do
18
+ expect(described_class::CAPACITY_VARIANCE).to eq(2)
19
+ end
20
+ end
21
+
22
+ describe 'score constants' do
23
+ it 'defines DEFAULT_COHERENCE as 0.5' do
24
+ expect(described_class::DEFAULT_COHERENCE).to eq(0.5)
25
+ end
26
+
27
+ it 'defines COHERENCE_BOOST as 0.08' do
28
+ expect(described_class::COHERENCE_BOOST).to eq(0.08)
29
+ end
30
+
31
+ it 'defines COHERENCE_DECAY as 0.03' do
32
+ expect(described_class::COHERENCE_DECAY).to eq(0.03)
33
+ end
34
+
35
+ it 'defines RECALL_DECAY as 0.02' do
36
+ expect(described_class::RECALL_DECAY).to eq(0.02)
37
+ end
38
+
39
+ it 'defines RECALL_BOOST as 0.1' do
40
+ expect(described_class::RECALL_BOOST).to eq(0.1)
41
+ end
42
+ end
43
+
44
+ describe 'CHUNK_SIZE_LABELS' do
45
+ it 'labels size 7 as large' do
46
+ label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(7) }&.last
47
+ expect(label).to eq(:large)
48
+ end
49
+
50
+ it 'labels size 5 as medium' do
51
+ label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(5) }&.last
52
+ expect(label).to eq(:medium)
53
+ end
54
+
55
+ it 'labels size 3 as small' do
56
+ label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(3) }&.last
57
+ expect(label).to eq(:small)
58
+ end
59
+
60
+ it 'labels size 1 as micro' do
61
+ label = described_class::CHUNK_SIZE_LABELS.find { |range, _| range.cover?(1) }&.last
62
+ expect(label).to eq(:micro)
63
+ end
64
+ end
65
+
66
+ describe 'COHERENCE_LABELS' do
67
+ it 'labels 0.9 as tightly_chunked' do
68
+ label = described_class::COHERENCE_LABELS.find { |range, _| range.cover?(0.9) }&.last
69
+ expect(label).to eq(:tightly_chunked)
70
+ end
71
+
72
+ it 'labels 0.5 as loosely_chunked' do
73
+ label = described_class::COHERENCE_LABELS.find { |range, _| range.cover?(0.5) }&.last
74
+ expect(label).to eq(:loosely_chunked)
75
+ end
76
+
77
+ it 'labels 0.1 as unchunked' do
78
+ label = described_class::COHERENCE_LABELS.find { |range, _| range.cover?(0.1) }&.last
79
+ expect(label).to eq(:unchunked)
80
+ end
81
+ end
82
+
83
+ describe 'RECALL_LABELS' do
84
+ it 'labels 0.9 as instant' do
85
+ label = described_class::RECALL_LABELS.find { |range, _| range.cover?(0.9) }&.last
86
+ expect(label).to eq(:instant)
87
+ end
88
+
89
+ it 'labels 0.5 as moderate' do
90
+ label = described_class::RECALL_LABELS.find { |range, _| range.cover?(0.5) }&.last
91
+ expect(label).to eq(:moderate)
92
+ end
93
+
94
+ it 'labels 0.1 as forgotten' do
95
+ label = described_class::RECALL_LABELS.find { |range, _| range.cover?(0.1) }&.last
96
+ expect(label).to eq(:forgotten)
97
+ end
98
+ end
99
+
100
+ describe 'CAPACITY_LABELS' do
101
+ it 'labels 0.9 as overloaded' do
102
+ label = described_class::CAPACITY_LABELS.find { |range, _| range.cover?(0.9) }&.last
103
+ expect(label).to eq(:overloaded)
104
+ end
105
+
106
+ it 'labels 0.5 as comfortable' do
107
+ label = described_class::CAPACITY_LABELS.find { |range, _| range.cover?(0.5) }&.last
108
+ expect(label).to eq(:comfortable)
109
+ end
110
+
111
+ it 'labels 0.1 as empty' do
112
+ label = described_class::CAPACITY_LABELS.find { |range, _| range.cover?(0.1) }&.last
113
+ expect(label).to eq(:empty)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::CognitiveChunking::Helpers::InformationItem do
4
+ let(:item) { described_class.new(content: 'bishop controls e4', domain: :chess) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a uuid id' do
8
+ expect(item.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'stores content' do
12
+ expect(item.content).to eq('bishop controls e4')
13
+ end
14
+
15
+ it 'stores domain' do
16
+ expect(item.domain).to eq(:chess)
17
+ end
18
+
19
+ it 'defaults chunked to false' do
20
+ expect(item.chunked?).to be false
21
+ end
22
+
23
+ it 'defaults chunk_id to nil' do
24
+ expect(item.chunk_id).to be_nil
25
+ end
26
+
27
+ it 'sets created_at' do
28
+ expect(item.created_at).to be_a(Time)
29
+ end
30
+
31
+ it 'defaults domain to :general' do
32
+ plain = described_class.new(content: 'hello')
33
+ expect(plain.domain).to eq(:general)
34
+ end
35
+ end
36
+
37
+ describe '#assign_to_chunk!' do
38
+ it 'marks item as chunked' do
39
+ item.assign_to_chunk!(chunk_id: 'abc-123')
40
+ expect(item.chunked?).to be true
41
+ end
42
+
43
+ it 'stores the chunk_id' do
44
+ item.assign_to_chunk!(chunk_id: 'abc-123')
45
+ expect(item.chunk_id).to eq('abc-123')
46
+ end
47
+ end
48
+
49
+ describe '#unchunk!' do
50
+ before { item.assign_to_chunk!(chunk_id: 'abc-123') }
51
+
52
+ it 'clears chunked flag' do
53
+ item.unchunk!
54
+ expect(item.chunked?).to be false
55
+ end
56
+
57
+ it 'clears chunk_id' do
58
+ item.unchunk!
59
+ expect(item.chunk_id).to be_nil
60
+ end
61
+ end
62
+
63
+ describe '#to_h' do
64
+ it 'includes all expected keys' do
65
+ h = item.to_h
66
+ expect(h.keys).to contain_exactly(:id, :content, :domain, :chunked, :chunk_id, :created_at)
67
+ end
68
+
69
+ it 'reflects chunked state' do
70
+ item.assign_to_chunk!(chunk_id: 'xyz')
71
+ expect(item.to_h[:chunked]).to be true
72
+ expect(item.to_h[:chunk_id]).to eq('xyz')
73
+ end
74
+ end
75
+ end