event_system 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,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe EventSystem::Storage::FileStore do
6
+ let(:temp_dir) { @temp_dir }
7
+ let(:store) { EventSystem::Storage::FileStore.new(temp_dir, 'test_session') }
8
+
9
+ describe '#initialize' do
10
+ it 'initializes with given directory and session ID' do
11
+ store = EventSystem::Storage::FileStore.new(temp_dir, 'custom_session')
12
+ expect(store.current_session).to eq('custom_session')
13
+ expect(store.storage_path).to eq(temp_dir)
14
+ end
15
+
16
+ it 'generates timestamp session ID when none provided' do
17
+ store = EventSystem::Storage::FileStore.new(temp_dir)
18
+ expect(store.current_session).to match(/\d{8}_\d{6}/)
19
+ end
20
+
21
+ it 'creates directory if it does not exist' do
22
+ new_dir = File.join(temp_dir, 'new_directory')
23
+ store = EventSystem::Storage::FileStore.new(new_dir)
24
+ expect(Dir.exist?(new_dir)).to be true
25
+ end
26
+ end
27
+
28
+ describe '#store' do
29
+ it 'stores an event to file' do
30
+ event = create_test_event(:test_type, 'TestSource', { key: 'value' })
31
+ store.store(event)
32
+
33
+ # Read the file to verify event was stored
34
+ file_path = File.join(temp_dir, "events_#{store.current_session}.jsonl")
35
+ expect(File.exist?(file_path)).to be true
36
+
37
+ content = File.read(file_path)
38
+ expect(content).to include(event.id)
39
+ expect(content).to include('test_type')
40
+ end
41
+
42
+ it 'flushes data immediately' do
43
+ event = create_test_event(:test_type)
44
+ store.store(event)
45
+
46
+ # File should be immediately available
47
+ file_path = File.join(temp_dir, "events_#{store.current_session}.jsonl")
48
+ expect(File.exist?(file_path)).to be true
49
+ end
50
+ end
51
+
52
+ describe '#query' do
53
+ let(:event1) { create_test_event(:type1, 'Source1', { key: 'value1' }) }
54
+ let(:event2) { create_test_event(:type2, 'Source2', { key: 'value2' }) }
55
+ let(:event3) { create_test_event(:type1, 'Source1', { key: 'value3' }) }
56
+
57
+ before do
58
+ store.store(event1)
59
+ store.store(event2)
60
+ store.store(event3)
61
+ end
62
+
63
+ it 'returns all events by default' do
64
+ events = store.query
65
+ expect(events).to include(event1, event2, event3)
66
+ end
67
+
68
+ it 'filters by type' do
69
+ events = store.query(type: 'type1')
70
+ expect(events).to include(event1, event3)
71
+ expect(events).not_to include(event2)
72
+ end
73
+
74
+ it 'filters by session_id' do
75
+ store2 = EventSystem::Storage::FileStore.new(temp_dir, 'other_session')
76
+ other_event = create_test_event(:type1)
77
+ store2.store(other_event)
78
+
79
+ events = store.query(session_id: 'other_session')
80
+ expect(events).to include(other_event)
81
+ expect(events).not_to include(event1, event2, event3)
82
+ end
83
+
84
+ it 'filters by start_time' do
85
+ # Create a new store to avoid session conflicts
86
+ new_store = EventSystem::Storage::FileStore.new(@temp_dir, 'time_test_session')
87
+
88
+ # Store events with different timestamps
89
+ past_event = EventSystem::Event.new(:past_type, 'Source', { key: 'past' }, nil, Time.now - 1)
90
+ future_event = EventSystem::Event.new(:future_type, 'Source', { key: 'future' }, nil, Time.now + 1)
91
+
92
+ new_store.store(past_event)
93
+ new_store.store(future_event)
94
+
95
+ # Query with start_time
96
+ start_time = Time.now
97
+ events = new_store.query(start_time: start_time)
98
+ expect(events).to include(future_event)
99
+ expect(events).not_to include(past_event)
100
+ end
101
+
102
+ it 'filters by end_time' do
103
+ # Create a new store to avoid session conflicts
104
+ new_store = EventSystem::Storage::FileStore.new(@temp_dir, 'time_test_session2')
105
+
106
+ # Store events with different timestamps
107
+ past_event = EventSystem::Event.new(:past_type, 'Source', { key: 'past' }, nil, Time.now - 1)
108
+ future_event = EventSystem::Event.new(:future_type, 'Source', { key: 'future' }, nil, Time.now + 1)
109
+
110
+ new_store.store(past_event)
111
+ new_store.store(future_event)
112
+
113
+ # Query with end_time
114
+ end_time = Time.now
115
+ events = new_store.query(end_time: end_time)
116
+ expect(events).to include(past_event)
117
+ expect(events).not_to include(future_event)
118
+ end
119
+
120
+ it 'limits results' do
121
+ events = store.query(limit: 2)
122
+ expect(events.length).to eq(2)
123
+ end
124
+
125
+ it 'sorts by timestamp' do
126
+ events = store.query
127
+ timestamps = events.map(&:timestamp)
128
+ expect(timestamps).to eq(timestamps.sort)
129
+ end
130
+
131
+ it 'combines multiple filters' do
132
+ events = store.query(type: 'type1', limit: 1)
133
+ expect(events.length).to eq(1)
134
+ expect(events.first.type).to eq('type1')
135
+ end
136
+ end
137
+
138
+ describe '#load_session' do
139
+ it 'loads events from current session by default' do
140
+ event = create_test_event(:test_type)
141
+ store.store(event)
142
+
143
+ events = store.load_session
144
+ expect(events).to include(event)
145
+ end
146
+
147
+ it 'loads events from specific session' do
148
+ event = create_test_event(:test_type)
149
+ store.store(event)
150
+
151
+ events = store.load_session('test_session')
152
+ expect(events).to include(event)
153
+ end
154
+
155
+ it 'returns empty array for non-existent session' do
156
+ events = store.load_session('non_existent')
157
+ expect(events).to eq([])
158
+ end
159
+
160
+ it 'handles malformed JSON lines gracefully' do
161
+ file_path = File.join(temp_dir, "events_#{store.current_session}.jsonl")
162
+ File.write(file_path, "valid_json_line\nmalformed_json_line\n")
163
+
164
+ events = store.load_session
165
+ expect(events).to be_empty
166
+ end
167
+
168
+ it 'skips empty lines' do
169
+ file_path = File.join(temp_dir, "events_#{store.current_session}.jsonl")
170
+ File.write(file_path, "\n\n")
171
+
172
+ events = store.load_session
173
+ expect(events).to be_empty
174
+ end
175
+ end
176
+
177
+ describe '#list_sessions' do
178
+ it 'returns list of session files' do
179
+ # Store an event to create the session file
180
+ store.store(create_test_event(:test_type))
181
+ expect(store.list_sessions).to eq(['test_session'])
182
+ end
183
+
184
+ it 'includes sessions created after initialization' do
185
+ # Store an event to create the initial session file
186
+ store.store(create_test_event(:test_type))
187
+ store.create_session('new_session')
188
+ # Store an event in the new session to create its file
189
+ store.store(create_test_event(:test_type))
190
+ expect(store.list_sessions).to include('test_session', 'new_session')
191
+ end
192
+
193
+ it 'sorts sessions' do
194
+ store.create_session('a_session')
195
+ store.store(create_test_event(:test_type)) # Create file for a_session
196
+ store.create_session('z_session')
197
+ store.store(create_test_event(:test_type)) # Create file for z_session
198
+
199
+ sessions = store.list_sessions
200
+ expect(sessions).to eq(sessions.sort)
201
+ end
202
+
203
+ it 'handles different filename patterns' do
204
+ # Create files with different patterns
205
+ File.write(File.join(temp_dir, 'events_session1.jsonl'), '')
206
+ File.write(File.join(temp_dir, 'session2.jsonl'), '')
207
+
208
+ store = EventSystem::Storage::FileStore.new(temp_dir)
209
+ sessions = store.list_sessions
210
+ expect(sessions).to include('session1')
211
+ # session2.jsonl doesn't match the events_*.jsonl pattern
212
+ end
213
+ end
214
+
215
+ describe '#current_session' do
216
+ it 'returns current session ID' do
217
+ expect(store.current_session).to eq('test_session')
218
+ end
219
+ end
220
+
221
+ describe '#switch_session' do
222
+ it 'switches to different session' do
223
+ store.switch_session('new_session')
224
+ expect(store.current_session).to eq('new_session')
225
+ end
226
+
227
+ it 'closes current file when switching' do
228
+ event = create_test_event(:test_type)
229
+ store.store(event)
230
+
231
+ store.switch_session('new_session')
232
+
233
+ # Should be able to store to new session
234
+ new_event = create_test_event(:new_type)
235
+ expect { store.store(new_event) }.not_to raise_error
236
+ end
237
+ end
238
+
239
+ describe '#create_session' do
240
+ it 'creates session with given ID' do
241
+ session_id = store.create_session('custom_session')
242
+ expect(session_id).to eq('custom_session')
243
+ expect(store.current_session).to eq('custom_session')
244
+ end
245
+
246
+ it 'generates timestamp ID when none provided' do
247
+ session_id = store.create_session
248
+ expect(session_id).to match(/\d{8}_\d{6}/)
249
+ expect(store.current_session).to eq(session_id)
250
+ end
251
+ end
252
+
253
+ describe '#close' do
254
+ it 'closes file handle' do
255
+ event = create_test_event(:test_type)
256
+ store.store(event)
257
+
258
+ store.close
259
+
260
+ # Should be able to close multiple times
261
+ expect { store.close }.not_to raise_error
262
+ end
263
+ end
264
+
265
+ describe '#stats' do
266
+ it 'returns storage statistics' do
267
+ store.store(create_test_event(:type1))
268
+ store.create_session('other_session')
269
+ store.store(create_test_event(:type2)) # Create file for other_session
270
+
271
+ stats = store.stats
272
+ expect(stats[:type]).to eq('EventSystem::Storage::FileStore')
273
+ expect(stats[:available]).to be true
274
+ expect(stats[:directory]).to eq(temp_dir)
275
+ expect(stats[:total_sessions]).to eq(2)
276
+ end
277
+ end
278
+
279
+ describe '#available?' do
280
+ it 'returns true' do
281
+ expect(store.available?).to be true
282
+ end
283
+ end
284
+
285
+ describe 'session isolation' do
286
+ it 'maintains separate files per session' do
287
+ event1 = create_test_event(:type1)
288
+ store.store(event1)
289
+
290
+ store.create_session('session2')
291
+ event2 = create_test_event(:type2)
292
+ store.store(event2)
293
+
294
+ session1_file = File.join(temp_dir, 'events_test_session.jsonl')
295
+ session2_file = File.join(temp_dir, 'events_session2.jsonl')
296
+
297
+ expect(File.exist?(session1_file)).to be true
298
+ expect(File.exist?(session2_file)).to be true
299
+
300
+ session1_content = File.read(session1_file)
301
+ session2_content = File.read(session2_file)
302
+
303
+ expect(session1_content).to include('type1')
304
+ expect(session1_content).not_to include('type2')
305
+ expect(session2_content).to include('type2')
306
+ expect(session2_content).not_to include('type1')
307
+ end
308
+ end
309
+
310
+ describe 'file format' do
311
+ it 'stores events in JSONL format' do
312
+ event = create_test_event(:test_type, 'TestSource', { key: 'value' })
313
+ store.store(event)
314
+
315
+ file_path = File.join(temp_dir, "events_#{store.current_session}.jsonl")
316
+ content = File.read(file_path)
317
+
318
+ # Should be valid JSON
319
+ expect { JSON.parse(content) }.not_to raise_error
320
+
321
+ # Should contain event data
322
+ parsed = JSON.parse(content, symbolize_names: true)
323
+ expect(parsed[:type]).to eq('test_type')
324
+ expect(parsed[:source]).to eq('TestSource')
325
+ expect(parsed[:data][:key]).to eq('value')
326
+ end
327
+ end
328
+
329
+ describe 'backward compatibility' do
330
+ it 'finds session files with different naming patterns' do
331
+ # Create file without events_ prefix
332
+ file_path = File.join(temp_dir, 'legacy_session.jsonl')
333
+ File.write(file_path, '{"type":"test","source":"test","timestamp":"2023-01-01T00:00:00.000Z","id":"test-id","data":{}}')
334
+
335
+ store = EventSystem::Storage::FileStore.new(temp_dir)
336
+ events = store.load_session('legacy_session')
337
+ expect(events.length).to eq(1)
338
+ expect(events.first.type).to eq('test')
339
+ end
340
+ end
341
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe EventSystem::Storage::MemoryStore do
6
+ let(:store) { EventSystem::Storage::MemoryStore.new('test_session') }
7
+
8
+ describe '#initialize' do
9
+ it 'initializes with given session ID' do
10
+ store = EventSystem::Storage::MemoryStore.new('custom_session')
11
+ expect(store.current_session).to eq('custom_session')
12
+ end
13
+
14
+ it 'generates timestamp session ID when none provided' do
15
+ store = EventSystem::Storage::MemoryStore.new
16
+ expect(store.current_session).to match(/\d{8}_\d{6}/)
17
+ end
18
+
19
+ it 'initializes empty events and sessions' do
20
+ expect(store.size).to eq(0)
21
+ expect(store.list_sessions).to eq(['test_session'])
22
+ end
23
+ end
24
+
25
+ describe '#store' do
26
+ it 'stores an event' do
27
+ event = create_test_event(:test_type)
28
+ store.store(event)
29
+
30
+ expect(store.size).to eq(1)
31
+ expect(store.load_session).to include(event)
32
+ end
33
+
34
+ it 'stores event in current session' do
35
+ event = create_test_event(:test_type)
36
+ store.store(event)
37
+
38
+ events = store.load_session('test_session')
39
+ expect(events).to include(event)
40
+ end
41
+ end
42
+
43
+ describe '#query' do
44
+ let(:event1) { create_test_event(:type1, 'Source1', { key: 'value1' }) }
45
+ let(:event2) { create_test_event(:type2, 'Source2', { key: 'value2' }) }
46
+ let(:event3) { create_test_event(:type1, 'Source1', { key: 'value3' }) }
47
+
48
+ before do
49
+ store.store(event1)
50
+ store.store(event2)
51
+ store.store(event3)
52
+ end
53
+
54
+ it 'returns all events by default' do
55
+ events = store.query
56
+ expect(events).to include(event1, event2, event3)
57
+ end
58
+
59
+ it 'filters by type' do
60
+ events = store.query(type: 'type1')
61
+ expect(events).to include(event1, event3)
62
+ expect(events).not_to include(event2)
63
+ end
64
+
65
+ it 'filters by session_id' do
66
+ store2 = EventSystem::Storage::MemoryStore.new('other_session')
67
+ other_event = create_test_event(:type1)
68
+ store2.store(other_event)
69
+
70
+ events = store2.query(session_id: 'other_session')
71
+ expect(events).to include(other_event)
72
+ expect(events).not_to include(event1, event2, event3)
73
+ end
74
+
75
+ it 'filters by start_time' do
76
+ start_time = event2.timestamp
77
+ events = store.query(start_time: start_time)
78
+ expect(events).to include(event2, event3)
79
+ expect(events).not_to include(event1)
80
+ end
81
+
82
+ it 'filters by end_time' do
83
+ end_time = event1.timestamp
84
+ events = store.query(end_time: end_time)
85
+ expect(events).to include(event1)
86
+ expect(events).not_to include(event2, event3)
87
+ end
88
+
89
+ it 'limits results' do
90
+ events = store.query(limit: 2)
91
+ expect(events.length).to eq(2)
92
+ end
93
+
94
+ it 'sorts by timestamp' do
95
+ events = store.query
96
+ timestamps = events.map(&:timestamp)
97
+ expect(timestamps).to eq(timestamps.sort)
98
+ end
99
+
100
+ it 'combines multiple filters' do
101
+ events = store.query(type: 'type1', limit: 1)
102
+ expect(events.length).to eq(1)
103
+ expect(events.first.type).to eq('type1')
104
+ end
105
+ end
106
+
107
+ describe '#load_session' do
108
+ it 'loads events from current session by default' do
109
+ event = create_test_event(:test_type)
110
+ store.store(event)
111
+
112
+ events = store.load_session
113
+ expect(events).to include(event)
114
+ end
115
+
116
+ it 'loads events from specific session' do
117
+ event = create_test_event(:test_type)
118
+ store.store(event)
119
+
120
+ events = store.load_session('test_session')
121
+ expect(events).to include(event)
122
+ end
123
+
124
+ it 'returns empty array for non-existent session' do
125
+ events = store.load_session('non_existent')
126
+ expect(events).to eq([])
127
+ end
128
+ end
129
+
130
+ describe '#list_sessions' do
131
+ it 'returns list of session IDs' do
132
+ expect(store.list_sessions).to eq(['test_session'])
133
+ end
134
+
135
+ it 'includes sessions created after initialization' do
136
+ store.create_session('new_session')
137
+ expect(store.list_sessions).to include('test_session', 'new_session')
138
+ end
139
+
140
+ it 'sorts sessions' do
141
+ store.create_session('a_session')
142
+ store.create_session('z_session')
143
+
144
+ sessions = store.list_sessions
145
+ expect(sessions).to eq(sessions.sort)
146
+ end
147
+ end
148
+
149
+ describe '#current_session' do
150
+ it 'returns current session ID' do
151
+ expect(store.current_session).to eq('test_session')
152
+ end
153
+ end
154
+
155
+ describe '#switch_session' do
156
+ it 'switches to different session' do
157
+ store.switch_session('new_session')
158
+ expect(store.current_session).to eq('new_session')
159
+ end
160
+
161
+ it 'creates session if it does not exist' do
162
+ store.switch_session('new_session')
163
+ expect(store.list_sessions).to include('new_session')
164
+ end
165
+ end
166
+
167
+ describe '#create_session' do
168
+ it 'creates session with given ID' do
169
+ session_id = store.create_session('custom_session')
170
+ expect(session_id).to eq('custom_session')
171
+ expect(store.current_session).to eq('custom_session')
172
+ end
173
+
174
+ it 'generates timestamp ID when none provided' do
175
+ session_id = store.create_session
176
+ expect(session_id).to match(/\d{8}_\d{6}/)
177
+ expect(store.current_session).to eq(session_id)
178
+ end
179
+ end
180
+
181
+ describe '#clear!' do
182
+ it 'clears all events and sessions' do
183
+ event = create_test_event(:test_type)
184
+ store.store(event)
185
+ store.create_session('other_session')
186
+
187
+ store.clear!
188
+
189
+ expect(store.size).to eq(0)
190
+ expect(store.list_sessions.length).to eq(1)
191
+ expect(store.current_session).to match(/\d{8}_\d{6}/)
192
+ end
193
+ end
194
+
195
+ describe '#size' do
196
+ it 'returns total number of events' do
197
+ expect(store.size).to eq(0)
198
+
199
+ store.store(create_test_event(:type1))
200
+ store.store(create_test_event(:type2))
201
+
202
+ expect(store.size).to eq(2)
203
+ end
204
+ end
205
+
206
+ describe '#stats' do
207
+ it 'returns storage statistics' do
208
+ store.store(create_test_event(:type1))
209
+ store.create_session('other_session')
210
+ store.store(create_test_event(:type2)) # Store event in new session
211
+
212
+ stats = store.stats
213
+ expect(stats[:type]).to eq('EventSystem::Storage::MemoryStore')
214
+ expect(stats[:available]).to be true
215
+ expect(stats[:total_events]).to eq(2)
216
+ expect(stats[:sessions]).to eq(2)
217
+ expect(stats[:current_session_events]).to eq(1)
218
+ end
219
+ end
220
+
221
+ describe '#available?' do
222
+ it 'returns true' do
223
+ expect(store.available?).to be true
224
+ end
225
+ end
226
+
227
+ describe 'session isolation' do
228
+ it 'maintains separate events per session' do
229
+ # Use separate store instances to test isolation
230
+ store1 = EventSystem::Storage::MemoryStore.new('session1')
231
+ store2 = EventSystem::Storage::MemoryStore.new('session2')
232
+
233
+ event1 = create_test_event(:type1)
234
+ event2 = create_test_event(:type2)
235
+
236
+ store1.store(event1)
237
+ store2.store(event2)
238
+
239
+ session1_events = store1.load_session('session1')
240
+ session2_events = store2.load_session('session2')
241
+
242
+ expect(session1_events).to include(event1)
243
+ expect(session1_events).not_to include(event2)
244
+ expect(session2_events).to include(event2)
245
+ expect(session2_events).not_to include(event1)
246
+ end
247
+ end
248
+ end