lex-apollo 0.2.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +135 -0
  4. data/lib/legion/extensions/apollo/actors/corroboration_checker.rb +22 -0
  5. data/lib/legion/extensions/apollo/actors/decay.rb +22 -0
  6. data/lib/legion/extensions/apollo/actors/expertise_aggregator.rb +22 -0
  7. data/lib/legion/extensions/apollo/actors/ingest.rb +25 -0
  8. data/lib/legion/extensions/apollo/actors/query_responder.rb +25 -0
  9. data/lib/legion/extensions/apollo/client.rb +30 -0
  10. data/lib/legion/extensions/apollo/helpers/confidence.rb +46 -0
  11. data/lib/legion/extensions/apollo/helpers/embedding.rb +22 -0
  12. data/lib/legion/extensions/apollo/helpers/graph_query.rb +77 -0
  13. data/lib/legion/extensions/apollo/helpers/similarity.rb +36 -0
  14. data/lib/legion/extensions/apollo/runners/expertise.rb +71 -0
  15. data/lib/legion/extensions/apollo/runners/knowledge.rb +213 -0
  16. data/lib/legion/extensions/apollo/runners/maintenance.rb +72 -0
  17. data/lib/legion/extensions/apollo/transport/exchanges/apollo.rb +19 -0
  18. data/lib/legion/extensions/apollo/transport/messages/ingest.rb +43 -0
  19. data/lib/legion/extensions/apollo/transport/messages/query.rb +43 -0
  20. data/lib/legion/extensions/apollo/transport/queues/ingest.rb +23 -0
  21. data/lib/legion/extensions/apollo/transport/queues/query.rb +23 -0
  22. data/lib/legion/extensions/apollo/version.rb +9 -0
  23. data/lib/legion/extensions/apollo.rb +25 -0
  24. data/spec/legion/extensions/apollo/actors/decay_spec.rb +45 -0
  25. data/spec/legion/extensions/apollo/actors/expertise_aggregator_spec.rb +41 -0
  26. data/spec/legion/extensions/apollo/actors/ingest_spec.rb +33 -0
  27. data/spec/legion/extensions/apollo/client_spec.rb +75 -0
  28. data/spec/legion/extensions/apollo/helpers/confidence_spec.rb +109 -0
  29. data/spec/legion/extensions/apollo/helpers/embedding_spec.rb +68 -0
  30. data/spec/legion/extensions/apollo/helpers/graph_query_spec.rb +69 -0
  31. data/spec/legion/extensions/apollo/helpers/similarity_spec.rb +58 -0
  32. data/spec/legion/extensions/apollo/runners/expertise_spec.rb +111 -0
  33. data/spec/legion/extensions/apollo/runners/knowledge_spec.rb +308 -0
  34. data/spec/legion/extensions/apollo/runners/maintenance_spec.rb +133 -0
  35. data/spec/legion/extensions/apollo/transport/messages/ingest_spec.rb +65 -0
  36. data/spec/legion/extensions/apollo/transport/messages/query_spec.rb +75 -0
  37. data/spec/spec_helper.rb +30 -0
  38. metadata +108 -0
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/apollo/helpers/confidence'
5
+ require 'legion/extensions/apollo/helpers/similarity'
6
+ require 'legion/extensions/apollo/helpers/embedding'
7
+ require 'legion/extensions/apollo/helpers/graph_query'
8
+ require 'legion/extensions/apollo/runners/knowledge'
9
+
10
+ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
11
+ let(:runner) do
12
+ obj = Object.new
13
+ obj.extend(described_class)
14
+ obj
15
+ end
16
+
17
+ describe '#store_knowledge' do
18
+ it 'returns a message payload hash' do
19
+ result = runner.store_knowledge(
20
+ content: 'Vault namespace ash1234 uses PKI',
21
+ content_type: :fact,
22
+ tags: %w[vault pki]
23
+ )
24
+ expect(result).to be_a(Hash)
25
+ expect(result[:action]).to eq(:store)
26
+ expect(result[:content]).to eq('Vault namespace ash1234 uses PKI')
27
+ expect(result[:content_type]).to eq(:fact)
28
+ expect(result[:tags]).to eq(%w[vault pki])
29
+ end
30
+
31
+ it 'includes source_agent when provided' do
32
+ result = runner.store_knowledge(
33
+ content: 'test', content_type: :fact, source_agent: 'worker-1'
34
+ )
35
+ expect(result[:source_agent]).to eq('worker-1')
36
+ end
37
+
38
+ it 'rejects invalid content_type' do
39
+ expect do
40
+ runner.store_knowledge(content: 'test', content_type: :invalid)
41
+ end.to raise_error(ArgumentError, /content_type/)
42
+ end
43
+ end
44
+
45
+ describe '#query_knowledge' do
46
+ it 'returns a query payload hash' do
47
+ result = runner.query_knowledge(query: 'PKI configuration')
48
+ expect(result[:action]).to eq(:query)
49
+ expect(result[:query]).to eq('PKI configuration')
50
+ expect(result[:limit]).to eq(10)
51
+ expect(result[:min_confidence]).to eq(0.3)
52
+ end
53
+
54
+ it 'accepts custom limit and filters' do
55
+ result = runner.query_knowledge(
56
+ query: 'vault', limit: 5, min_confidence: 0.5,
57
+ status: [:confirmed], tags: %w[vault]
58
+ )
59
+ expect(result[:limit]).to eq(5)
60
+ expect(result[:min_confidence]).to eq(0.5)
61
+ expect(result[:status]).to eq([:confirmed])
62
+ expect(result[:tags]).to eq(%w[vault])
63
+ end
64
+ end
65
+
66
+ describe '#related_entries' do
67
+ it 'returns a traversal payload hash' do
68
+ result = runner.related_entries(entry_id: 'uuid-123')
69
+ expect(result[:action]).to eq(:traverse)
70
+ expect(result[:entry_id]).to eq('uuid-123')
71
+ expect(result[:depth]).to eq(2)
72
+ end
73
+
74
+ it 'accepts relation_types filter' do
75
+ result = runner.related_entries(
76
+ entry_id: 'uuid-123', relation_types: %w[causes depends_on], depth: 3
77
+ )
78
+ expect(result[:relation_types]).to eq(%w[causes depends_on])
79
+ expect(result[:depth]).to eq(3)
80
+ end
81
+ end
82
+
83
+ describe '#deprecate_entry' do
84
+ it 'returns a deprecate payload' do
85
+ result = runner.deprecate_entry(entry_id: 'uuid-123', reason: 'outdated')
86
+ expect(result[:action]).to eq(:deprecate)
87
+ expect(result[:entry_id]).to eq('uuid-123')
88
+ expect(result[:reason]).to eq('outdated')
89
+ end
90
+ end
91
+
92
+ describe '#handle_ingest' do
93
+ let(:host) { Object.new.extend(described_class) }
94
+
95
+ context 'when Apollo data is not available' do
96
+ before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
97
+
98
+ it 'returns a structured error' do
99
+ result = host.handle_ingest(content: 'test', content_type: 'fact')
100
+ expect(result[:success]).to be false
101
+ expect(result[:error]).to eq('apollo_data_not_available')
102
+ end
103
+ end
104
+
105
+ context 'when Apollo data is available' do
106
+ let(:mock_entry_class) { double('ApolloEntry') }
107
+ let(:mock_relation_class) { double('ApolloRelation') }
108
+ let(:mock_expertise_class) { double('ApolloExpertise') }
109
+ let(:mock_access_log_class) { double('ApolloAccessLog') }
110
+ let(:mock_entry) { double('entry', id: 'uuid-123', embedding: nil) }
111
+ let(:empty_dataset) { double('dataset', each: nil) }
112
+
113
+ before do
114
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
115
+ stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
116
+ stub_const('Legion::Data::Model::ApolloExpertise', mock_expertise_class)
117
+ stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
118
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
119
+ .and_return(Array.new(1536, 0.0))
120
+
121
+ allow(mock_entry_class).to receive(:where).and_return(double(exclude: double(limit: empty_dataset)))
122
+ allow(mock_entry_class).to receive(:create).and_return(mock_entry)
123
+ allow(mock_expertise_class).to receive(:where).and_return(double(first: nil))
124
+ allow(mock_expertise_class).to receive(:create)
125
+ allow(mock_access_log_class).to receive(:create)
126
+ end
127
+
128
+ it 'creates a new candidate entry when novel' do
129
+ result = host.handle_ingest(content: 'Ruby is great', content_type: 'fact',
130
+ tags: ['ruby'], source_agent: 'agent-1')
131
+ expect(result[:success]).to be true
132
+ expect(result[:status]).to eq('candidate')
133
+ expect(result[:corroborated]).to be false
134
+ end
135
+
136
+ it 'creates expertise record for source agent' do
137
+ expect(mock_expertise_class).to receive(:create).with(
138
+ hash_including(agent_id: 'agent-1', domain: 'ruby')
139
+ )
140
+ host.handle_ingest(content: 'Ruby is great', content_type: 'fact',
141
+ tags: ['ruby'], source_agent: 'agent-1')
142
+ end
143
+
144
+ it 'logs access' do
145
+ expect(mock_access_log_class).to receive(:create).with(
146
+ hash_including(agent_id: 'agent-1', action: 'ingest')
147
+ )
148
+ host.handle_ingest(content: 'Ruby is great', content_type: 'fact',
149
+ tags: ['ruby'], source_agent: 'agent-1')
150
+ end
151
+
152
+ it 'defaults domain to general when no tags' do
153
+ expect(mock_expertise_class).to receive(:create).with(
154
+ hash_including(domain: 'general')
155
+ )
156
+ host.handle_ingest(content: 'test', content_type: 'fact', source_agent: 'agent-1')
157
+ end
158
+ end
159
+
160
+ context 'when Sequel raises an error' do
161
+ before do
162
+ stub_const('Legion::Data::Model::ApolloEntry', Class.new)
163
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
164
+ .and_return(Array.new(1536, 0.0))
165
+ allow(Legion::Data::Model::ApolloEntry).to receive(:where)
166
+ .and_raise(Sequel::Error, 'connection lost')
167
+ end
168
+
169
+ it 'returns a structured error' do
170
+ result = host.handle_ingest(content: 'test', content_type: 'fact', source_agent: 'a')
171
+ expect(result[:success]).to be false
172
+ expect(result[:error]).to eq('connection lost')
173
+ end
174
+ end
175
+ end
176
+
177
+ describe '#handle_query' do
178
+ let(:host) { Object.new.extend(described_class) }
179
+
180
+ context 'when Apollo data is not available' do
181
+ before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
182
+
183
+ it 'returns a structured error' do
184
+ result = host.handle_query(query: 'test')
185
+ expect(result[:success]).to be false
186
+ expect(result[:error]).to eq('apollo_data_not_available')
187
+ end
188
+ end
189
+
190
+ context 'when Apollo data is available' do
191
+ let(:mock_entry_class) { double('ApolloEntry') }
192
+ let(:mock_access_log_class) { double('ApolloAccessLog') }
193
+ let(:mock_db) { double('db') }
194
+ let(:sample_entries) do
195
+ [{ id: 'uuid-1', content: 'Ruby is interpreted', content_type: 'fact',
196
+ confidence: 0.8, distance: 0.15, tags: ['ruby'], source_agent: 'agent-1',
197
+ access_count: 3 }]
198
+ end
199
+
200
+ before do
201
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
202
+ stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
203
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
204
+ .and_return(Array.new(1536, 0.0))
205
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
206
+ allow(mock_db).to receive(:fetch).and_return(double(all: sample_entries))
207
+ allow(mock_entry_class).to receive(:where).and_return(double(update: true))
208
+ allow(mock_access_log_class).to receive(:create)
209
+ end
210
+
211
+ it 'returns matching entries' do
212
+ result = host.handle_query(query: 'Ruby', agent_id: 'agent-2')
213
+ expect(result[:success]).to be true
214
+ expect(result[:count]).to eq(1)
215
+ expect(result[:entries].first[:content]).to eq('Ruby is interpreted')
216
+ end
217
+
218
+ it 'boosts access count on matched entries' do
219
+ expect(mock_entry_class).to receive(:where).with(id: 'uuid-1')
220
+ .and_return(double(update: true))
221
+ host.handle_query(query: 'Ruby', agent_id: 'agent-2')
222
+ end
223
+
224
+ it 'logs query access' do
225
+ expect(mock_access_log_class).to receive(:create).with(
226
+ hash_including(agent_id: 'agent-2', action: 'query')
227
+ )
228
+ host.handle_query(query: 'Ruby', agent_id: 'agent-2')
229
+ end
230
+ end
231
+
232
+ context 'when no results found' do
233
+ let(:mock_entry_class) { double('ApolloEntry') }
234
+ let(:mock_db) { double('db') }
235
+
236
+ before do
237
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
238
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
239
+ .and_return(Array.new(1536, 0.0))
240
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
241
+ allow(mock_db).to receive(:fetch).and_return(double(all: []))
242
+ end
243
+
244
+ it 'returns empty entries array' do
245
+ result = host.handle_query(query: 'nonexistent')
246
+ expect(result[:success]).to be true
247
+ expect(result[:entries]).to eq([])
248
+ expect(result[:count]).to eq(0)
249
+ end
250
+ end
251
+ end
252
+
253
+ describe '#retrieve_relevant' do
254
+ let(:host) { Object.new.extend(described_class) }
255
+
256
+ it 'returns skipped when skip is true' do
257
+ result = host.retrieve_relevant(skip: true)
258
+ expect(result[:status]).to eq(:skipped)
259
+ end
260
+
261
+ context 'when Apollo data is not available' do
262
+ before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
263
+
264
+ it 'returns a structured error' do
265
+ result = host.retrieve_relevant(query: 'test')
266
+ expect(result[:success]).to be false
267
+ end
268
+ end
269
+
270
+ it 'returns empty when query is nil' do
271
+ stub_const('Legion::Data::Model::ApolloEntry', Class.new)
272
+ result = host.retrieve_relevant(query: nil)
273
+ expect(result[:success]).to be true
274
+ expect(result[:entries]).to eq([])
275
+ end
276
+
277
+ it 'returns empty when query is blank' do
278
+ stub_const('Legion::Data::Model::ApolloEntry', Class.new)
279
+ result = host.retrieve_relevant(query: ' ')
280
+ expect(result[:success]).to be true
281
+ expect(result[:entries]).to eq([])
282
+ end
283
+
284
+ context 'with valid query and data available' do
285
+ let(:mock_entry_class) { double('ApolloEntry') }
286
+ let(:mock_db) { double('db') }
287
+ let(:sample_entries) do
288
+ [{ id: 'uuid-1', content: 'fact', content_type: 'fact',
289
+ confidence: 0.7, distance: 0.2, tags: ['ruby'], source_agent: 'agent-1' }]
290
+ end
291
+
292
+ before do
293
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
294
+ allow(Legion::Extensions::Apollo::Helpers::Embedding).to receive(:generate)
295
+ .and_return(Array.new(1536, 0.0))
296
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
297
+ allow(mock_db).to receive(:fetch).and_return(double(all: sample_entries))
298
+ allow(mock_entry_class).to receive(:where).and_return(double(update: true))
299
+ end
300
+
301
+ it 'returns entries without access logging' do
302
+ result = host.retrieve_relevant(query: 'Ruby facts')
303
+ expect(result[:success]).to be true
304
+ expect(result[:count]).to eq(1)
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/apollo/helpers/confidence'
5
+ require 'legion/extensions/apollo/helpers/similarity'
6
+ require 'legion/extensions/apollo/helpers/embedding'
7
+ require 'legion/extensions/apollo/runners/maintenance'
8
+
9
+ RSpec.describe Legion::Extensions::Apollo::Runners::Maintenance do
10
+ let(:runner) do
11
+ obj = Object.new
12
+ obj.extend(described_class)
13
+ obj
14
+ end
15
+
16
+ describe '#force_decay' do
17
+ it 'returns a force_decay payload' do
18
+ result = runner.force_decay(factor: 0.5)
19
+ expect(result[:action]).to eq(:force_decay)
20
+ expect(result[:factor]).to eq(0.5)
21
+ end
22
+ end
23
+
24
+ describe '#archive_stale' do
25
+ it 'returns an archive payload with default days' do
26
+ result = runner.archive_stale
27
+ expect(result[:action]).to eq(:archive_stale)
28
+ expect(result[:days]).to eq(90)
29
+ end
30
+ end
31
+
32
+ describe '#resolve_dispute' do
33
+ it 'returns a resolve payload' do
34
+ result = runner.resolve_dispute(entry_id: 'uuid-123', resolution: :keep)
35
+ expect(result[:action]).to eq(:resolve_dispute)
36
+ expect(result[:resolution]).to eq(:keep)
37
+ end
38
+ end
39
+
40
+ describe '#check_corroboration' do
41
+ let(:host) { Object.new.extend(described_class) }
42
+
43
+ context 'when Apollo data is not available' do
44
+ before { hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry) }
45
+
46
+ it 'returns a structured error' do
47
+ result = host.check_corroboration
48
+ expect(result[:success]).to be false
49
+ expect(result[:error]).to eq('apollo_data_not_available')
50
+ end
51
+ end
52
+
53
+ context 'when no candidates exist' do
54
+ let(:mock_entry_class) { double('ApolloEntry') }
55
+
56
+ before do
57
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
58
+ allow(mock_entry_class).to receive(:where).with(status: 'candidate')
59
+ .and_return(double(exclude: double(all: [])))
60
+ allow(mock_entry_class).to receive(:where).with(status: 'confirmed')
61
+ .and_return(double(exclude: double(all: [])))
62
+ end
63
+
64
+ it 'returns zero promoted and scanned' do
65
+ result = host.check_corroboration
66
+ expect(result[:success]).to be true
67
+ expect(result[:promoted]).to eq(0)
68
+ expect(result[:scanned]).to eq(0)
69
+ end
70
+ end
71
+
72
+ context 'when a candidate matches a confirmed entry' do
73
+ let(:mock_entry_class) { double('ApolloEntry') }
74
+ let(:mock_relation_class) { double('ApolloRelation') }
75
+ let(:embedding) { Array.new(1536, 0.5) }
76
+ let(:candidate) do
77
+ double('candidate', id: 'c-1', content_type: 'fact', embedding: embedding,
78
+ confidence: 0.5, update: true)
79
+ end
80
+ let(:confirmed_entry) do
81
+ double('confirmed', id: 'f-1', content_type: 'fact', embedding: embedding)
82
+ end
83
+
84
+ before do
85
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
86
+ stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
87
+ allow(mock_entry_class).to receive(:where).with(status: 'candidate')
88
+ .and_return(double(exclude: double(all: [candidate])))
89
+ allow(mock_entry_class).to receive(:where).with(status: 'confirmed')
90
+ .and_return(double(exclude: double(all: [confirmed_entry])))
91
+ allow(mock_relation_class).to receive(:create)
92
+ end
93
+
94
+ it 'promotes the candidate' do
95
+ expect(candidate).to receive(:update).with(hash_including(status: 'confirmed'))
96
+ result = host.check_corroboration
97
+ expect(result[:promoted]).to eq(1)
98
+ expect(result[:scanned]).to eq(1)
99
+ end
100
+
101
+ it 'creates a similar_to relation' do
102
+ expect(mock_relation_class).to receive(:create).with(
103
+ hash_including(from_entry_id: 'c-1', to_entry_id: 'f-1', relation_type: 'similar_to')
104
+ )
105
+ host.check_corroboration
106
+ end
107
+ end
108
+
109
+ context 'when candidate has different content_type than confirmed' do
110
+ let(:mock_entry_class) { double('ApolloEntry') }
111
+ let(:embedding) { Array.new(1536, 0.5) }
112
+ let(:candidate) do
113
+ double('candidate', id: 'c-1', content_type: 'fact', embedding: embedding, confidence: 0.5)
114
+ end
115
+ let(:confirmed_entry) do
116
+ double('confirmed', id: 'f-1', content_type: 'concept', embedding: embedding)
117
+ end
118
+
119
+ before do
120
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
121
+ allow(mock_entry_class).to receive(:where).with(status: 'candidate')
122
+ .and_return(double(exclude: double(all: [candidate])))
123
+ allow(mock_entry_class).to receive(:where).with(status: 'confirmed')
124
+ .and_return(double(exclude: double(all: [confirmed_entry])))
125
+ end
126
+
127
+ it 'does not promote' do
128
+ result = host.check_corroboration
129
+ expect(result[:promoted]).to eq(0)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ unless defined?(Legion::Transport::Message)
6
+ module Legion
7
+ module Transport
8
+ class Message
9
+ attr_reader :options
10
+
11
+ def initialize(**opts)
12
+ @options = opts
13
+ end
14
+
15
+ def publish
16
+ { published: true }
17
+ end
18
+ end
19
+
20
+ class Exchange
21
+ def exchange_name
22
+ 'mock'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ $LOADED_FEATURES << 'legion/transport/message' unless $LOADED_FEATURES.include?('legion/transport/message')
28
+ $LOADED_FEATURES << 'legion/transport/exchange' unless $LOADED_FEATURES.include?('legion/transport/exchange')
29
+ end
30
+
31
+ require 'legion/extensions/apollo/transport/exchanges/apollo'
32
+ require 'legion/extensions/apollo/transport/messages/ingest'
33
+
34
+ RSpec.describe Legion::Extensions::Apollo::Transport::Messages::Ingest do
35
+ let(:message) do
36
+ described_class.new(
37
+ content: 'test fact',
38
+ content_type: :fact,
39
+ tags: %w[vault],
40
+ source_agent: 'worker-1'
41
+ )
42
+ end
43
+
44
+ describe '#routing_key' do
45
+ it 'returns apollo.ingest' do
46
+ expect(message.routing_key).to eq('apollo.ingest')
47
+ end
48
+ end
49
+
50
+ describe '#message' do
51
+ it 'includes content and metadata' do
52
+ msg = message.message
53
+ expect(msg[:content]).to eq('test fact')
54
+ expect(msg[:content_type]).to eq(:fact)
55
+ expect(msg[:tags]).to eq(%w[vault])
56
+ expect(msg[:source_agent]).to eq('worker-1')
57
+ end
58
+ end
59
+
60
+ describe '#type' do
61
+ it 'returns apollo_ingest' do
62
+ expect(message.type).to eq('apollo_ingest')
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ unless defined?(Legion::Transport::Message)
6
+ module Legion
7
+ module Transport
8
+ class Message
9
+ attr_reader :options
10
+
11
+ def initialize(**opts)
12
+ @options = opts
13
+ end
14
+
15
+ def publish
16
+ { published: true }
17
+ end
18
+ end
19
+
20
+ class Exchange
21
+ def exchange_name
22
+ 'mock'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ $LOADED_FEATURES << 'legion/transport/message' unless $LOADED_FEATURES.include?('legion/transport/message')
28
+ $LOADED_FEATURES << 'legion/transport/exchange' unless $LOADED_FEATURES.include?('legion/transport/exchange')
29
+ end
30
+
31
+ require 'legion/extensions/apollo/transport/exchanges/apollo'
32
+ require 'legion/extensions/apollo/transport/messages/query'
33
+
34
+ RSpec.describe Legion::Extensions::Apollo::Transport::Messages::Query do
35
+ let(:message) do
36
+ described_class.new(
37
+ action: :query,
38
+ query: 'PKI configuration',
39
+ limit: 5,
40
+ min_confidence: 0.3,
41
+ reply_to: 'reply-queue',
42
+ correlation_id: 'corr-123'
43
+ )
44
+ end
45
+
46
+ describe '#routing_key' do
47
+ it 'returns apollo.query' do
48
+ expect(message.routing_key).to eq('apollo.query')
49
+ end
50
+ end
51
+
52
+ describe '#message' do
53
+ it 'includes query params' do
54
+ msg = message.message
55
+ expect(msg[:action]).to eq(:query)
56
+ expect(msg[:query]).to eq('PKI configuration')
57
+ expect(msg[:limit]).to eq(5)
58
+ expect(msg[:reply_to]).to eq('reply-queue')
59
+ expect(msg[:correlation_id]).to eq('corr-123')
60
+ end
61
+
62
+ it 'compacts nil values' do
63
+ simple = described_class.new(action: :query, query: 'test')
64
+ msg = simple.message
65
+ expect(msg).not_to have_key(:tags)
66
+ expect(msg).not_to have_key(:relation_types)
67
+ end
68
+ end
69
+
70
+ describe '#type' do
71
+ it 'returns apollo_query' do
72
+ expect(message.type).to eq('apollo_query')
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+ def self.info(_msg); end
9
+ def self.warn(_msg); end
10
+ def self.error(_msg); end
11
+ end
12
+ end
13
+
14
+ # Sequel is a runtime dependency via legion-data; stub for specs
15
+ unless defined?(Sequel)
16
+ module Sequel
17
+ class Error < StandardError; end
18
+
19
+ def self.pg_array(arr) = arr
20
+ def self.lit(str) = str
21
+ Expr = Struct.new(:value) { def +(other) = "#{value} + #{other}" }
22
+ def self.expr(sym) = Expr.new(sym)
23
+ end
24
+ end
25
+
26
+ RSpec.configure do |config|
27
+ config.example_status_persistence_file_path = '.rspec_status'
28
+ config.disable_monkey_patching!
29
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
30
+ end